From 937d92d977ebf0d659551596fe52479e5490ea4f Mon Sep 17 00:00:00 2001 From: organic-CPU Date: Sat, 11 Jul 2026 23:32:21 +0200 Subject: [PATCH] back again --- js/blog.js | 374 +++++++++++++++++++++++++++-------------------------- 1 file changed, 192 insertions(+), 182 deletions(-) diff --git a/js/blog.js b/js/blog.js index 9d15360..930ae75 100644 --- a/js/blog.js +++ b/js/blog.js @@ -1,208 +1,218 @@ -// blog.js 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; -// ------------------------------------------------------------ -// 1. Fetch the list of blog post files -// ------------------------------------------------------------ -async function getPostFiles() { - // Try manifest first +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 r = await fetch(MANIFEST, { cache: 'no-cache' }); - if (r.ok) { - const data = await r.json(); - return data.filter(f => f.endsWith('.txt')); + const response = await fetch(MANIFEST, { cache: 'no-cache' }); + if (response.ok) { + return await response.json(); } - } catch (_) { /* ignore */ } + } catch (e) { + // manifest missing or unreadable + } - // Fallback: parse directory listing (requires autoindex) try { - const r = await fetch(BLOG_DIR, { cache: 'no-cache' }); - if (r.ok) { - const text = await r.text(); - const hrefs = []; - const re = /href="([^"]+)"/g; - let m; - while ((m = re.exec(text)) !== null) { - const name = m[1]; - if (name.endsWith('.txt')) hrefs.push(name); - } - return hrefs; + 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 []; } -// ------------------------------------------------------------ -// 2. Parse a single blog post file -// ------------------------------------------------------------ -async function fetchPost(filename) { - const url = BLOG_DIR + filename; - const resp = await fetch(url); - if (!resp.ok) throw new Error(`Failed to fetch ${filename}`); - const text = await resp.text(); - const lines = text.split('\n'); - // First line: date/time, second: title, rest: content - const dateTime = lines[0]?.trim() || 'Unknown date'; - const title = lines[1]?.trim() || 'Untitled'; - const content = lines.slice(2).join('\n').trim(); - return { filename, dateTime, title, content }; +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; } -// ------------------------------------------------------------ -// 3. Generate a preview for long posts -// ------------------------------------------------------------ -function createPreview(content, maxLen = 300) { - // Return { preview: string, truncated: boolean } - if (content.length <= maxLen) { - return { preview: content, truncated: false }; - } - - // Try to get at least 3 sentences - const sentences = content.match(/[^.!?]*[.!?](\s+|$)/g) || []; - if (sentences.length >= 3) { - let preview = sentences.slice(0, 3).join(''); - // If preview is still > maxLen, trim to nearest space - if (preview.length > maxLen) { - const trimmed = preview.slice(0, maxLen); - const lastSpace = trimmed.lastIndexOf(' '); - if (lastSpace > 0) preview = trimmed.slice(0, lastSpace) + '…'; - else preview = trimmed + '…'; - } else { - preview = preview.trim() + '…'; - } - return { preview, truncated: true }; - } - - // Fallback: at least 3 lines (by newline) - const lines = content.split('\n').filter(l => l.trim() !== ''); - if (lines.length >= 3) { - let preview = lines.slice(0, 3).join('\n'); - if (preview.length > maxLen) { - const trimmed = preview.slice(0, maxLen); - const lastSpace = trimmed.lastIndexOf(' '); - if (lastSpace > 0) preview = trimmed.slice(0, lastSpace) + '…'; - else preview = trimmed + '…'; - } else { - preview = preview.trim() + '…'; - } - return { preview, truncated: true }; - } - - // Last resort: take first maxLen chars, break at word boundary - if (content.length > maxLen) { - const trimmed = content.slice(0, maxLen); - const lastSpace = trimmed.lastIndexOf(' '); - const preview = (lastSpace > 0 ? trimmed.slice(0, lastSpace) : trimmed) + '…'; - return { preview, truncated: true }; - } - - return { preview: content, truncated: false }; -} - -// ------------------------------------------------------------ -// 4. Render a single blog post -// ------------------------------------------------------------ -function renderPost(post) { - const { dateTime, title, content } = post; - - const article = document.createElement('article'); - article.className = 'blog-post'; - - // Title - const h3 = document.createElement('h3'); - h3.className = 'blog-title'; - h3.textContent = title; - article.appendChild(h3); - - // Meta (date) - const meta = document.createElement('div'); - meta.className = 'blog-meta'; - meta.textContent = dateTime; - article.appendChild(meta); - - // Body content: split paragraphs by double newline, wrap in

- const bodyDiv = document.createElement('div'); - bodyDiv.className = 'blog-body'; - const paragraphs = content.split(/\n\s*\n/); - paragraphs.forEach(para => { - const p = document.createElement('p'); - p.textContent = para.trim(); - bodyDiv.appendChild(p); +function extractAttachments(body) { + const attachments = new Set(); + body.replace(ATTACHMENT_RE, (_, filename) => { + const safeName = filename.trim(); + if (safeName) attachments.add(safeName); + return ''; }); - - // Decide if we need collapse - const { preview, truncated } = createPreview(content); - - if (!truncated) { - // Short post – show full content directly - article.appendChild(bodyDiv); - } else { - // Long post – wrap in

- const details = document.createElement('details'); - details.className = 'blog-collapse'; - - const summary = document.createElement('summary'); - // Preview text - const previewDiv = document.createElement('div'); - previewDiv.className = 'blog-preview'; - // Split preview by newline and create lines, but we can just set innerHTML with
- previewDiv.innerHTML = preview.replace(/\n/g, '
'); - summary.appendChild(previewDiv); - - // "Read more" text - const moreSpan = document.createElement('span'); - moreSpan.className = 'read-more-text'; - moreSpan.textContent = 'Read more'; - summary.appendChild(moreSpan); - - details.appendChild(summary); - // Full content inside the details - details.appendChild(bodyDiv); - article.appendChild(details); - } - - return article; + return Array.from(attachments); } -// ------------------------------------------------------------ -// 5. Main: load posts and render -// ------------------------------------------------------------ -async function initBlog() { - const feed = document.querySelector('.blog-feed'); - if (!feed) return; +function parseBlogText(text, filename) { + const lines = text.replace(/\r/g, '').split('\n'); + let index = 0; + while (index < lines.length && !lines[index].trim()) index += 1; - feed.innerHTML = 'Loading blog posts…'; + const rawDate = index < lines.length ? lines[index].trim() : ''; + if (rawDate.toLowerCase().startsWith('date:')) { + index += 1; + } else { + index += 1; + } - try { - const files = await getPostFiles(); - if (!files.length) { - feed.innerHTML = 'No blog posts found.'; - return; + 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)); } - // Fetch and parse all posts - const posts = await Promise.all(files.map(f => fetchPost(f))); + 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); + } - // Sort by date descending (assuming ISO-like date in first line) - posts.sort((a, b) => { - const dA = new Date(a.dateTime); - const dB = new Date(b.dateTime); - return dB - dA; - }); - - // Render each post - feed.innerHTML = ''; - posts.forEach(post => { - const el = renderPost(post); - feed.appendChild(el); - }); - } catch (err) { - console.error(err); - feed.innerHTML = 'Error loading blog posts.'; - } + article.appendChild(title); + article.appendChild(meta); + article.appendChild(body); + container.appendChild(article); + }); } -document.addEventListener('DOMContentLoaded', initBlog); \ No newline at end of file +function sortEntries(entries) { + return entries.slice().sort((a, b) => { + if (a.date && b.date) return b.date - a.date; + if (a.date) return -1; + if (b.date) return 1; + return a.filename.localeCompare(b.filename); + }); +} + +async function initBlog() { + const fileNames = await loadBlogFiles(); + if (!fileNames.length) { + renderBlogEntries([]); + return; + } + + const promises = fileNames + .filter((name) => TXT_EXT.test(name)) + .map(async (name) => { + try { + const response = await fetch(BLOG_DIR + name, { cache: 'no-cache' }); + if (!response.ok) throw new Error('Failed to load ' + name); + const text = await response.text(); + return parseBlogText(text, name); + } catch (error) { + console.warn(error); + return null; + } + }); + + const entries = (await Promise.all(promises)).filter(Boolean); + renderBlogEntries(sortEntries(entries)); +} + +document.addEventListener('DOMContentLoaded', initBlog);