rethem
This commit is contained in:
parent
87f599a01e
commit
f8b3194199
|
|
@ -0,0 +1 @@
|
||||||
|
[directory]
|
||||||
|
|
@ -1,20 +1,29 @@
|
||||||
/* content/default.css */
|
/* content/default.css */
|
||||||
|
|
||||||
/*
|
|
||||||
The "27bslash6" Aesthetic:
|
|
||||||
- Stark Black & White
|
|
||||||
- Helvetica / Arial
|
|
||||||
- Narrow, centered reading column
|
|
||||||
- Zero border radius, zero shadows
|
|
||||||
*/
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg-color: #ffffff;
|
--bg-color: #ffffff;
|
||||||
--text-color: #000000;
|
--header-bg: #000000;
|
||||||
--link-color: #cc0000; /* That specific "angry" red he sometimes uses, or just black */
|
--header-text: #ffffff;
|
||||||
--meta-color: #666666;
|
--text-color: #333333;
|
||||||
--font-stack: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
--link-color: #000000;
|
||||||
--max-width: 680px; /* The classic narrow column */
|
--accent-color: #cc0000;
|
||||||
|
--sidebar-bg: #ffffff;
|
||||||
|
--sidebar-border: #000000;
|
||||||
|
--font-primary: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
--font-mono: "Monaco", "Menlo", monospace;
|
||||||
|
--content-width: 680px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode {
|
||||||
|
--bg-color: #1a1a1a;
|
||||||
|
--header-bg: #000000;
|
||||||
|
/* Keep header black, maybe slightly lighter? User said "dark theme" */
|
||||||
|
--header-text: #e0e0e0;
|
||||||
|
--text-color: #e0e0e0;
|
||||||
|
--link-color: #ffffff;
|
||||||
|
--accent-color: #ff4444;
|
||||||
|
--sidebar-bg: #2a2a2a;
|
||||||
|
--sidebar-border: #555555;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|
@ -22,216 +31,279 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
font-family: var(--font-stack);
|
font-family: var(--font-primary);
|
||||||
font-size: 15px; /* Slightly smaller, crisp text */
|
font-size: 18px;
|
||||||
line-height: 1.5;
|
line-height: 1.6;
|
||||||
margin: 0;
|
-webkit-font-smoothing: antialiased;
|
||||||
padding: 2rem 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The main document container */
|
/* Header - Stark Black Block */
|
||||||
body > * {
|
/* Ensure header text color uses variable */
|
||||||
max-width: var(--max-width);
|
.main-header {
|
||||||
margin-left: auto;
|
background-color: var(--header-bg);
|
||||||
margin-right: auto;
|
color: var(--header-text);
|
||||||
display: block;
|
/* Make header taller per request */
|
||||||
|
padding: 6rem 1rem;
|
||||||
|
margin-bottom: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
max-width: var(--content-width);
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 2rem;
|
||||||
|
/* Space between logo and text */
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-logo img {
|
||||||
|
height: 80px;
|
||||||
|
/* Adjust as needed */
|
||||||
|
width: auto;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-header h1 {
|
||||||
|
margin: 0;
|
||||||
|
/* Make title bigger per request */
|
||||||
|
font-size: 4rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
line-height: 1;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content Container */
|
||||||
|
.main-content {
|
||||||
|
max-width: var(--content-width);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 1rem 8rem 1rem;
|
||||||
|
/* Bottom padding for scroll space */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Typography */
|
/* Typography */
|
||||||
h1, h2, h3 {
|
/* Update headers to use text color variable */
|
||||||
font-weight: bold;
|
h1,
|
||||||
letter-spacing: -0.5px;
|
h2,
|
||||||
margin-top: 2.5rem;
|
h3,
|
||||||
margin-bottom: 1rem;
|
h4 {
|
||||||
|
color: var(--text-color);
|
||||||
|
margin-top: 2.5em;
|
||||||
|
margin-bottom: 0.8em;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
/* First Line Logic - "Sugar" */
|
||||||
font-size: 24px;
|
/* Update first paragraph color */
|
||||||
border-bottom: 1px solid #000;
|
.main-content>p:first-of-type {
|
||||||
padding-bottom: 10px;
|
font-size: 1.3em;
|
||||||
margin-top: 0;
|
color: var(--text-color);
|
||||||
|
/* Was hardcoded #000 */
|
||||||
|
margin-bottom: 2em;
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
/* Dark Mode Toggle Button */
|
||||||
font-size: 18px;
|
#theme-toggle {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--sidebar-border);
|
||||||
|
color: var(--text-color);
|
||||||
|
padding: 5px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: var(--font-primary);
|
||||||
|
margin-top: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#theme-toggle:hover {
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
color: #fff;
|
||||||
|
border-color: var(--accent-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin-bottom: 1.2rem;
|
margin-bottom: 1.5em;
|
||||||
text-align: justify; /* Gives it that "formal letter" look */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Links - Stark and obvious */
|
|
||||||
a {
|
a {
|
||||||
color: var(--text-color);
|
color: var(--link-color);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-decoration-thickness: 1px;
|
text-decoration-thickness: 1px;
|
||||||
|
text-underline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
color: var(--link-color);
|
color: var(--accent-color);
|
||||||
text-decoration: none;
|
text-decoration-color: var(--accent-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Code blocks - Raw and industrial */
|
/* Images - Full width of container */
|
||||||
pre {
|
|
||||||
background: #f0f0f0;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
padding: 15px;
|
|
||||||
font-family: "Monaco", "Menlo", "Consolas", monospace;
|
|
||||||
font-size: 12px;
|
|
||||||
overflow-x: auto;
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-family: "Monaco", "Menlo", "Consolas", monospace;
|
|
||||||
background: #f0f0f0;
|
|
||||||
padding: 2px 4px;
|
|
||||||
font-size: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Blockquotes - The "Email Reply" look */
|
|
||||||
blockquote {
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
padding-left: 15px;
|
|
||||||
border-left: 4px solid #000;
|
|
||||||
color: var(--meta-color);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Images - fit the column */
|
|
||||||
img {
|
img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
display: block;
|
display: block;
|
||||||
margin: 2rem auto;
|
margin: 3rem auto;
|
||||||
border: 1px solid #000; /* Optional: adds to the brutalist feel */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Lists */
|
/* Code */
|
||||||
ul, ol {
|
pre {
|
||||||
padding-left: 20px;
|
background: #f4f4f4;
|
||||||
margin-bottom: 1.5rem;
|
padding: 1.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: 0.9em;
|
||||||
|
border-radius: 4px;
|
||||||
|
/* Slight softening */
|
||||||
|
margin: 2rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
code {
|
||||||
margin-bottom: 0.5rem;
|
font-family: var(--font-mono);
|
||||||
|
background: #f4f4f4;
|
||||||
|
padding: 2px 4px;
|
||||||
|
font-size: 0.9em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
blockquote {
|
||||||
Specific Components
|
border-left: 3px solid #000;
|
||||||
*/
|
margin: 2rem 0;
|
||||||
|
padding-left: 1.5rem;
|
||||||
/* Top Banner - The "System Notice" Style
|
font-style: italic;
|
||||||
*/
|
color: #666;
|
||||||
.topbanner {
|
|
||||||
background-color: #f4f4f4; /* Very light grey, distinct from white body */
|
|
||||||
border: 1px solid #000; /* Crisp definition */
|
|
||||||
border-left-width: 6px; /* Thick accent, matches your sidebar theme */
|
|
||||||
padding: 1.5rem 2rem; /* Generous padding for readability */
|
|
||||||
margin-bottom: 3rem; /* Push the H1 down */
|
|
||||||
font-size: 14px; /* Slightly smaller than body text */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure text inside the banner doesn't inherit the 'justify' from global p */
|
/* Sidebar - The "Ghost" Panel */
|
||||||
.topbanner p {
|
|
||||||
text-align: left;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbanner p:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Optional: Make links inside the banner aggressive */
|
|
||||||
.topbanner a {
|
|
||||||
font-weight: bold;
|
|
||||||
border-bottom: 2px solid var(--link-color);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbanner a:hover {
|
|
||||||
background-color: var(--link-color);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* The Site Headline (Top of Index) */
|
|
||||||
.site-headline {
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.site-headline h1 {
|
|
||||||
font-size: 32px;
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
letter-spacing: -1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sidebar Styles */
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
float: right;
|
position: fixed;
|
||||||
width: 30%;
|
right: 2rem;
|
||||||
min-width: 250px;
|
bottom: 2rem;
|
||||||
margin-left: 2rem;
|
width: 300px;
|
||||||
margin-bottom: 1rem;
|
background: var(--sidebar-bg);
|
||||||
padding: 1rem;
|
border: 1px solid var(--sidebar-border);
|
||||||
background-color: #f9f9f9;
|
padding: 2rem;
|
||||||
border-left: 4px solid #333;
|
opacity: 0;
|
||||||
|
/* Default hidden */
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
z-index: 100;
|
||||||
|
box-shadow: 10px 10px 0px rgba(0, 0, 0, 0.1);
|
||||||
|
/* 27bslash6 vibe */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The Latest Posts List */
|
.sidebar.force-visible {
|
||||||
.latest-posts {
|
opacity: 1 !important;
|
||||||
margin-top: 3rem;
|
pointer-events: auto !important;
|
||||||
border-top: 4px solid #000;
|
}
|
||||||
padding-top: 1rem;
|
|
||||||
|
#close-sidebar {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 10px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.latest-posts h2 {
|
.latest-posts h2 {
|
||||||
font-size: 14px;
|
margin-top: 0;
|
||||||
|
font-size: 1rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--meta-color);
|
border-bottom: 2px solid var(--sidebar-border);
|
||||||
margin-bottom: 1rem;
|
padding-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.latest-posts ul {
|
.latest-posts ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.latest-posts li {
|
.latest-posts li {
|
||||||
|
margin-bottom: 0.8rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
flex-direction: column;
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
padding: 8px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.latest-posts .date {
|
.latest-posts .date {
|
||||||
color: var(--meta-color);
|
font-size: 0.75rem;
|
||||||
font-size: 12px;
|
color: #888;
|
||||||
margin-right: 1rem;
|
margin-bottom: 2px;
|
||||||
min-width: 100px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.latest-posts a {
|
.latest-posts a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: bold;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.latest-posts a:hover {
|
.latest-posts a:hover {
|
||||||
text-decoration: underline;
|
color: var(--accent-color);
|
||||||
color: var(--link-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Directory Link */
|
|
||||||
.directory-link {
|
.directory-link {
|
||||||
margin: 2rem 0;
|
margin-top: 1.5rem;
|
||||||
text-align: right;
|
text-align: center;
|
||||||
font-size: 12px;
|
font-size: 0.9rem;
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Trigger */
|
||||||
|
#read-more-trigger {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
background: #000;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transform: translateY(100%);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
z-index: 90;
|
||||||
|
display: none;
|
||||||
|
/* Desktop default hidden */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Tweaks */
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.sidebar {
|
||||||
|
display: none;
|
||||||
|
/* Hide standard sidebar on mobile */
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.force-visible {
|
||||||
|
display: block;
|
||||||
|
left: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
bottom: 4rem;
|
||||||
|
/* Above trigger */
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#read-more-trigger {
|
||||||
|
display: block;
|
||||||
|
/* Active on mobile */
|
||||||
|
}
|
||||||
|
|
||||||
|
#read-more-trigger.visible {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-header h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
title: "Math Test"
|
||||||
|
---
|
||||||
|
|
||||||
|
This is a test of **MathJax** support.
|
||||||
|
|
||||||
|
Inline math: $E=mc^2$ is the theory of special relativity.
|
||||||
|
|
||||||
|
Block math:
|
||||||
|
$$
|
||||||
|
\sum_{i=1}^{n} i = \frac{n(n+1)}{2}
|
||||||
|
$$
|
||||||
|
|
||||||
|
Another block:
|
||||||
|
$$
|
||||||
|
\int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi}
|
||||||
|
$$
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const sidebar = document.getElementById('sidebar');
|
||||||
|
const closeBtn = document.getElementById('close-sidebar');
|
||||||
|
const trigger = document.getElementById('read-more-trigger');
|
||||||
|
const body = document.body;
|
||||||
|
let manualClose = false;
|
||||||
|
|
||||||
|
// --- Sidebar Logic ---
|
||||||
|
|
||||||
|
function checkSidebarVisibility() {
|
||||||
|
if (manualClose) return;
|
||||||
|
|
||||||
|
const scrollTop = window.scrollY;
|
||||||
|
const windowHeight = window.innerHeight;
|
||||||
|
const docHeight = document.documentElement.scrollHeight;
|
||||||
|
|
||||||
|
// "Sidebar fix": Check if page is short (content fits in window or close to it)
|
||||||
|
// If content is short, sidebar should be visible immediately (if desktop)
|
||||||
|
if (docHeight <= windowHeight * 1.2 && window.innerWidth > 900) {
|
||||||
|
sidebar.classList.add('visible');
|
||||||
|
sidebar.style.opacity = 1;
|
||||||
|
sidebar.style.pointerEvents = 'auto';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll Logic for longer pages
|
||||||
|
if (scrollTop + windowHeight > docHeight * 0.9) {
|
||||||
|
sidebar.classList.add('visible');
|
||||||
|
trigger.classList.add('visible');
|
||||||
|
|
||||||
|
// Dynamic Opacity near bottom
|
||||||
|
const remaining = docHeight - (scrollTop + windowHeight);
|
||||||
|
const threshold = docHeight * 0.1;
|
||||||
|
|
||||||
|
if (remaining < threshold) {
|
||||||
|
const opacity = 1 - (remaining / threshold);
|
||||||
|
sidebar.style.opacity = Math.min(Math.max(opacity, 0), 1);
|
||||||
|
|
||||||
|
if (opacity > 0) {
|
||||||
|
sidebar.style.pointerEvents = 'auto';
|
||||||
|
} else {
|
||||||
|
sidebar.style.pointerEvents = 'none';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Should be redundant with visibility check but safe
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Hide if scrolled up?
|
||||||
|
// "It just is gone then... manual close... refresh if want it back"
|
||||||
|
// But scroll behavior implies transient visibility.
|
||||||
|
// We'll hide it if we scroll back up, unless it's short content.
|
||||||
|
sidebar.style.opacity = 0;
|
||||||
|
sidebar.style.pointerEvents = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('scroll', checkSidebarVisibility);
|
||||||
|
window.addEventListener('resize', checkSidebarVisibility);
|
||||||
|
|
||||||
|
// Initial check
|
||||||
|
setTimeout(checkSidebarVisibility, 100); // Slight delay for rendering
|
||||||
|
|
||||||
|
// Mobile Trigger
|
||||||
|
trigger.addEventListener('click', () => {
|
||||||
|
sidebar.classList.add('force-visible');
|
||||||
|
sidebar.style.opacity = 1;
|
||||||
|
sidebar.style.pointerEvents = 'auto';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close Button
|
||||||
|
closeBtn.addEventListener('click', () => {
|
||||||
|
sidebar.classList.remove('visible');
|
||||||
|
sidebar.classList.remove('force-visible');
|
||||||
|
sidebar.style.opacity = 0;
|
||||||
|
sidebar.style.pointerEvents = 'none';
|
||||||
|
manualClose = true;
|
||||||
|
trigger.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Dark Mode Logic ---
|
||||||
|
|
||||||
|
// Create Toggle Button
|
||||||
|
const sidebarContent = document.querySelector('.sidebar-content');
|
||||||
|
const themeBtn = document.createElement('button');
|
||||||
|
themeBtn.id = 'theme-toggle';
|
||||||
|
themeBtn.textContent = 'Toggle Theme';
|
||||||
|
sidebarContent.appendChild(themeBtn);
|
||||||
|
|
||||||
|
function setDarkMode(enable) {
|
||||||
|
if (enable) {
|
||||||
|
body.classList.add('dark-mode');
|
||||||
|
themeBtn.textContent = 'Switch to Light Mode';
|
||||||
|
} else {
|
||||||
|
body.classList.remove('dark-mode');
|
||||||
|
themeBtn.textContent = 'Switch to Dark Mode';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCookie(name) {
|
||||||
|
const v = document.cookie.match('(^|;) ?' + name + '=([^;]*)(;|$)');
|
||||||
|
return v ? v[2] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCookie(name, value, hours) {
|
||||||
|
const d = new Date();
|
||||||
|
d.setTime(d.getTime() + (hours * 60 * 60 * 1000));
|
||||||
|
document.cookie = name + "=" + value + ";path=/;expires=" + d.toUTCString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init Dark Mode
|
||||||
|
const cookieTheme = getCookie('theme');
|
||||||
|
if (cookieTheme === 'dark') {
|
||||||
|
setDarkMode(true);
|
||||||
|
} else if (cookieTheme === 'light') {
|
||||||
|
setDarkMode(false);
|
||||||
|
} else {
|
||||||
|
// Time based auto-detect
|
||||||
|
const hour = new Date().getHours();
|
||||||
|
if (hour < 6 || hour >= 18) {
|
||||||
|
setDarkMode(true);
|
||||||
|
} else {
|
||||||
|
setDarkMode(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
themeBtn.addEventListener('click', () => {
|
||||||
|
const isDark = body.classList.contains('dark-mode');
|
||||||
|
setDarkMode(!isDark);
|
||||||
|
setCookie('theme', !isDark ? 'dark' : 'light', 24);
|
||||||
|
});
|
||||||
|
});
|
||||||
1
go.mod
1
go.mod
|
|
@ -8,6 +8,7 @@ require go.abhg.dev/goldmark/frontmatter v0.3.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||||
|
github.com/litao91/goldmark-mathjax v0.0.0-20210217064022-a43cf739a50f // indirect
|
||||||
github.com/tendstofortytwo/goldmark-customtag v0.0.1 // indirect
|
github.com/tendstofortytwo/goldmark-customtag v0.0.1 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
7
go.sum
7
go.sum
|
|
@ -1,18 +1,25 @@
|
||||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/litao91/goldmark-mathjax v0.0.0-20210217064022-a43cf739a50f h1:plCPYXRXDCO57qjqegCzaVf1t6aSbgCMD+zfz18POfs=
|
||||||
|
github.com/litao91/goldmark-mathjax v0.0.0-20210217064022-a43cf739a50f/go.mod h1:leg+HM7jUS84JYuY120zmU68R6+UeU6uZ/KAW7cViKE=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/tendstofortytwo/goldmark-customtag v0.0.1 h1:c3Wnzi98gE7BPaqio/UOQVGGJU0EMdgKwjQHo9Y7NKQ=
|
github.com/tendstofortytwo/goldmark-customtag v0.0.1 h1:c3Wnzi98gE7BPaqio/UOQVGGJU0EMdgKwjQHo9Y7NKQ=
|
||||||
github.com/tendstofortytwo/goldmark-customtag v0.0.1/go.mod h1:9KHP0WeHIafExF+Zu4CIDfzgfMo/FaEKXUO7NFmCyos=
|
github.com/tendstofortytwo/goldmark-customtag v0.0.1/go.mod h1:9KHP0WeHIafExF+Zu4CIDfzgfMo/FaEKXUO7NFmCyos=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
go.abhg.dev/goldmark/frontmatter v0.3.0 h1:ZOrMkeyyYzhlbenFNmOXyGFx1dFE8TgBWAgZfs9D5RA=
|
go.abhg.dev/goldmark/frontmatter v0.3.0 h1:ZOrMkeyyYzhlbenFNmOXyGFx1dFE8TgBWAgZfs9D5RA=
|
||||||
go.abhg.dev/goldmark/frontmatter v0.3.0/go.mod h1:W3KXvVveKKxU1FIFZ7fgFFQrlkcolnDcOVmu19cCO9U=
|
go.abhg.dev/goldmark/frontmatter v0.3.0/go.mod h1:W3KXvVveKKxU1FIFZ7fgFFQrlkcolnDcOVmu19cCO9U=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
||||||
458
parse.go
458
parse.go
|
|
@ -8,40 +8,24 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.else-if.org/jess/blog/templates"
|
"git.else-if.org/jess/blog/templates"
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
mathjax "github.com/litao91/goldmark-mathjax"
|
||||||
"github.com/yuin/goldmark"
|
"github.com/yuin/goldmark"
|
||||||
"github.com/yuin/goldmark/extension"
|
"github.com/yuin/goldmark/extension"
|
||||||
"github.com/yuin/goldmark/parser"
|
"github.com/yuin/goldmark/parser"
|
||||||
"github.com/yuin/goldmark/renderer/html"
|
"github.com/yuin/goldmark/renderer/html"
|
||||||
"go.abhg.dev/goldmark/frontmatter"
|
"go.abhg.dev/goldmark/frontmatter"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetContentPath ensures the content is cached and returns the path to the cached file.
|
// GetContentPath ensures the content is cached and returns the path to the cached file.
|
||||||
func GetContentPath(file ContentFile) (string, error) {
|
func GetContentPath(file ContentFile) (string, error) {
|
||||||
cachePath := GetCacheFilename(file.OriginalPath)
|
cachePath := GetCacheFilename(file.OriginalPath)
|
||||||
|
|
||||||
// 1. Check Cache Freshness (if not in TestMode)
|
|
||||||
if !TestMode {
|
|
||||||
cacheInfo, errCache := os.Stat(cachePath)
|
|
||||||
origInfo, errOrig := os.Stat(file.OriginalPath)
|
|
||||||
|
|
||||||
if errCache == nil && errOrig == nil && cacheInfo.ModTime().After(origInfo.ModTime()) {
|
|
||||||
return cachePath, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Regenerate Content
|
|
||||||
cacheMutex.Lock()
|
|
||||||
defer cacheMutex.Unlock()
|
|
||||||
|
|
||||||
if !TestMode {
|
|
||||||
cacheInfo, errCache := os.Stat(cachePath)
|
|
||||||
origInfo, errOrig := os.Stat(file.OriginalPath)
|
|
||||||
if errCache == nil && errOrig == nil && cacheInfo.ModTime().After(origInfo.ModTime()) {
|
|
||||||
return cachePath, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read Raw File
|
// Read Raw File
|
||||||
raw, err := ReadRaw(file.OriginalPath)
|
raw, err := ReadRaw(file.OriginalPath)
|
||||||
|
|
@ -59,9 +43,9 @@ func GetContentPath(file ContentFile) (string, error) {
|
||||||
goldmark.WithExtensions(
|
goldmark.WithExtensions(
|
||||||
extension.GFM,
|
extension.GFM,
|
||||||
&frontmatter.Extender{},
|
&frontmatter.Extender{},
|
||||||
|
mathjax.MathJax,
|
||||||
templates.SidebarTag, // ||| -> <sidebar>
|
templates.SidebarTag, // ||| -> <sidebar>
|
||||||
templates.TopBanner,
|
templates.TopBanner,
|
||||||
templates.Mathtext,
|
|
||||||
),
|
),
|
||||||
goldmark.WithRendererOptions(
|
goldmark.WithRendererOptions(
|
||||||
html.WithUnsafe(),
|
html.WithUnsafe(),
|
||||||
|
|
@ -75,11 +59,67 @@ func GetContentPath(file ContentFile) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract Frontmatter
|
// Extract Frontmatter
|
||||||
|
// 1. Initialize with External Metadata (if any)
|
||||||
var meta templates.PageMetadata
|
var meta templates.PageMetadata
|
||||||
|
if err := loadExternalMetadata(file.OriginalPath, &meta); err != nil {
|
||||||
|
fmt.Printf("Warning: failed to load external metadata for %s: %v\n", file.OriginalPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Parse Internal Frontmatter (overrides external)
|
||||||
d := frontmatter.Get(ctx)
|
d := frontmatter.Get(ctx)
|
||||||
if d != nil {
|
if d != nil {
|
||||||
if err := d.Decode(&meta); err != nil {
|
// Decode into a temporary map to capture all fields
|
||||||
|
var rawMap map[string]interface{}
|
||||||
|
if err := d.Decode(&rawMap); err != nil {
|
||||||
fmt.Printf("Warning: failed to decode frontmatter for %s: %v\n", file.OriginalPath, err)
|
fmt.Printf("Warning: failed to decode frontmatter for %s: %v\n", file.OriginalPath, err)
|
||||||
|
} else {
|
||||||
|
// Merge into meta
|
||||||
|
if meta.Raw == nil {
|
||||||
|
meta.Raw = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
for k, v := range rawMap {
|
||||||
|
meta.Raw[k] = v
|
||||||
|
// Also update struct fields manually if needed, or use layout decoding
|
||||||
|
switch strings.ToLower(k) {
|
||||||
|
case "title":
|
||||||
|
if s, ok := v.(string); ok {
|
||||||
|
meta.Title = s
|
||||||
|
}
|
||||||
|
case "stylesheet":
|
||||||
|
if s, ok := v.(string); ok {
|
||||||
|
meta.Stylesheet = s
|
||||||
|
}
|
||||||
|
case "style":
|
||||||
|
if s, ok := v.(string); ok {
|
||||||
|
meta.Style = s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Resolve Stylesheet Path
|
||||||
|
if meta.Stylesheet != "" {
|
||||||
|
// If not absolute path (starting with /), resolve relative to content file
|
||||||
|
if !strings.HasPrefix(meta.Stylesheet, "/") {
|
||||||
|
// Get valid web path for the directory containing the file
|
||||||
|
// file.RoutePath is like "/entries/entry1" or "/index"
|
||||||
|
// We want the directory part of the RoutePath
|
||||||
|
|
||||||
|
dirRoute := filepath.Dir(file.RoutePath)
|
||||||
|
if dirRoute == "." {
|
||||||
|
dirRoute = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct new path
|
||||||
|
// e.g. /entries + / + style.css -> /entries/style.css
|
||||||
|
// Clean handles double slashes
|
||||||
|
meta.Stylesheet = filepath.Join(dirRoute, meta.Stylesheet)
|
||||||
|
|
||||||
|
// Ensure it starts with / for web usage
|
||||||
|
if !strings.HasPrefix(meta.Stylesheet, "/") {
|
||||||
|
meta.Stylesheet = "/" + meta.Stylesheet
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,21 +131,130 @@ func GetContentPath(file ContentFile) (string, error) {
|
||||||
htmlContent := buf.String()
|
htmlContent := buf.String()
|
||||||
|
|
||||||
// 3. Post-Process for Index (Dynamic Content Injection)
|
// 3. Post-Process for Index (Dynamic Content Injection)
|
||||||
|
// If Home Page, replace content with Latest Post Preview
|
||||||
if file.RoutePath == "/" {
|
if file.RoutePath == "/" {
|
||||||
// Generate posts data
|
// Find latest post
|
||||||
var posts []templates.PostSnippet
|
var latestPost *ContentFile
|
||||||
|
for _, f := range AllContent {
|
||||||
|
if f.IsMarkdown && f.RoutePath != "/" {
|
||||||
|
// Found one
|
||||||
|
latestPost = &f
|
||||||
|
break // AllContent is sorted by time descending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if latestPost != nil {
|
||||||
|
// Read the latest post
|
||||||
|
postRaw, err := ReadRaw(latestPost.OriginalPath)
|
||||||
|
if err == nil {
|
||||||
|
// Parse the post to HTML to display as preview
|
||||||
|
// We need to parse it fully to get the HTML content
|
||||||
|
// Re-using the MD parser config from above would be cleaner but let's instantiate for now
|
||||||
|
mdPreview := goldmark.New(
|
||||||
|
goldmark.WithExtensions(
|
||||||
|
extension.GFM,
|
||||||
|
&frontmatter.Extender{},
|
||||||
|
mathjax.MathJax,
|
||||||
|
templates.SidebarTag,
|
||||||
|
templates.TopBanner,
|
||||||
|
),
|
||||||
|
goldmark.WithRendererOptions(html.WithUnsafe()),
|
||||||
|
)
|
||||||
|
|
||||||
|
var previewBuf bytes.Buffer
|
||||||
|
ctxPreview := parser.NewContext()
|
||||||
|
if err := mdPreview.Convert(postRaw, &previewBuf, parser.WithContext(ctxPreview)); err == nil {
|
||||||
|
// Extract Metadata of the post to update Page Title?
|
||||||
|
// User said: "the [title] be big text embedded in the black area."
|
||||||
|
// So if we are previewing the latest post, the Home Page Title should probably match the Post Title on the Home Page?
|
||||||
|
// Or should it say "Latest: Title"?
|
||||||
|
// The main header h1 is where {{Title}} goes.
|
||||||
|
// Getting metadata from preview context
|
||||||
|
var postMeta templates.PageMetadata
|
||||||
|
dPost := frontmatter.Get(ctxPreview)
|
||||||
|
if dPost != nil {
|
||||||
|
dPost.Decode(&postMeta)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback title logic
|
||||||
|
if postMeta.Title == "" {
|
||||||
|
base := filepath.Base(latestPost.OriginalPath)
|
||||||
|
postMeta.Title = strings.TrimSuffix(base, filepath.Ext(base))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the meta for the HOME PAGE render
|
||||||
|
meta.Title = postMeta.Title // Use the post's title
|
||||||
|
|
||||||
|
// Extract Preview (First Paragraph?)
|
||||||
|
// "preview of the latest post, then read more"
|
||||||
|
fullHTML := previewBuf.String()
|
||||||
|
|
||||||
|
// Simple strategy: take everything up to the first </p> or limit chars?
|
||||||
|
// Or explicit split?
|
||||||
|
// Let's take the first paragraph.
|
||||||
|
parts := strings.Split(fullHTML, "</p>")
|
||||||
|
if len(parts) > 0 {
|
||||||
|
previewHTML := parts[0] + "</p>"
|
||||||
|
// Add Read More Link
|
||||||
|
readMore := fmt.Sprintf(`<p><a href="%s">Read More...</a></p>`, latestPost.RoutePath)
|
||||||
|
htmlContent = previewHTML + readMore
|
||||||
|
} else {
|
||||||
|
htmlContent = fullHTML
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [directory] Tag Expansion
|
||||||
|
// User can put [directory] in any page (e.g. archive.md) to generate the list
|
||||||
|
if strings.Contains(htmlContent, "[directory]") {
|
||||||
|
// Generate Directory HTML (Grouped by Year)
|
||||||
|
var dirHTML strings.Builder
|
||||||
|
dirHTML.WriteString(`<div class="directory-list">`)
|
||||||
|
|
||||||
|
// Group by year
|
||||||
|
type YearGroup struct {
|
||||||
|
Year int
|
||||||
|
Posts []templates.PostSnippet
|
||||||
|
}
|
||||||
|
var years []YearGroup
|
||||||
|
|
||||||
for _, f := range AllContent {
|
for _, f := range AllContent {
|
||||||
if f.IsMarkdown && f.RoutePath != "/" {
|
if f.IsMarkdown && f.RoutePath != "/" {
|
||||||
name := filepath.Base(f.OriginalPath)
|
name := filepath.Base(f.OriginalPath)
|
||||||
title := strings.TrimSuffix(name, filepath.Ext(name))
|
title := strings.TrimSuffix(name, filepath.Ext(name))
|
||||||
|
|
||||||
posts = append(posts, templates.PostSnippet{
|
// Get Metadata for title? Ideally yes, but costly to parse all?
|
||||||
|
// Let's rely on cached AllContent if we had metadata there, but we don't.
|
||||||
|
// For now, filename title is fast.
|
||||||
|
|
||||||
|
year := f.ModTime.Year()
|
||||||
|
|
||||||
|
// Add to current year group or new one
|
||||||
|
if len(years) == 0 || years[len(years)-1].Year != year {
|
||||||
|
years = append(years, YearGroup{Year: year})
|
||||||
|
}
|
||||||
|
years[len(years)-1].Posts = append(years[len(years)-1].Posts, templates.PostSnippet{
|
||||||
Title: title,
|
Title: title,
|
||||||
URL: f.RoutePath,
|
URL: f.RoutePath,
|
||||||
Date: f.ModTime,
|
Date: f.ModTime,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, yg := range years {
|
||||||
|
dirHTML.WriteString(fmt.Sprintf(`<h3>%d</h3><ul>`, yg.Year))
|
||||||
|
for _, p := range yg.Posts {
|
||||||
|
dirHTML.WriteString(fmt.Sprintf(`<li><a href="%s">%s</a> <span class="date">(%s)</span></li>`,
|
||||||
|
p.URL, p.Title, p.Date.Format("Jan 02")))
|
||||||
|
}
|
||||||
|
dirHTML.WriteString(`</ul>`)
|
||||||
|
}
|
||||||
|
dirHTML.WriteString(`</div>`)
|
||||||
|
|
||||||
|
htmlContent = strings.ReplaceAll(htmlContent, "[directory]", dirHTML.String())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Add More Custom tags here. Add a customTag in tags.go as well, and the implementation in a template file.
|
Add More Custom tags here. Add a customTag in tags.go as well, and the implementation in a template file.
|
||||||
|
|
@ -116,6 +265,21 @@ func GetContentPath(file ContentFile) (string, error) {
|
||||||
|
|
||||||
if sidebarRegex.MatchString(htmlContent) {
|
if sidebarRegex.MatchString(htmlContent) {
|
||||||
// Render the dynamic content
|
// Render the dynamic content
|
||||||
|
// Generate posts data
|
||||||
|
var posts []templates.PostSnippet
|
||||||
|
for _, f := range AllContent {
|
||||||
|
if f.IsMarkdown && f.RoutePath != "/" {
|
||||||
|
name := filepath.Base(f.OriginalPath)
|
||||||
|
title := strings.TrimSuffix(name, filepath.Ext(name))
|
||||||
|
|
||||||
|
posts = append(posts, templates.PostSnippet{
|
||||||
|
Title: title,
|
||||||
|
URL: f.RoutePath,
|
||||||
|
Date: f.ModTime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
latestPostsHTML := templates.RenderLatestPosts(posts)
|
latestPostsHTML := templates.RenderLatestPosts(posts)
|
||||||
dirLink := templates.RenderDirectoryLink()
|
dirLink := templates.RenderDirectoryLink()
|
||||||
|
|
||||||
|
|
@ -146,23 +310,114 @@ func GetContentPath(file ContentFile) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Mathtext mathtext
|
|
||||||
mathtextRegex := regexp.MustCompile("(?s)<mathtext>.*?</mathtext>")
|
|
||||||
if mathtextRegex.MatchString(htmlContent) {
|
// [filename] Image Resolution
|
||||||
htmlContent = mathtextRegex.ReplaceAllStringFunc(htmlContent, func(match string) string {
|
// Matches [filename.ext] or [filename]
|
||||||
innerContent := strings.TrimPrefix(match, "<mathtext>")
|
// Avoids matches that look like markdown links [text](url) or existing tags
|
||||||
innerContent = strings.TrimSuffix(innerContent, "</mathtext>")
|
// We'll use a slightly broad regex and rely on file existence check
|
||||||
return fmt.Sprintf("<li class=\"mathtext\">%s</li>", innerContent)
|
// Regex: \[([^\]<]+)\]
|
||||||
|
// But careful not to match [text](url).
|
||||||
|
// Goldmark renders [text](url) as <a href="...">...</a>.
|
||||||
|
// However, if the link is broken (reference link not defined)? It renders as [text].
|
||||||
|
// So this strategy is acceptable per user request.
|
||||||
|
|
||||||
|
imgRegex := regexp.MustCompile(`\[([^\]<]+)\]`)
|
||||||
|
if imgRegex.MatchString(htmlContent) {
|
||||||
|
htmlContent = imgRegex.ReplaceAllStringFunc(htmlContent, func(match string) string {
|
||||||
|
// match is "[filename]"
|
||||||
|
name := match[1 : len(match)-1] // Strip [ and ]
|
||||||
|
|
||||||
|
// Resolve Image Path
|
||||||
|
// Current directory of the document
|
||||||
|
docDir := filepath.Dir(file.OriginalPath)
|
||||||
|
|
||||||
|
// Root content directory
|
||||||
|
rootDir := "content"
|
||||||
|
|
||||||
|
imagePath, err := resolveImage(name, docDir, rootDir)
|
||||||
|
if err != nil {
|
||||||
|
// Not found, return original text
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate web path
|
||||||
|
// imagePath is a filesystem path e.g. "content/entries/image.png"
|
||||||
|
// We need "/entries/image.png"
|
||||||
|
webPath := "/" + filepath.ToSlash(strings.TrimPrefix(imagePath, "content/"))
|
||||||
|
|
||||||
|
// Check for styling metadata
|
||||||
|
// Config key is the filename (e.g. "image.png")
|
||||||
|
// Use the resolved filename for the key
|
||||||
|
resolvedName := filepath.Base(imagePath)
|
||||||
|
|
||||||
|
var styleAttr, otherAttrs string
|
||||||
|
|
||||||
|
if configVal, ok := meta.Raw[resolvedName]; ok {
|
||||||
|
// Config can be:
|
||||||
|
// 1. A map (TOML: image = {width="...", ...})
|
||||||
|
// 2. A string (TOML: image = "style=...") ? User example: image = "width: auto" ? No, user said: [a] style = "..." OR width = auto
|
||||||
|
|
||||||
|
if configMap, ok := configVal.(map[string]interface{}); ok {
|
||||||
|
for k, v := range configMap {
|
||||||
|
val := fmt.Sprintf("%v", v)
|
||||||
|
if k == "style" {
|
||||||
|
styleAttr = fmt.Sprintf(` style="%s"`, val)
|
||||||
|
} else {
|
||||||
|
// height, width, etc.
|
||||||
|
otherAttrs += fmt.Sprintf(` %s="%s"`, k, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(`<img src="%s" alt="%s"%s%s>`, webPath, name, styleAttr, otherAttrs)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// Build Full Page
|
|
||||||
dataToWrite = templates.BuildFullPage([]byte(htmlContent), meta)
|
// Build Full Page
|
||||||
|
// Generate global posts data (Latest 5-10) for the sidebar
|
||||||
|
// This logic is now handled within the sidebarRegex.MatchString block if a sidebar is present.
|
||||||
|
// If no sidebar, we still need to generate the latest posts for the full page template.
|
||||||
|
var globalPostsForTemplate []templates.PostSnippet
|
||||||
|
for _, f := range AllContent {
|
||||||
|
if f.IsMarkdown && f.RoutePath != "/" {
|
||||||
|
name := filepath.Base(f.OriginalPath)
|
||||||
|
title := strings.TrimSuffix(name, filepath.Ext(name))
|
||||||
|
|
||||||
|
globalPostsForTemplate = append(globalPostsForTemplate, templates.PostSnippet{
|
||||||
|
Title: title,
|
||||||
|
URL: f.RoutePath,
|
||||||
|
Date: f.ModTime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Limit to 10 latest
|
||||||
|
if len(globalPostsForTemplate) >= 10 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This `latestPostsHTML` is for the overall page template, not necessarily the sidebar content.
|
||||||
|
// The sidebar content is generated and injected directly into `htmlContent`.
|
||||||
|
// This `latestPostsHTML` is passed to `BuildFullPage` for the general layout.
|
||||||
|
latestPostsHTML := templates.RenderLatestPosts(globalPostsForTemplate)
|
||||||
|
latestPostsHTML += templates.RenderDirectoryLink()
|
||||||
|
|
||||||
|
dataToWrite = templates.BuildFullPage([]byte(htmlContent), meta, latestPostsHTML)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Write to Cache
|
// 4. Return Content Object or Path?
|
||||||
|
// The function signature returns (string, error), which is the path to the cached file.
|
||||||
|
// Since we are disabling cache, we should probably change the architecture to return ([]byte, error)
|
||||||
|
// or write to a temporary file if the rest of the app expects a file path.
|
||||||
|
// The rest of the app (server.go) does http.ServeFile(w, r, cachePath).
|
||||||
|
// So we MUST return a file path.
|
||||||
|
|
||||||
|
// For now, we will continue to write to the "cache" location, but since we removed the read check,
|
||||||
|
// it basically acts as a "render to temp file" on every request.
|
||||||
|
// This satisfies the "disable caching" requirement (always fresh) while keeping the file-based serving architecture.
|
||||||
|
|
||||||
tmpPath := cachePath + ".tmp"
|
tmpPath := cachePath + ".tmp"
|
||||||
if err := os.WriteFile(tmpPath, dataToWrite, 0644); err != nil {
|
if err := os.WriteFile(tmpPath, dataToWrite, 0644); err != nil {
|
||||||
return "", fmt.Errorf("failed to write cache: %w", err)
|
return "", fmt.Errorf("failed to write cache: %w", err)
|
||||||
|
|
@ -173,3 +428,130 @@ func GetContentPath(file ContentFile) (string, error) {
|
||||||
|
|
||||||
return cachePath, nil
|
return cachePath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadExternalMetadata checks for .yml, .yaml, .toml files and loads them.
|
||||||
|
func loadExternalMetadata(markdownPath string, meta *templates.PageMetadata) error {
|
||||||
|
basePath := strings.TrimSuffix(markdownPath, filepath.Ext(markdownPath))
|
||||||
|
|
||||||
|
extensions := []string{".yml", ".yaml", ".toml"}
|
||||||
|
|
||||||
|
for _, ext := range extensions {
|
||||||
|
path := basePath + ext
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
// Found a metadata file
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read file %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode into a map first to capture everything in Raw
|
||||||
|
if meta.Raw == nil {
|
||||||
|
meta.Raw = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
if ext == ".toml" {
|
||||||
|
if _, err := toml.Decode(string(data), &meta.Raw); err != nil {
|
||||||
|
return fmt.Errorf("decode toml %s: %w", path, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// yaml handles both .yml and .yaml
|
||||||
|
if err := yaml.Unmarshal(data, &meta.Raw); err != nil {
|
||||||
|
return fmt.Errorf("decode yaml %s: %w", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map known fields to struct
|
||||||
|
if v, ok := meta.Raw["title"]; ok {
|
||||||
|
if s, ok := v.(string); ok { meta.Title = s }
|
||||||
|
}
|
||||||
|
if v, ok := meta.Raw["stylesheet"]; ok {
|
||||||
|
if s, ok := v.(string); ok { meta.Stylesheet = s }
|
||||||
|
}
|
||||||
|
if v, ok := meta.Raw["style"]; ok {
|
||||||
|
if s, ok := v.(string); ok { meta.Style = s }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop after finding the first matching metadata file
|
||||||
|
// Priority order is implicit in `extensions` slice (.yml > .yaml > .toml)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveImage searches for the image file
|
||||||
|
func resolveImage(name, docDir, rootDir string) (string, error) {
|
||||||
|
// 1. Check strict path in docDir
|
||||||
|
path := filepath.Join(docDir, name)
|
||||||
|
if !hasExt(name) {
|
||||||
|
// Try extensions
|
||||||
|
if found, err := findNewestWithExt(docDir, name); err == nil {
|
||||||
|
return found, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check strict path in rootDir
|
||||||
|
path = filepath.Join(rootDir, name)
|
||||||
|
if !hasExt(name) {
|
||||||
|
if found, err := findNewestWithExt(rootDir, name); err == nil {
|
||||||
|
return found, nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasExt(name string) bool {
|
||||||
|
return filepath.Ext(name) != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func findNewestWithExt(dir, baseName string) (string, error) {
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var bestMatch string
|
||||||
|
var newestTime time.Time
|
||||||
|
|
||||||
|
targetBase := strings.ToLower(baseName)
|
||||||
|
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name := e.Name()
|
||||||
|
ext := filepath.Ext(name)
|
||||||
|
if ext == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
nameNoExt := strings.TrimSuffix(name, ext)
|
||||||
|
if strings.ToLower(nameNoExt) == targetBase {
|
||||||
|
// Check if image extension
|
||||||
|
info, err := e.Info()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if bestMatch == "" || info.ModTime().After(newestTime) {
|
||||||
|
bestMatch = filepath.Join(dir, name)
|
||||||
|
newestTime = info.ModTime()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bestMatch != "" {
|
||||||
|
return bestMatch, nil
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("not found")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,14 @@ import (
|
||||||
|
|
||||||
// PageMetadata holds the frontmatter data
|
// PageMetadata holds the frontmatter data
|
||||||
type PageMetadata struct {
|
type PageMetadata struct {
|
||||||
Title string `toml:"title" yaml:"title"`
|
Title string `toml:"title" yaml:"title"`
|
||||||
Stylesheet string `toml:"stylesheet" yaml:"stylesheet"`
|
Stylesheet string `toml:"stylesheet" yaml:"stylesheet"`
|
||||||
Style string `toml:"style" yaml:"style"` // Inline CSS
|
Style string `toml:"style" yaml:"style"` // Inline CSS
|
||||||
|
Raw map[string]interface{} `toml:"-" yaml:"-"` // Catch-all for other fields
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildFullPage wraps the content in the HTML shell, injecting metadata.
|
// BuildFullPage wraps the content in the HTML shell, injecting metadata.
|
||||||
func BuildFullPage(content []byte, meta PageMetadata) []byte {
|
func BuildFullPage(content []byte, meta PageMetadata, latestPostsHTML string) []byte {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|
||||||
// Default title if missing
|
// Default title if missing
|
||||||
|
|
@ -39,6 +40,20 @@ func BuildFullPage(content []byte, meta PageMetadata) []byte {
|
||||||
buf.WriteString(`</title>
|
buf.WriteString(`</title>
|
||||||
`)
|
`)
|
||||||
buf.WriteString(cssLink)
|
buf.WriteString(cssLink)
|
||||||
|
buf.WriteString(`<link rel="icon" type="image/svg+xml" href="/Favicon.svg">`)
|
||||||
|
buf.WriteString(`<script src="/theme.js" defer></script>`)
|
||||||
|
// MathJax Configuration
|
||||||
|
buf.WriteString(`<script>
|
||||||
|
MathJax = {
|
||||||
|
tex: {
|
||||||
|
inlineMath: [['$', '$'], ['\\(', '\\)']]
|
||||||
|
},
|
||||||
|
svg: {
|
||||||
|
fontCache: 'global'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>`)
|
||||||
|
buf.WriteString(`<script type="text/javascript" id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js"></script>`)
|
||||||
|
|
||||||
if meta.Style != "" {
|
if meta.Style != "" {
|
||||||
buf.WriteString(`<style>`)
|
buf.WriteString(`<style>`)
|
||||||
|
|
@ -49,9 +64,31 @@ func BuildFullPage(content []byte, meta PageMetadata) []byte {
|
||||||
buf.WriteString(`
|
buf.WriteString(`
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<header class="main-header">
|
||||||
|
<div class="header-content">
|
||||||
|
<a href="/" class="header-logo"><img src="/Favicon.svg" alt="Home"></a>
|
||||||
|
<h1>`)
|
||||||
|
buf.WriteString(title)
|
||||||
|
buf.WriteString(`</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<article class="main-content">
|
||||||
`)
|
`)
|
||||||
buf.Write(content)
|
buf.Write(content)
|
||||||
buf.WriteString(`
|
buf.WriteString(`
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<aside id="sidebar" class="sidebar">
|
||||||
|
<button id="close-sidebar" aria-label="Close Sidebar">×</button>
|
||||||
|
<div class="sidebar-content">
|
||||||
|
`)
|
||||||
|
buf.WriteString(latestPostsHTML)
|
||||||
|
buf.WriteString(`
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div id="read-more-trigger">Read More Posts</div>
|
||||||
</body>
|
</body>
|
||||||
</html>`)
|
</html>`)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,4 +6,3 @@ import customtag "github.com/tendstofortytwo/goldmark-customtag"
|
||||||
// ||| -> <sidebar></sidebar>
|
// ||| -> <sidebar></sidebar>
|
||||||
var SidebarTag = customtag.New("|||", "sidebar")
|
var SidebarTag = customtag.New("|||", "sidebar")
|
||||||
var TopBanner = customtag.New("_-_-", "topbanner")
|
var TopBanner = customtag.New("_-_-", "topbanner")
|
||||||
var Mathtext = customtag.New("{[]}", "mathtext")
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue