Migrated silverbullet-publish into this repo
This commit is contained in:
@@ -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,
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ export async function updateMarkdownPreview() {
|
||||
smartHardBreak: true,
|
||||
annotationPositions: true,
|
||||
renderFrontMatter: true,
|
||||
attachmentUrlPrefix: "fs/",
|
||||
});
|
||||
await editor.showPanel(
|
||||
"rhs",
|
||||
|
||||
@@ -79,4 +79,8 @@ Here is something
|
||||
|
||||
A new thing.
|
||||
|
||||

|
||||
 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 .
|
||||
```
|
||||
@@ -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>
|
||||
@@ -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%;
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user