Migrated silverbullet-publish into this repo

This commit is contained in:
Zef Hemel
2022-11-01 17:03:42 +01:00
parent 3d671e8195
commit c4f80589f3
18 changed files with 567 additions and 18 deletions
+20 -4
View File
@@ -12,6 +12,7 @@ type MarkdownRenderOptions = {
smartHardBreak?: true;
annotationPositions?: true;
renderFrontMatter?: true;
attachmentUrlPrefix?: string;
};
function cleanTags(values: (Tag | null)[]): Tag[] {
@@ -138,11 +139,15 @@ function render(
// Code blocks
case "FencedCode":
case "CodeBlock": {
// Clear out top-level indent blocks
t.children = t.children!.filter((c) => c.type);
return {
name: "pre",
body: cleanTags(mapRender(t.children!)),
};
}
case "CodeInfo":
return null;
case "CodeText":
return t.children![0].text!;
case "Blockquote":
@@ -201,7 +206,14 @@ function render(
};
case "Link": {
const linkText = t.children![1].text!;
const url = findNodeOfType(t, "URL")!.children![0].text!;
const urlNode = findNodeOfType(t, "URL");
if (!urlNode) {
return renderToText(t);
}
let url = urlNode.children![0].text!;
if (url.indexOf("://") === -1) {
url = `${options.attachmentUrlPrefix || ""}${url}`;
}
return {
name: "a",
attrs: {
@@ -212,9 +224,13 @@ function render(
}
case "Image": {
const altText = t.children![1].text!;
let url = findNodeOfType(t, "URL")!.children![0].text!;
const urlNode = findNodeOfType(t, "URL");
if (!urlNode) {
return renderToText(t);
}
let url = urlNode!.children![0].text!;
if (url.indexOf("://") === -1) {
url = `fs/${url}`;
url = `${options.attachmentUrlPrefix || ""}${url}`;
}
return {
name: "img",
@@ -233,7 +249,7 @@ function render(
return {
name: "a",
attrs: {
href: `/${ref}`,
href: `/${ref.replaceAll(" ", "_").replace("@", "#")}`,
},
body: ref,
};
+1
View File
@@ -16,6 +16,7 @@ export async function updateMarkdownPreview() {
smartHardBreak: true,
annotationPositions: true,
renderFrontMatter: true,
attachmentUrlPrefix: "fs/",
});
await editor.showPanel(
"rhs",
+5 -1
View File
@@ -79,4 +79,8 @@ Here is something
A new thing.
![alt text](https://image.jpg)
![alt text](https:/
## Weird stuff
* [{{#if done}}x{{else}} {{/if}}] [[{{page}}@{{pos}}]] {{name}} {{#if deadline}}📅 {{deadline}}{{/if}} {{#each tags}}{{.}} {{/each}}
+45
View File
@@ -0,0 +1,45 @@
# Silver Bullet Publish
A simple tool to export a subset of your [SilverBullet](https://silverbullet.md) space as a static website.
**Note:** this is highly experimental and not necessarily production ready code, use at your own risk.
silverbullet-publish currentenly publishes a subset of a space in two formats:
* Markdown (.md files)
* HTML (.html files based on currently hardcoded templates (see `page.hbs` and `style.css`)
The tool can be run in two ways:
1. As a Silver Bullet plug (via the `Silver Bullet Publish: Publish All` command)
2. As a stand-alone CLI tool (via `npx`)
The latter allows for automatic deployments to e.g. environments like Netlify.
## Configuration
SilverBullet Publish is configured via the `PUBLISH` page with the following properties:
```yaml
# Index page to use for public version
indexPage: Public
# Optional destination folder when used in plug mode
destDir: /Users/you/my-website
title: Name of the space
removeHashtags: true
removeUnpublishedLinks: false
# Publish all pages with specific tag
tags:
- "#pub"
# Publish all pages with a specifix prefix
prefixes:
- /public
```
## Running via `npx`
The easiest way to run SilverBullet Publish is via `npx`, it takes a few optional arguments beyond the path to your SilverBullet space:
* `-o` specifies where to write the output to (defaults to `./web`)
* `--index` runs a full space index (e.g. to index all hash tags) before publishing, this is primarily useful when run in a CI/CI pipeline (like Netlify) because there no `data.db` in your repo containing this index.
```bash
npx @silverbulletmd/publish -o web_build --index .
```
+16
View File
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{#if pageName}}{{pageName}} — {{config.title}}{{else}}{{config.title}}{{/if}}</title>
<style>
{{css}}
</style>
</head>
<body>
<h1>{{pageName}}</h1>
{{{body}}}
</body>
</html>
+53
View File
@@ -0,0 +1,53 @@
body {
font-family: georgia, times, serif;
font-size: 14pt;
max-width: 800px;
margin-left: auto;
margin-right: auto;
padding-left: 20px;
padding-right: 20px;
}
table {
width: 100%;
border-spacing: 0;
}
thead tr {
background-color: #333;
color: #eee;
}
th,
td {
padding: 8px;
}
tbody tr:nth-of-type(even) {
background-color: #f3f3f3;
}
ul li p {
margin: 0;
}
a[href] {
text-decoration: none;
}
blockquote {
border-left: 1px solid #333;
margin-left: 2px;
padding-left: 10px;
}
.footer {
border-top: 1px solid #000;
padding-top: 5px;
text-align: center;
font-size: 70%;
}
img {
max-width: 90%;
}
+15
View File
@@ -0,0 +1,15 @@
name: publish
imports:
- https://get.silverbullet.md/global.plug.json
requiredPermissions:
- fs
assets:
- "assets/*"
functions:
publishAll:
path: "./publish.ts:publishAll"
env: server
publishAllCommand:
path: "./publish.ts:publishAllCommand"
command:
name: "Silver Bullet Publish: Publish All"
+208
View File
@@ -0,0 +1,208 @@
import { asset, fs } from "$sb/plugos-syscall/mod.ts";
import {
editor,
index,
markdown,
space,
system,
} from "$sb/silverbullet-syscall/mod.ts";
import { readYamlPage } from "$sb/lib/yaml_page.ts";
import { renderMarkdownToHtml } from "../markdown/markdown_render.ts";
import Handlebars from "handlebars";
import {
collectNodesOfType,
findNodeOfType,
ParseTree,
renderToText,
replaceNodesMatching,
} from "$sb/lib/tree.ts";
type PublishConfig = {
destDir?: string;
title?: string;
indexPage?: string;
removeHashtags?: boolean;
publishAll?: boolean;
tags?: string[];
prefixes?: string[];
footerPage?: string;
};
async function generatePage(
pageName: string,
htmlPath: string,
mdPath: string,
publishedPages: string[],
publishConfig: PublishConfig,
destDir: string,
footerText: string,
) {
const pageTemplate = await asset.readAsset("assets/page.hbs");
const pageCSS = await asset.readAsset("assets/style.css");
const text = await space.readPage(pageName);
const renderPage = Handlebars.compile(pageTemplate);
console.log("Writing", pageName);
const mdTree = await markdown.parseMarkdown(`${text}\n${footerText}`);
const publishMd = cleanMarkdown(
mdTree,
publishConfig,
publishedPages,
);
const attachments = await collectAttachments(mdTree);
for (const attachment of attachments) {
try {
const result = await space.readAttachment(attachment);
console.log("Writing", `${destDir}/${attachment}`);
await fs.writeFile(`${destDir}/${attachment}`, result, "dataurl");
} catch (e: any) {
console.error("Error reading attachment", attachment, e.message);
}
}
// Write .md file
await fs.writeFile(mdPath, publishMd);
// Write .html file
await fs.writeFile(
htmlPath,
renderPage({
pageName,
config: publishConfig,
css: pageCSS,
body: renderMarkdownToHtml(mdTree, {
smartHardBreak: true,
attachmentUrlPrefix: "/",
}),
}),
);
}
export async function publishAll(destDir?: string) {
const publishConfig: PublishConfig = await readYamlPage("PUBLISH");
destDir = destDir || publishConfig.destDir || ".";
console.log("Publishing to", destDir);
let allPages: any[] = await space.listPages();
let allPageMap: Map<string, any> = new Map(
allPages.map((pm) => [pm.name, pm]),
);
for (const { page, value } of await index.queryPrefix("meta:")) {
const p = allPageMap.get(page);
if (p) {
for (const [k, v] of Object.entries(value)) {
p[k] = v;
}
}
}
allPages = [...allPageMap.values()];
let publishedPages = new Set<string>();
if (publishConfig.publishAll) {
publishedPages = new Set(allPages.map((p) => p.name));
} else {
for (const page of allPages) {
if (publishConfig.tags && page.tags) {
for (const tag of page.tags) {
if (publishConfig.tags.includes(tag)) {
publishedPages.add(page.name);
}
}
}
// Some sanity checking
if (typeof page.name !== "string") {
continue;
}
if (publishConfig.prefixes) {
for (const prefix of publishConfig.prefixes) {
if (page.name.startsWith(prefix)) {
publishedPages.add(page.name);
}
}
}
}
}
console.log("Starting this thing", [...publishedPages]);
let footer = "";
if (publishConfig.footerPage) {
footer = await space.readPage(publishConfig.footerPage);
}
const publishedPagesArray = [...publishedPages];
for (const page of publishedPagesArray) {
await generatePage(
page,
`${destDir}/${page.replaceAll(" ", "_")}/index.html`,
`${destDir}/${page}.md`,
publishedPagesArray,
publishConfig,
destDir,
footer,
);
}
if (publishConfig.indexPage) {
console.log("Writing", publishConfig.indexPage);
await generatePage(
publishConfig.indexPage,
`${destDir}/index.html`,
`${destDir}/index.md`,
publishedPagesArray,
publishConfig,
destDir,
footer,
);
}
}
export async function publishAllCommand() {
await editor.flashNotification("Publishing...");
await await system.invokeFunction("server", "publishAll");
await editor.flashNotification("Done!");
}
export function encodePageUrl(name: string): string {
return name.replaceAll(" ", "_");
}
async function collectAttachments(tree: ParseTree) {
const attachments: string[] = [];
collectNodesOfType(tree, "URL").forEach((node) => {
let url = node.children![0].text!;
if (url.indexOf("://") === -1) {
attachments.push(url);
}
});
return attachments;
}
function cleanMarkdown(
mdTree: ParseTree,
publishConfig: PublishConfig,
validPages: string[],
): string {
replaceNodesMatching(mdTree, (n) => {
if (n.type === "WikiLink") {
let page = n.children![1].children![0].text!;
if (page.includes("@")) {
page = page.split("@")[0];
}
if (!validPages.includes(page)) {
// Replace with just page text
return {
text: `_${page}_`,
};
}
}
// Simply get rid of these
if (n.type === "CommentBlock" || n.type === "Comment") {
return null;
}
if (n.type === "Hashtag") {
if (publishConfig.removeHashtags) {
return null;
}
}
});
return renderToText(mdTree).trim();
}