test
This commit is contained in:
+2
-3
@@ -1,5 +1,4 @@
|
||||
[
|
||||
"2026-06-06.text",
|
||||
"2026-07-09-welcome.txt",
|
||||
"2026-07-10-attachment-test.txt"
|
||||
"10072026.text",
|
||||
"the first post.text"
|
||||
]
|
||||
@@ -0,0 +1,159 @@
|
||||
[
|
||||
"_DSC2592.jpg",
|
||||
"_DSC2563.jpg",
|
||||
"_DSC2536.jpg",
|
||||
"_DSC2534.jpg",
|
||||
"_DSC2532.jpg",
|
||||
"_DSC2527.jpg",
|
||||
"_DSC2627.jpg",
|
||||
"_DSC2600.jpg",
|
||||
"_DSC5088.jpg",
|
||||
"_DSC5082.jpg",
|
||||
"_DSC5070.jpg",
|
||||
"_DSC5060.jpg",
|
||||
"_DSC5058.jpg",
|
||||
"_DSC5037.jpg",
|
||||
"_DSC5031.jpg",
|
||||
"_DSC5014.jpg",
|
||||
"_DSC5005.jpg",
|
||||
"_DSC4996.jpg",
|
||||
"_DSC4984.jpg",
|
||||
"_DSC4935.jpg",
|
||||
"_DSC4930.jpg",
|
||||
"_DSC4927.jpg",
|
||||
"_DSC4907.jpg",
|
||||
"_DSC4859.jpg",
|
||||
"_DSC4850.jpg",
|
||||
"_DSC4824.jpg",
|
||||
"_DSC4814.jpg",
|
||||
"_DSC4784.jpg",
|
||||
"_DSC4774.jpg",
|
||||
"_DSC4760.jpg",
|
||||
"_DSC4689.jpg",
|
||||
"_DSC4686.jpg",
|
||||
"_DSC4670.jpg",
|
||||
"_DSC4640.jpg",
|
||||
"_DSC4565.jpg",
|
||||
"_DSC4561.jpg",
|
||||
"_DSC4558.jpg",
|
||||
"_DSC4537.jpg",
|
||||
"_DSC4532.jpg",
|
||||
"_DSC4523.jpg",
|
||||
"_DSC4522.jpg",
|
||||
"_DSC4478.jpg",
|
||||
"_DSC4447.jpg",
|
||||
"_DSC4416.jpg",
|
||||
"_DSC4408.jpg",
|
||||
"_DSC4373.jpg",
|
||||
"_DSC4343.jpg",
|
||||
"_DSC4332.jpg",
|
||||
"_DSC4322.jpg",
|
||||
"_DSC5220.jpg",
|
||||
"_DSC5112.jpg",
|
||||
"_DSC5109.jpg",
|
||||
"_DSC5096.jpg",
|
||||
"_DSC1049.jpg",
|
||||
"_DSC1042.jpg",
|
||||
"_DSC1037.jpg",
|
||||
"_DSC1030.jpg",
|
||||
"_DSC1028.jpg",
|
||||
"_DSC1021.jpg",
|
||||
"_DSC1016.jpg",
|
||||
"_DSC1015.jpg",
|
||||
"_DSC1009.jpg",
|
||||
"_DSC1007.jpg",
|
||||
"_DSC1102.jpg",
|
||||
"_DSC1098.jpg",
|
||||
"_DSC1052.jpg",
|
||||
"_DSC1468.jpg",
|
||||
"_DSC1464.jpg",
|
||||
"_DSC1559.jpg",
|
||||
"_DSC1550.jpg",
|
||||
"_DSC1543.jpg",
|
||||
"_DSC1542.jpg",
|
||||
"_DSC1539.jpg",
|
||||
"_DSC1527.jpg",
|
||||
"_DSC1516.jpg",
|
||||
"_DSC1498.jpg",
|
||||
"_DSC1495.jpg",
|
||||
"_DSC1487.jpg",
|
||||
"_DSC1471.jpg",
|
||||
"_DSC1152.jpg",
|
||||
"_DSC1150.jpg",
|
||||
"_DSC1396.jpg",
|
||||
"_DSC1341.jpg",
|
||||
"_DSC1330.jpg",
|
||||
"_DSC1315.jpg",
|
||||
"_DSC1282.jpg",
|
||||
"_DSC1174.jpg",
|
||||
"_DSC1164.jpg",
|
||||
"_DSC0779.jpg",
|
||||
"_DSC0762.jpg",
|
||||
"_DSC0732.jpg",
|
||||
"_DSC0707.jpg",
|
||||
"_DSC0701.jpg",
|
||||
"_DSC0699.jpg",
|
||||
"_DSC0697.jpg",
|
||||
"_DSC0683.jpg",
|
||||
"_DSC0678.jpg",
|
||||
"_DSC0677.jpg",
|
||||
"_DSC0671.jpg",
|
||||
"_DSC0669.jpg",
|
||||
"_DSC0665.jpg",
|
||||
"_DSC0649.jpg",
|
||||
"_DSC0646.jpg",
|
||||
"_DSC0624.jpg",
|
||||
"_DSC0621.jpg",
|
||||
"_DSC0601.jpg",
|
||||
"_DSC0590.jpg",
|
||||
"_DSC0582.jpg",
|
||||
"_DSC0866.jpg",
|
||||
"_DSC0861.jpg",
|
||||
"_DSC0858.jpg",
|
||||
"_DSC0854.jpg",
|
||||
"_DSC0850.jpg",
|
||||
"_DSC0846.jpg",
|
||||
"_DSC0841.jpg",
|
||||
"_DSC0837.jpg",
|
||||
"_DSC0833.jpg",
|
||||
"_DSC0823.jpg",
|
||||
"_DSC0817.jpg",
|
||||
"_DSC0784.jpg",
|
||||
"_DSC0782.jpg",
|
||||
"DSC_0337.jpg",
|
||||
"DSC_0335.jpg",
|
||||
"DSC_0319.jpg",
|
||||
"DSC_0297.jpg",
|
||||
"DSC_0278.jpg",
|
||||
"DSC_0257.jpg",
|
||||
"DSC_0234.jpg",
|
||||
"DSC_0224.jpg",
|
||||
"DSC_0214.jpg",
|
||||
"DSC_0207.jpg",
|
||||
"DSC_0499.jpg",
|
||||
"DSC_0470.jpg",
|
||||
"DSC_0455.jpg",
|
||||
"DSC_0450.jpg",
|
||||
"DSC_0446.jpg",
|
||||
"DSC_0439.jpg",
|
||||
"DSC_0430.jpg",
|
||||
"DSC_0404.jpg",
|
||||
"DSC_0402.jpg",
|
||||
"DSC_0389.jpg",
|
||||
"DSC_0387.jpg",
|
||||
"DSC_0378.jpg",
|
||||
"DSC_0368.jpg",
|
||||
"DSC_0036.jpg",
|
||||
"DSC_0122.jpg",
|
||||
"DSC_0114.jpg",
|
||||
"DSC_0089.jpg",
|
||||
"DSC_0087.jpg",
|
||||
"DSC_0084.jpg",
|
||||
"DSC_0083.jpg",
|
||||
"DSC_0081.jpg",
|
||||
"DSC_0068.jpg",
|
||||
"DSC_0054.jpg",
|
||||
"DSC_0048.jpg",
|
||||
"DSC_0045.jpg",
|
||||
"DSC_0042.jpg"
|
||||
]
|
||||
+80
-171
@@ -1,127 +1,3 @@
|
||||
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, '"')
|
||||
.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 = '';
|
||||
@@ -141,78 +17,111 @@ function renderBlogEntries(entries) {
|
||||
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'blog-meta';
|
||||
meta.textContent = entry.date ? formatDate(entry.date) : entry.dateLabel || 'Date not provided';
|
||||
meta.textContent = entry.date
|
||||
? formatDate(entry.date)
|
||||
: entry.dateLabel || 'Date not provided';
|
||||
|
||||
|
||||
const createBody = () => {
|
||||
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));
|
||||
}
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
|
||||
let bodyContainer;
|
||||
|
||||
if (entry.body.length > BLOG_COLLAPSE_THRESHOLD) {
|
||||
const details = document.createElement('details');
|
||||
details.className = 'blog-collapse';
|
||||
|
||||
const summary = document.createElement('summary');
|
||||
|
||||
// Create preview
|
||||
const preview = document.createElement('div');
|
||||
preview.className = 'blog-preview';
|
||||
|
||||
const previewText = entry.body
|
||||
.split('\n')
|
||||
.filter(line => line.trim())
|
||||
.slice(0, BLOG_PREVIEW_LINES)
|
||||
.join('\n');
|
||||
|
||||
preview.textContent = previewText;
|
||||
|
||||
|
||||
summary.appendChild(preview);
|
||||
|
||||
const moreText = document.createElement('span');
|
||||
moreText.className = 'read-more-text';
|
||||
moreText.textContent = 'Read more';
|
||||
|
||||
summary.appendChild(moreText);
|
||||
|
||||
|
||||
details.appendChild(summary);
|
||||
details.appendChild(createBody());
|
||||
|
||||
|
||||
details.addEventListener('toggle', () => {
|
||||
moreText.textContent = details.open
|
||||
? 'Show less'
|
||||
: 'Read more';
|
||||
|
||||
preview.style.display = details.open
|
||||
? 'none'
|
||||
: 'block';
|
||||
});
|
||||
|
||||
|
||||
bodyContainer = details;
|
||||
|
||||
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));
|
||||
bodyContainer = createBody();
|
||||
}
|
||||
|
||||
|
||||
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(body);
|
||||
article.appendChild(bodyContainer);
|
||||
|
||||
container.appendChild(article);
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user