testing dark/light mode
This commit is contained in:
371
post.html
371
post.html
@@ -1,279 +1,144 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Post</title>
|
||||
<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 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>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--ink: #26241f;
|
||||
--ink-soft: #6b6a63;
|
||||
--paper: #fbfaf7;
|
||||
--rule: #e4e1d8;
|
||||
--accent: #3a5a40;
|
||||
--code-bg: #f1efe8;
|
||||
}
|
||||
<script>
|
||||
(function() {
|
||||
var stored = localStorage.getItem('theme');
|
||||
if (stored === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
||||
})();
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
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>
|
||||
|
||||
html { -webkit-text-size-adjust: 100%; }
|
||||
<link rel="stylesheet" href="styles/main.css">
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
font-family: Georgia, 'Iowan Old Style', 'Palatino Linotype', serif;
|
||||
line-height: 1.7;
|
||||
}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
.wrap {
|
||||
max-width: 680px;
|
||||
margin: 0 auto;
|
||||
padding: 4rem 1.5rem 6rem;
|
||||
}
|
||||
<div class="wrap">
|
||||
<button id="theme-toggle" onclick="toggleTheme()">toggle theme</button>
|
||||
<a class="back-link" href="index.html">← Return home</a>
|
||||
<p class="meta" id="meta" hidden></p>
|
||||
<div id="content"><p class="state">Loading post…</p></div>
|
||||
</div>
|
||||
|
||||
.back-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
font-size: 0.85rem;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ink-soft);
|
||||
text-decoration: none;
|
||||
margin-bottom: 3rem;
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: border-color 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
.back-link:hover {
|
||||
color: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
<script>
|
||||
function parseFrontMatter(raw) {
|
||||
const match = raw.match(/^---\r?\n([\s\S]+?)\r?\n---\r?\n([\s\S]*)$/);
|
||||
if (!match) return { meta: {}, body: raw };
|
||||
|
||||
.meta {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
font-size: 0.85rem;
|
||||
color: var(--ink-soft);
|
||||
margin: 0 0 0.5rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
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;
|
||||
});
|
||||
|
||||
#content h1:first-child {
|
||||
font-size: 2.1rem;
|
||||
line-height: 1.25;
|
||||
margin: 0 0 0.75rem;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
return { meta, body: match[2] };
|
||||
}
|
||||
|
||||
#content h1, #content h2, #content h3 {
|
||||
font-family: Georgia, serif;
|
||||
color: var(--ink);
|
||||
margin-top: 2.2em;
|
||||
margin-bottom: 0.6em;
|
||||
}
|
||||
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' });
|
||||
}
|
||||
|
||||
#content h2 { font-size: 1.5rem; border-bottom: 1px solid var(--rule); padding-bottom: 0.3em; }
|
||||
#content h3 { font-size: 1.2rem; }
|
||||
function showError(message) {
|
||||
document.getElementById('content').innerHTML =
|
||||
`<p class="state error">${message}</p>`;
|
||||
}
|
||||
|
||||
#content p { margin: 1.1em 0; }
|
||||
// ---- main ------------------------------------------------------------
|
||||
|
||||
#content a { color: var(--accent); text-decoration-color: rgba(58,90,64,0.35); }
|
||||
#content a:hover { text-decoration-color: var(--accent); }
|
||||
marked.setOptions({
|
||||
highlight: (code, lang) => {
|
||||
try {
|
||||
return lang
|
||||
? hljs.highlight(code, { language: lang }).value
|
||||
: hljs.highlightAuto(code).value;
|
||||
} catch (e) {
|
||||
return code;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
#content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
margin: 1.5em 0;
|
||||
}
|
||||
async function loadPost() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const file = params.get('file');
|
||||
|
||||
#content blockquote {
|
||||
margin: 1.5em 0;
|
||||
padding: 0.2em 1.2em;
|
||||
border-left: 3px solid var(--accent);
|
||||
color: var(--ink-soft);
|
||||
font-style: italic;
|
||||
}
|
||||
if (!file) {
|
||||
showError('No post specified. Link to this page as <code>post.html?file=your-post.md</code>.');
|
||||
return;
|
||||
}
|
||||
|
||||
#content ul, #content ol {
|
||||
padding-left: 1.4em;
|
||||
}
|
||||
#content li { margin: 0.4em 0; }
|
||||
// Guard against path traversal — keep the fetch confined to posts/
|
||||
if (file.includes('..') || file.startsWith('/')) {
|
||||
showError('Invalid post path.');
|
||||
return;
|
||||
}
|
||||
|
||||
#content hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--rule);
|
||||
margin: 2.5em 0;
|
||||
}
|
||||
try {
|
||||
const res = await fetch('posts/' + file);
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
const raw = await res.text();
|
||||
|
||||
#content code {
|
||||
font-family: 'SF Mono', Menlo, Consolas, monospace;
|
||||
font-size: 0.85em;
|
||||
background: var(--code-bg);
|
||||
padding: 0.15em 0.4em;
|
||||
border-radius: 3px;
|
||||
}
|
||||
const { meta, body } = parseFrontMatter(raw);
|
||||
const html = marked.parse(body);
|
||||
|
||||
#content pre {
|
||||
background: var(--code-bg);
|
||||
padding: 1em 1.2em;
|
||||
border-radius: 6px;
|
||||
overflow-x: auto;
|
||||
line-height: 1.5;
|
||||
}
|
||||
#content pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
document.getElementById('content').innerHTML = html;
|
||||
|
||||
#content table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1.5em 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
#content th, #content td {
|
||||
text-align: left;
|
||||
padding: 0.5em 0.8em;
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
#content th { color: var(--ink-soft); font-weight: 600; }
|
||||
// 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;
|
||||
|
||||
.state {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||
color: var(--ink-soft);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
|
||||
.state.error { color: #a3413a; }
|
||||
// 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.');
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.wrap { animation: rise 0.35s ease-out both; }
|
||||
@keyframes rise {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="wrap">
|
||||
<a class="back-link" href="index.html">← All posts</a>
|
||||
<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>
|
||||
document.addEventListener('DOMContentLoaded', loadPost);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user