238 lines
6.5 KiB
JavaScript
238 lines
6.5 KiB
JavaScript
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, '"')
|
|
.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 `<a href="${url}" target="_blank" rel="noopener noreferrer">${escapeHtml(safeName)}</a>`;
|
|
});
|
|
const lines = text.split('\n');
|
|
p.innerHTML = lines.join('<br>');
|
|
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);
|
|
});
|
|
} |