144 lines
5.7 KiB
HTML
144 lines
5.7 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Post</title>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js/styles/github.min.css">
|
|
<script src="https://cdn.jsdelivr.net/npm/highlight.js/lib/common.min.js"></script>
|
|
|
|
<script>
|
|
(function() {
|
|
var stored = localStorage.getItem('theme');
|
|
if (stored === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
|
})();
|
|
|
|
function toggleTheme() {
|
|
var isLight = document.documentElement.getAttribute('data-theme') === 'light';
|
|
if (isLight) {
|
|
document.documentElement.removeAttribute('data-theme');
|
|
localStorage.setItem('theme', 'dark');
|
|
} else {
|
|
document.documentElement.setAttribute('data-theme', 'light');
|
|
localStorage.setItem('theme', 'light');
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<link rel="stylesheet" href="styles/master.css">
|
|
|
|
</head>
|
|
<body>
|
|
|
|
<div class="wrap">
|
|
<a class="back-link" href="index.html">← Return home</a>
|
|
<button id="theme-toggle" onclick="toggleTheme()">toggle light/dark</button>
|
|
<p class="meta" id="meta" hidden></p>
|
|
<div id="content"><p class="state">Loading post…</p></div>
|
|
</div>
|
|
|
|
<script>
|
|
function parseFrontMatter(raw) {
|
|
const match = raw.match(/^---\r?\n([\s\S]+?)\r?\n---\r?\n([\s\S]*)$/);
|
|
if (!match) return { meta: {}, body: raw };
|
|
|
|
const meta = {};
|
|
match[1].split(/\r?\n/).forEach(line => {
|
|
const idx = line.indexOf(':');
|
|
if (idx === -1) return;
|
|
const key = line.slice(0, idx).trim();
|
|
let value = line.slice(idx + 1).trim();
|
|
// strip optional surrounding quotes: title: "My Post"
|
|
value = value.replace(/^["'](.*)["']$/, '$1');
|
|
meta[key] = value;
|
|
});
|
|
|
|
return { meta, body: match[2] };
|
|
}
|
|
|
|
function formatDate(dateStr) {
|
|
if (!dateStr) return '';
|
|
const d = new Date(dateStr);
|
|
if (isNaN(d)) return dateStr; // fall back to raw string if unparseable
|
|
return d.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' });
|
|
}
|
|
|
|
function showError(message) {
|
|
document.getElementById('content').innerHTML =
|
|
`<p class="state error">${message}</p>`;
|
|
}
|
|
|
|
// ---- main ------------------------------------------------------------
|
|
|
|
marked.setOptions({
|
|
highlight: (code, lang) => {
|
|
try {
|
|
return lang
|
|
? hljs.highlight(code, { language: lang }).value
|
|
: hljs.highlightAuto(code).value;
|
|
} catch (e) {
|
|
return code;
|
|
}
|
|
}
|
|
});
|
|
|
|
async function loadPost() {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const file = params.get('file');
|
|
|
|
if (!file) {
|
|
showError('No post specified. Link to this page as <code>post.html?file=your-post.md</code>.');
|
|
return;
|
|
}
|
|
|
|
// Guard against path traversal — keep the fetch confined to posts/
|
|
if (file.includes('..') || file.startsWith('/')) {
|
|
showError('Invalid post path.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch('posts/' + file);
|
|
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
const raw = await res.text();
|
|
|
|
const { meta, body } = parseFrontMatter(raw);
|
|
const html = marked.parse(body);
|
|
|
|
document.getElementById('content').innerHTML = html;
|
|
|
|
// Title: front matter > first H1 in the doc > filename
|
|
let title = meta.title;
|
|
if (!title) {
|
|
const h1 = document.querySelector('#content h1');
|
|
title = h1 ? h1.textContent : file.replace(/\.md$/, '');
|
|
}
|
|
document.title = title;
|
|
|
|
// Inject an H1 if front matter supplied a title but the body didn't have one
|
|
if (meta.title && !document.querySelector('#content h1:first-child')) {
|
|
const h1 = document.createElement('h1');
|
|
h1.textContent = meta.title;
|
|
document.getElementById('content').prepend(h1);
|
|
}
|
|
|
|
// Meta line: date (and tags, if present)
|
|
const metaEl = document.getElementById('meta');
|
|
const parts = [];
|
|
if (meta.date) parts.push(formatDate(meta.date));
|
|
if (meta.tags) parts.push(meta.tags.split(',').map(t => t.trim()).join(' · '));
|
|
if (parts.length) {
|
|
metaEl.innerHTML = parts.join(' / ');
|
|
metaEl.hidden = false;
|
|
}
|
|
} catch (err) {
|
|
showError('Could not load this post. It may have been moved or deleted.');
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', loadPost);
|
|
</script>
|
|
</body>
|
|
</html> |