forked from cory/tildefriends
208 lines
6.0 KiB
JavaScript
208 lines
6.0 KiB
JavaScript
import * as commonmark from './commonmark.min.js';
|
|
|
|
function escape(text) {
|
|
return (text ?? '')
|
|
.replaceAll('&', '&')
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>');
|
|
}
|
|
|
|
function escapeAttribute(text) {
|
|
return (text ?? '')
|
|
.replaceAll('&', '&')
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>')
|
|
.replaceAll('"', '"')
|
|
.replaceAll("'", ''');
|
|
}
|
|
|
|
export async function get_blog_message(id) {
|
|
let message;
|
|
await ssb.sqlAsync(
|
|
'SELECT author, timestamp, content FROM messages WHERE id = ?',
|
|
[id],
|
|
function (row) {
|
|
let content = JSON.parse(row.content);
|
|
message = {
|
|
author: row.author,
|
|
timestamp: row.timestamp,
|
|
blog: content?.blog,
|
|
title: content?.title,
|
|
};
|
|
}
|
|
);
|
|
if (message) {
|
|
await ssb.sqlAsync(
|
|
`
|
|
SELECT json_extract(content, '$.name') AS name
|
|
FROM messages
|
|
WHERE author = ?
|
|
AND json_extract(content, '$.type') = 'about'
|
|
AND json_extract(content, '$.about') = author
|
|
AND name IS NOT NULL
|
|
ORDER BY sequence DESC LIMIT 1
|
|
`,
|
|
[message.author],
|
|
function (row) {
|
|
message.name = row.name;
|
|
}
|
|
);
|
|
}
|
|
return message;
|
|
}
|
|
|
|
export function markdown(md) {
|
|
let reader = new commonmark.Parser();
|
|
let writer = new commonmark.HtmlRenderer({safe: true});
|
|
let parsed = reader.parse(md || '');
|
|
let walker = parsed.walker();
|
|
let event, node;
|
|
while ((event = walker.next())) {
|
|
node = event.node;
|
|
if (event.entering) {
|
|
if (node.destination?.startsWith('&')) {
|
|
node.destination =
|
|
'/' + node.destination + '/view?filename=' + node.firstChild?.literal;
|
|
} else if (
|
|
node.destination?.startsWith('@') ||
|
|
node.destination?.startsWith('%')
|
|
) {
|
|
node.destination = '/~core/ssb/#' + escape(node.destination);
|
|
}
|
|
}
|
|
}
|
|
return writer.render(parsed);
|
|
}
|
|
|
|
export async function render_blog_post_html(blog_post) {
|
|
let blob = utf8Decode(await ssb.blobGet(blog_post.blog));
|
|
return `<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>🪵Tilde Friends Blog - ${markdown(blog_post.title)}</title>
|
|
<base target="_top">
|
|
</head>
|
|
<body>
|
|
<h1><a href="./">🪵Tilde Friends Blog</a></h1>
|
|
<div>
|
|
<div><a href="../ssb/#${escapeAttribute(blog_post.author)}">${escape(blog_post.name)}</a> ${escape(new Date(blog_post.timestamp).toString())}</div>
|
|
<div>${markdown(blob)}</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`;
|
|
}
|
|
|
|
function render_blog_post(blog_post) {
|
|
return `
|
|
<div>
|
|
<h2><a href="/~${core.app.owner}/${core.app.name}/${escapeAttribute(blog_post.id)}">${escape(blog_post.title)}</a></h2>
|
|
<div><a href="../ssb/#${escapeAttribute(blog_post.author)}">${escape(blog_post.name)}</a> ${escape(new Date(blog_post.timestamp).toString())}</div>
|
|
<div>${markdown(blog_post.summary)}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
export function render_html(blogs) {
|
|
return `<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>🪵Tilde Friends Blog</title>
|
|
<link href="./atom" type="application/atom+xml" rel="alternate" title="🪵Tilde Blog"/>
|
|
<style>
|
|
html {
|
|
background-color: #ccc;
|
|
}
|
|
</style>
|
|
<base target="_top">
|
|
</head>
|
|
<body>
|
|
<div style="display: flex; flex-direction: row; align-items: center; gap: 1em">
|
|
<h1>🪵Tilde Friends Blog</h1>
|
|
<div style="font-size: xx-small; vertical-align: middle"><a href="/~cory/blog/atom">atom feed</a></div>
|
|
</div>
|
|
${blogs.map((blog_post) => render_blog_post(blog_post)).join('\n')}
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
function render_blog_post_atom(blog_post) {
|
|
return `<entry>
|
|
<title>${escape(blog_post.title)}</title>
|
|
<link href="/~cory/ssb/#${blog_post.id}" />
|
|
<id>${blog_post.id}</id>
|
|
<published>${escape(new Date(blog_post.timestamp).toString())}</published>
|
|
<summary>${escape(blog_post.summary)}</summary>
|
|
<author>
|
|
<name>${escape(blog_post.name)}</name>
|
|
<feed>${escape(blog_post.author)}</feed>
|
|
</author>
|
|
</entry>`;
|
|
}
|
|
|
|
export function render_atom(blogs) {
|
|
return `<?xml version="1.0" encoding="utf-8"?>
|
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
|
<title>🪵Tilde Blog</title>
|
|
<subtitle>A subtitle.</subtitle>
|
|
<link href="${core.url}/atom" rel="self"/>
|
|
<link href="${core.url}"/>
|
|
<id>${core.url}</id>
|
|
<updated>${new Date().toString()}</updated>
|
|
${blogs.map((blog_post) => render_blog_post_atom(blog_post)).join('\n')}
|
|
</feed>`;
|
|
}
|
|
|
|
export async function get_posts() {
|
|
let blogs = [];
|
|
let ids = await ssb.getIdentities();
|
|
await ssb.sqlAsync(
|
|
`
|
|
WITH
|
|
blogs AS (
|
|
SELECT
|
|
messages.author,
|
|
messages.id,
|
|
json_extract(messages.content, '$.title') AS title,
|
|
json_extract(messages.content, '$.summary') AS summary,
|
|
json_extract(messages.content, '$.blog') AS blog,
|
|
messages.timestamp
|
|
FROM messages_fts('blog')
|
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
|
WHERE json_extract(messages.content, '$.type') = 'blog'),
|
|
public AS (
|
|
SELECT author FROM (
|
|
SELECT
|
|
messages.author,
|
|
RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank,
|
|
json_extract(messages.content, '$.publicWebHosting') AS is_public
|
|
FROM messages_fts('about')
|
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
|
WHERE json_extract(messages.content, '$.type') = 'about' AND is_public IS NOT NULL)
|
|
WHERE author_rank = 1 AND is_public),
|
|
names AS (
|
|
SELECT author, name FROM (
|
|
SELECT
|
|
messages.author,
|
|
RANK() OVER (PARTITION BY messages.author ORDER BY messages.sequence DESC) AS author_rank,
|
|
json_extract(messages.content, '$.name') AS name
|
|
FROM messages_fts('about')
|
|
JOIN messages ON messages.rowid = messages_fts.rowid
|
|
WHERE json_extract(messages.content, '$.type') = 'about' AND
|
|
json_extract(messages.content, '$.about') = messages.author AND
|
|
name IS NOT NULL)
|
|
WHERE author_rank = 1)
|
|
SELECT blogs.*, names.name FROM blogs
|
|
JOIN json_each(?) AS self ON self.value = blogs.author
|
|
JOIN public ON public.author = blogs.author
|
|
LEFT OUTER JOIN names ON names.author = blogs.author
|
|
ORDER BY blogs.timestamp DESC LIMIT 20
|
|
`,
|
|
[JSON.stringify(ids)],
|
|
function (row) {
|
|
blogs.push(row);
|
|
}
|
|
);
|
|
return blogs;
|
|
}
|