const BLOG_DIR = 'blog/'; const ATTACHMENT_DIR = BLOG_DIR + 'files/'; const MANIFEST = BLOG_DIR + 'manifest.json'; const TXT_EXT = /\.(txt|text)$/i; const ATTACHMENT_RE = /\[\[(?:file|attachment):\s*([^\]]+)\]\]/gi; const BLOG_COLLAPSE_THRESHOLD = 300; // characters before collapsing const BLOG_PREVIEW_LINES = 3; // number of lines shown in preview function escapeHtml(value) { return value .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function parseIndexHtmlList(text) { const hrefs = []; const re = /href="([^"]+)"/g; let m; while ((m = re.exec(text)) !== null) { const name = m[1]; if (TXT_EXT.test(name)) hrefs.push(name); } return hrefs; } async function loadBlogFiles() { try { const response = await fetch(MANIFEST, { cache: 'no-cache' }); if (response.ok) { return await response.json(); } } catch (e) { // manifest missing or unreadable } try { const response = await fetch(BLOG_DIR, { cache: 'no-cache' }); if (response.ok) { const text = await response.text(); return parseIndexHtmlList(text); } } catch (e) { console.warn('Could not read blog directory listing', e); } return []; } function parseDateLine(rawDate) { if (!rawDate) return null; let value = rawDate.trim(); if (value.toLowerCase().startsWith('date:')) { value = value.slice(5).trim(); } if (!value) return null; const normalized = value.replace(' ', 'T'); const date = new Date(normalized); return Number.isNaN(date.getTime()) ? null : date; } function extractAttachments(body) { const attachments = new Set(); body.replace(ATTACHMENT_RE, (_, filename) => { const safeName = filename.trim(); if (safeName) attachments.add(safeName); return ''; }); return Array.from(attachments); } function parseBlogText(text, filename) { const lines = text.replace(/\r/g, '').split('\n'); let index = 0; while (index < lines.length && !lines[index].trim()) index += 1; const rawDate = index < lines.length ? lines[index].trim() : ''; if (rawDate.toLowerCase().startsWith('date:')) { index += 1; } else { index += 1; } while (index < lines.length && !lines[index].trim()) index += 1; const title = index < lines.length ? lines[index].trim() : 'Untitled'; index += 1; while (index < lines.length && !lines[index].trim()) index += 1; const body = lines.slice(index).join('\n').trim(); return { filename, title: title || 'Untitled', date: parseDateLine(rawDate) || null, dateLabel: rawDate || '', body: body || '', attachments: extractAttachments(body), }; } function formatDate(date) { if (!date) return 'Unknown date'; return new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }).format(date); } function createParagraphs(body) { return body .split(/\n{2,}/) .filter(Boolean) .map((paragraph) => { const p = document.createElement('p'); const text = escapeHtml(paragraph).replace(ATTACHMENT_RE, (match, filename) => { const safeName = filename.trim(); const url = ATTACHMENT_DIR + encodeURI(safeName); return `${escapeHtml(safeName)}`; }); const lines = text.split('\n'); p.innerHTML = lines.join('
'); return p; }); } function renderBlogEntries(entries) { const container = document.querySelector('.blog-feed'); container.innerHTML = ''; if (!entries.length) { container.textContent = 'No posts found. Add .txt files to the blog/ folder and enable blog manifest or directory listing.'; return; } entries.forEach((entry) => { const article = document.createElement('article'); article.className = 'blog-post'; const title = document.createElement('h3'); title.className = 'blog-title'; title.textContent = entry.title; const meta = document.createElement('div'); meta.className = 'blog-meta'; meta.textContent = entry.date ? formatDate(entry.date) : entry.dateLabel || 'Date not provided'; const body = document.createElement('div'); body.className = 'blog-body'; const paragraphs = createParagraphs(entry.body); if (!paragraphs.length) { const empty = document.createElement('p'); empty.textContent = 'No content yet.'; body.appendChild(empty); } else { paragraphs.forEach((p) => body.appendChild(p)); } let content = body; // Collapse long posts if (entry.body.length > BLOG_COLLAPSE_THRESHOLD) { const wrapper = document.createElement('div'); wrapper.className = 'blog-collapse'; const preview = document.createElement('p'); preview.className = 'blog-preview'; preview.textContent = entry.body .split('\n') .filter(line => line.trim()) .slice(0, BLOG_PREVIEW_LINES) .join('\n'); const button = document.createElement('button'); button.className = 'blog-expand-button'; button.textContent = 'Read more'; body.style.display = 'none'; button.addEventListener('click', () => { const expanded = body.style.display !== 'none'; body.style.display = expanded ? 'none' : 'block'; preview.style.display = expanded ? 'block' : 'none'; button.textContent = expanded ? 'Read more' : 'Show less'; }); wrapper.appendChild(preview); wrapper.appendChild(button); wrapper.appendChild(body); content = wrapper; } if (entry.attachments && entry.attachments.length) { const attachTitle = document.createElement('div'); attachTitle.className = 'blog-meta'; attachTitle.textContent = 'Attachments:'; const list = document.createElement('ul'); list.className = 'blog-attachment-list'; entry.attachments.forEach((attachment) => { const item = document.createElement('li'); const link = document.createElement('a'); link.href = ATTACHMENT_DIR + encodeURI(attachment); link.target = '_blank'; link.rel = 'noopener noreferrer'; link.textContent = attachment; item.appendChild(link); list.appendChild(item); }); article.appendChild(attachTitle); article.appendChild(list); } article.appendChild(title); article.appendChild(meta); article.appendChild(content); container.appendChild(article); }); }