From 5cc0ffe553f578150c1887c57ee3a3b524af1eba Mon Sep 17 00:00:00 2001 From: organic-CPU Date: Sat, 11 Jul 2026 23:34:52 +0200 Subject: [PATCH] again test --- js/blog.js | 344 ++++++++++++++++++++++++++--------------------------- 1 file changed, 166 insertions(+), 178 deletions(-) diff --git a/js/blog.js b/js/blog.js index 930ae75..1f8a4be 100644 --- a/js/blog.js +++ b/js/blog.js @@ -1,218 +1,206 @@ +// blog.js – Loads and renders blog posts with automatic collapse for long entries + 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; -function escapeHtml(value) { - return value - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} +// ---------- helpers ---------- -function parseIndexHtmlList(text) { +// crude HTML parser for directory listings (when manifest is missing) +function parseIndexHtmlList(html) { const hrefs = []; const re = /href="([^"]+)"/g; let m; - while ((m = re.exec(text)) !== null) { + while ((m = re.exec(html)) !== null) { const name = m[1]; - if (TXT_EXT.test(name)) hrefs.push(name); + // accept typical text file extensions (and no extension) + if (name.match(/\.(txt|md|text)$/i) || !name.includes('.')) { + // avoid directory links (ending with /) + if (!name.endsWith('/')) hrefs.push(name); + } } return hrefs; } -async function loadBlogFiles() { +// fetch list of blog post filenames (manifest first, fallback to directory listing) +async function loadPostList() { try { - const response = await fetch(MANIFEST, { cache: 'no-cache' }); - if (response.ok) { - return await response.json(); - } - } catch (e) { - // manifest missing or unreadable - } + const r = await fetch(MANIFEST, { cache: 'no-cache' }); + if (r.ok) return await r.json(); + } catch (_) { /* ignore */ } try { - const response = await fetch(BLOG_DIR, { cache: 'no-cache' }); - if (response.ok) { - const text = await response.text(); - return parseIndexHtmlList(text); + const r = await fetch(BLOG_DIR, { cache: 'no-cache' }); + if (r.ok) { + const html = await r.text(); + return parseIndexHtmlList(html); } } 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; +// fetch a single post file +async function fetchPost(filename) { + const res = await fetch(BLOG_DIR + filename, { cache: 'no-cache' }); + if (!res.ok) throw new Error(`Failed to fetch ${filename}`); + return await res.text(); } -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); -} +// generate preview: up to 3 sentences, but not exceeding ~300 chars +function getPreview(text, maxLen = 300) { + if (text.length <= maxLen) return text; // not needed, but safe -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); + // try to split on sentence boundaries (. ! ?) followed by space or newline + const sentences = text.match(/[^.!?]+[.!?]+/g) || []; + let preview = ''; + let count = 0; + for (const s of sentences) { + if (preview.length + s.length <= maxLen) { + preview += s; + count++; + if (count >= 3) break; } else { - paragraphs.forEach((p) => body.appendChild(p)); + // if we haven't reached 3 sentences, break and use what we have + break; } + } - 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); - } + // If we have at least one sentence, use that; otherwise fallback to word‑break + if (preview.trim().length > 0) { + return preview.trim(); + } - article.appendChild(title); - article.appendChild(meta); - article.appendChild(body); - container.appendChild(article); - }); + // fallback: take first maxLen chars and cut at last space + let truncated = text.slice(0, maxLen); + const lastSpace = truncated.lastIndexOf(' '); + if (lastSpace > 0) truncated = truncated.slice(0, lastSpace); + return truncated + '…'; } -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); - }); +// ---------- rendering ---------- + +function renderPost(filename, content) { + const lines = content.split('\n'); + if (lines.length < 3) { + // malformed: skip or treat as title‑only + return null; + } + const dateTime = lines[0].trim(); + const title = lines[1].trim(); + const bodyLines = lines.slice(2); + const body = bodyLines.join('\n').trim(); + + const postDiv = document.createElement('div'); + postDiv.className = 'blog-post'; + + // title + const titleEl = document.createElement('h3'); + titleEl.className = 'blog-title'; + titleEl.textContent = title; + postDiv.appendChild(titleEl); + + // meta + const metaEl = document.createElement('p'); + metaEl.className = 'blog-meta'; + metaEl.textContent = dateTime; + postDiv.appendChild(metaEl); + + const isLong = body.length > 300; + + if (!isLong) { + // short post – display full body directly + const bodyDiv = document.createElement('div'); + bodyDiv.className = 'blog-body'; + bodyDiv.style.whiteSpace = 'pre-line'; + bodyDiv.textContent = body; + postDiv.appendChild(bodyDiv); + } else { + // long post – use
with preview + const details = document.createElement('details'); + details.className = 'blog-collapse'; + + const summary = document.createElement('summary'); + // preview container + const previewDiv = document.createElement('div'); + previewDiv.className = 'blog-preview'; + previewDiv.style.whiteSpace = 'pre-line'; + const previewText = getPreview(body); + previewDiv.textContent = previewText + (previewText.endsWith('…') ? '' : '…'); // add ellipsis if not already + + const readMoreSpan = document.createElement('span'); + readMoreSpan.className = 'read-more-text'; + readMoreSpan.textContent = 'Read more'; + + summary.appendChild(previewDiv); + summary.appendChild(readMoreSpan); + details.appendChild(summary); + + // full body (hidden until opened) + const fullDiv = document.createElement('div'); + fullDiv.className = 'blog-body'; + fullDiv.style.whiteSpace = 'pre-line'; + fullDiv.textContent = body; + details.appendChild(fullDiv); + + // toggle text when details opens/closes + details.addEventListener('toggle', function() { + const span = this.querySelector('.read-more-text'); + span.textContent = this.open ? 'Show less' : 'Read more'; + }); + + postDiv.appendChild(details); + } + + return postDiv; } +// ---------- main ---------- + async function initBlog() { - const fileNames = await loadBlogFiles(); - if (!fileNames.length) { - renderBlogEntries([]); - return; - } + const feed = document.querySelector('.blog-feed'); + if (!feed) return; - const promises = fileNames - .filter((name) => TXT_EXT.test(name)) - .map(async (name) => { + feed.textContent = 'Loading blog posts…'; + + try { + const files = await loadPostList(); + if (!files.length) { + feed.textContent = 'No blog posts found.'; + return; + } + + // sort files by name (oldest first? we'll sort by date if embedded, but we'll just alphabetical) + // Typically filenames like 2025-01-01.txt, so alphabetical = chronological + files.sort(); + + // clear feed + feed.innerHTML = ''; + + for (const file of files) { 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 content = await fetchPost(file); + const el = renderPost(file, content); + if (el) feed.appendChild(el); + } catch (err) { + console.warn(`Failed to load ${file}:`, err); + // optionally show error placeholder + const errDiv = document.createElement('div'); + errDiv.className = 'blog-post'; + errDiv.style.color = '#b91c1c'; + errDiv.textContent = `⚠️ Could not load post: ${file}`; + feed.appendChild(errDiv); } - }); + } - const entries = (await Promise.all(promises)).filter(Boolean); - renderBlogEntries(sortEntries(entries)); + // if nothing was added, show message + if (feed.children.length === 0) { + feed.textContent = 'No valid blog posts found.'; + } + } catch (err) { + console.error('Blog init error:', err); + feed.textContent = 'Error loading blog posts.'; + } } -document.addEventListener('DOMContentLoaded', initBlog); +document.addEventListener('DOMContentLoaded', initBlog); \ No newline at end of file