Files
tildefriends/apps/bookclub/app.js
2025-12-17 18:48:38 -05:00

182 lines
4.4 KiB
JavaScript

import * as commonmark from './commonmark.min.js';
async function query(sql, args) {
let result = [];
await ssb.sqlAsync(sql, args, function (row) {
result.push(row);
});
return result;
}
function image(node, entering) {
if (
node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('video:')
) {
if (entering) {
this.lit(
'<video style="max-width: 100%; max-height: 480px" title="' +
this.esc(node.firstChild?.literal) +
'" controls>'
);
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
this.disableTags += 1;
} else {
this.disableTags -= 1;
this.lit('</video>');
}
} else if (
node.firstChild?.type === 'text' &&
node.firstChild.literal.startsWith('audio:')
) {
if (entering) {
this.lit(
'<audio style="height: 32px; max-width: 100%" title="' +
this.esc(node.firstChild?.literal) +
'" controls>'
);
this.lit('<source src="' + this.esc(node.destination) + '"></source>');
this.disableTags += 1;
} else {
this.disableTags -= 1;
this.lit('</audio>');
}
} else {
if (entering) {
if (this.disableTags === 0) {
this.lit(
'<div class="img_caption">' +
this.esc(node.firstChild?.literal || node.destination) +
'</div>'
);
if (this.options.safe && potentiallyUnsafe(node.destination)) {
this.lit('<img src="" title="');
} else {
this.lit('<img src="' + this.esc(node.destination) + '" title="');
}
}
this.disableTags += 1;
} else {
this.disableTags -= 1;
if (this.disableTags === 0) {
if (node.title) {
this.lit('" title="' + this.esc(node.title));
}
this.lit('" />');
}
}
}
}
function code(node) {
let attrs = this.attrs(node);
attrs.push(['class', k_code_classes]);
this.tag('code', attrs);
this.out(node.literal);
this.tag('/code');
}
function attrs(node) {
let result = commonmark.HtmlRenderer.prototype.attrs.bind(this)(node);
if (node.type == 'block_quote') {
result.push(['class', 'w3-theme-d1']);
} else if (node.type == 'code_block') {
result.push(['class', k_code_classes]);
}
return result;
}
function markdown(md) {
let reader = new commonmark.Parser();
let writer = new commonmark.HtmlRenderer({safe: true});
//writer.image = image;
writer.code = code;
writer.attrs = attrs;
let parsed = reader.parse(md || '');
let walker = parsed.walker();
let event, node;
while ((event = walker.next())) {
node = event.node;
if (event.entering) {
if (node.type == 'link') {
if (
node.destination.startsWith('@') &&
node.destination.endsWith('.ed25519')
) {
node.destination = '#' + encodeURIComponent(node.destination);
} else if (
node.destination.startsWith('%') &&
node.destination.endsWith('.sha256')
) {
node.destination = '#' + encodeURIComponent(node.destination);
} else if (
node.destination.startsWith('&') &&
node.destination.endsWith('.sha256')
) {
node.destination = '/' + node.destination + '/view';
}
} else if (node.type == 'image') {
if (node.destination.startsWith('&')) {
node.destination = '/' + node.destination + '/view';
}
}
}
}
return writer.render(parsed);
}
async function main() {
let data = await query(`
SELECT
content ->> 'title' AS title,
content ->> '$.image.link' AS image,
content ->> 'description' AS description
FROM messages
WHERE
content ->> 'type' = 'bookclub' AND
title IS NOT NULL AND
image IS NOT NULL AND
description IS NOT NULL
`);
if (!data?.length) {
await app.setDocument(`
<!DOCTYPE html>
<html>
<body style="background-color: #fff">
<p>No bookclub messages found.</p>
</body>
</html>
`);
return;
}
await app.setDocument(`
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="w3.css">
</head>
<body class="w3-grid" style="background-color: #fff; gap:8px;grid-template-columns:repeat(auto-fit, minmax(4in,1fr))">
${data
.map(
(x) => `
<div class="w3-card-4">
<header class="w3-container w3-center">
<h1>${markdown(x.title)}</h1>
</header>
<div class="w3-container w3-center">
<img src="/${x.image}/view" style="max-height: 2in; max-width: 2in">
</div>
<div class="w3-container">
<p>${markdown(x.description)}</p>
</div>
</div>
`
)
.join('\n')}
</body>
</html>
`);
}
main();