Work on $share
This commit is contained in:
parent
c5c6cd3af0
commit
5b1ec14891
@ -65,6 +65,7 @@ export async function publishCommand(options: {
|
||||
new PlugSpacePrimitives(
|
||||
new DiskSpacePrimitives(pagesPath),
|
||||
namespaceHook,
|
||||
"server",
|
||||
),
|
||||
eventHook,
|
||||
),
|
||||
|
155
plug-api/lib/frontmatter.ts
Normal file
155
plug-api/lib/frontmatter.ts
Normal file
@ -0,0 +1,155 @@
|
||||
import * as YAML from "yaml";
|
||||
|
||||
import {
|
||||
addParentPointers,
|
||||
findNodeOfType,
|
||||
ParseTree,
|
||||
renderToText,
|
||||
replaceNodesMatching,
|
||||
traverseTree,
|
||||
} from "$sb/lib/tree.ts";
|
||||
|
||||
// Extracts front matter (or legacy "meta" code blocks) from a markdown document
|
||||
// optionally removes certain keys from the front matter
|
||||
export function extractFrontmatter(
|
||||
tree: ParseTree,
|
||||
removeKeys: string[] = [],
|
||||
): any {
|
||||
let data: any = {};
|
||||
addParentPointers(tree);
|
||||
|
||||
replaceNodesMatching(tree, (t) => {
|
||||
// Find top-level hash tags
|
||||
if (t.type === "Hashtag") {
|
||||
// Check if if nested directly into a Paragraph
|
||||
if (t.parent && t.parent.type === "Paragraph") {
|
||||
const tagname = t.children![0].text!.substring(1);
|
||||
if (!data.tags) {
|
||||
data.tags = [];
|
||||
}
|
||||
if (!data.tags.includes(tagname)) {
|
||||
data.tags.push(tagname);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Find FrontMatter and parse it
|
||||
if (t.type === "FrontMatter") {
|
||||
const yamlText = renderToText(t.children![1].children![0]);
|
||||
try {
|
||||
const parsedData: any = YAML.parse(yamlText);
|
||||
const newData = { ...parsedData };
|
||||
data = { ...data, ...parsedData };
|
||||
if (removeKeys.length > 0) {
|
||||
let removedOne = false;
|
||||
|
||||
for (const key of removeKeys) {
|
||||
if (key in newData) {
|
||||
delete newData[key];
|
||||
removedOne = true;
|
||||
}
|
||||
}
|
||||
if (removedOne) {
|
||||
t.children![0].text = YAML.stringify(newData);
|
||||
}
|
||||
}
|
||||
// If nothing is left, let's just delete this whole block
|
||||
if (Object.keys(newData).length === 0) {
|
||||
return null;
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error("Could not parse frontmatter", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Find a fenced code block with `meta` as the language type
|
||||
if (t.type !== "FencedCode") {
|
||||
return;
|
||||
}
|
||||
const codeInfoNode = findNodeOfType(t, "CodeInfo");
|
||||
if (!codeInfoNode) {
|
||||
return;
|
||||
}
|
||||
if (codeInfoNode.children![0].text !== "meta") {
|
||||
return;
|
||||
}
|
||||
const codeTextNode = findNodeOfType(t, "CodeText");
|
||||
if (!codeTextNode) {
|
||||
// Honestly, this shouldn't happen
|
||||
return;
|
||||
}
|
||||
const codeText = codeTextNode.children![0].text!;
|
||||
const parsedData: any = YAML.parse(codeText);
|
||||
const newData = { ...parsedData };
|
||||
data = { ...data, ...parsedData };
|
||||
if (removeKeys.length > 0) {
|
||||
let removedOne = false;
|
||||
for (const key of removeKeys) {
|
||||
if (key in newData) {
|
||||
delete newData[key];
|
||||
removedOne = true;
|
||||
}
|
||||
}
|
||||
if (removedOne) {
|
||||
codeTextNode.children![0].text = YAML.stringify(newData).trim();
|
||||
}
|
||||
}
|
||||
// If nothing is left, let's just delete this whole block
|
||||
if (Object.keys(newData).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
|
||||
if (data.name) {
|
||||
data.displayName = data.name;
|
||||
delete data.name;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Updates the front matter of a markdown document and returns the text as a rendered string
|
||||
export function prepareFrontmatterDispatch(
|
||||
tree: ParseTree,
|
||||
data: Record<string, any>,
|
||||
): any {
|
||||
let dispatchData: any = null;
|
||||
traverseTree(tree, (t) => {
|
||||
// Find FrontMatter and parse it
|
||||
if (t.type === "FrontMatter") {
|
||||
const bodyNode = t.children![1].children![0];
|
||||
const yamlText = renderToText(bodyNode);
|
||||
|
||||
try {
|
||||
const parsedYaml = YAML.parse(yamlText) as any;
|
||||
const newData = { ...parsedYaml, ...data };
|
||||
// Patch inline
|
||||
dispatchData = {
|
||||
changes: {
|
||||
from: bodyNode.from,
|
||||
to: bodyNode.to,
|
||||
insert: YAML.stringify(newData, { noArrayIndent: true }),
|
||||
},
|
||||
};
|
||||
} catch (e: any) {
|
||||
console.error("Error parsing YAML", e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (!dispatchData) {
|
||||
// If we didn't find frontmatter, let's add it
|
||||
dispatchData = {
|
||||
changes: {
|
||||
from: 0,
|
||||
to: 0,
|
||||
insert: "---\n" + YAML.stringify(data, { noArrayIndent: true }) +
|
||||
"---\n",
|
||||
},
|
||||
};
|
||||
}
|
||||
return dispatchData;
|
||||
}
|
35
plugs/collab/collab.plug.yaml
Normal file
35
plugs/collab/collab.plug.yaml
Normal file
@ -0,0 +1,35 @@
|
||||
name: collab
|
||||
imports:
|
||||
- https://get.silverbullet.md/global.plug.json
|
||||
functions:
|
||||
detectCollabPage:
|
||||
path: "./collab.ts:detectPage"
|
||||
events:
|
||||
- editor:pageLoaded
|
||||
- plugs:loaded
|
||||
joinCommand:
|
||||
path: "./collab.ts:joinCommand"
|
||||
command:
|
||||
name: "Share: Join Collab"
|
||||
shareCommand:
|
||||
path: "./collab.ts:shareCommand"
|
||||
command:
|
||||
name: "Share: Collab"
|
||||
readPageCollab:
|
||||
path: ./collab.ts:readFileCollab
|
||||
env: client
|
||||
pageNamespace:
|
||||
pattern: "collab:.+"
|
||||
operation: readFile
|
||||
writePageCollab:
|
||||
path: ./collab.ts:writeFileCollab
|
||||
env: client
|
||||
pageNamespace:
|
||||
pattern: "collab:.+"
|
||||
operation: writeFile
|
||||
getPageMetaCollab:
|
||||
path: ./collab.ts:getFileMetaCollab
|
||||
env: client
|
||||
pageNamespace:
|
||||
pattern: "collab:.+"
|
||||
operation: getFileMeta
|
169
plugs/collab/collab.ts
Normal file
169
plugs/collab/collab.ts
Normal file
@ -0,0 +1,169 @@
|
||||
import {
|
||||
findNodeOfType,
|
||||
removeParentPointers,
|
||||
renderToText,
|
||||
} from "$sb/lib/tree.ts";
|
||||
import { getText } from "$sb/silverbullet-syscall/editor.ts";
|
||||
import { parseMarkdown } from "$sb/silverbullet-syscall/markdown.ts";
|
||||
import {
|
||||
extractFrontmatter,
|
||||
prepareFrontmatterDispatch,
|
||||
} from "$sb/lib/frontmatter.ts";
|
||||
import * as YAML from "yaml";
|
||||
import {
|
||||
clientStore,
|
||||
collab,
|
||||
editor,
|
||||
markdown,
|
||||
space,
|
||||
} from "$sb/silverbullet-syscall/mod.ts";
|
||||
|
||||
import { nanoid } from "https://esm.sh/nanoid@4.0.0";
|
||||
import {
|
||||
FileData,
|
||||
FileEncoding,
|
||||
} from "../../common/spaces/space_primitives.ts";
|
||||
import { FileMeta } from "../../common/types.ts";
|
||||
import { base64EncodedDataUrl } from "../../plugos/asset_bundle/base64.ts";
|
||||
|
||||
const defaultServer = "wss://collab.silverbullet.md";
|
||||
|
||||
async function ensureUsername(): Promise<string> {
|
||||
let username = await clientStore.get("collabUsername");
|
||||
if (!username) {
|
||||
username = await editor.prompt(
|
||||
"Please enter a publicly visible user name (or cancel for 'anonymous'):",
|
||||
);
|
||||
if (!username) {
|
||||
return "anonymous";
|
||||
} else {
|
||||
await clientStore.set("collabUsername", username);
|
||||
}
|
||||
}
|
||||
return username;
|
||||
}
|
||||
|
||||
export async function joinCommand() {
|
||||
let collabUri = await editor.prompt(
|
||||
"Collab share URI:",
|
||||
);
|
||||
if (!collabUri) {
|
||||
return;
|
||||
}
|
||||
if (!collabUri.startsWith("collab:")) {
|
||||
collabUri = "collab:" + collabUri;
|
||||
}
|
||||
await editor.navigate(collabUri);
|
||||
}
|
||||
|
||||
export async function shareCommand() {
|
||||
const serverUrl = await editor.prompt(
|
||||
"Please enter the URL of the collab server to use:",
|
||||
defaultServer,
|
||||
);
|
||||
if (!serverUrl) {
|
||||
return;
|
||||
}
|
||||
const roomId = nanoid();
|
||||
await editor.save();
|
||||
const text = await editor.getText();
|
||||
const tree = await markdown.parseMarkdown(text);
|
||||
let { $share } = extractFrontmatter(tree);
|
||||
if (!$share) {
|
||||
$share = [];
|
||||
}
|
||||
if (!Array.isArray($share)) {
|
||||
$share = [$share];
|
||||
}
|
||||
|
||||
removeParentPointers(tree);
|
||||
const dispatchData = prepareFrontmatterDispatch(tree, {
|
||||
$share: [...$share, `collab:${serverUrl}/${roomId}`],
|
||||
});
|
||||
|
||||
await editor.dispatch(dispatchData);
|
||||
|
||||
collab.start(
|
||||
serverUrl,
|
||||
roomId,
|
||||
await ensureUsername(),
|
||||
);
|
||||
}
|
||||
|
||||
export async function detectPage() {
|
||||
const tree = await parseMarkdown(await getText());
|
||||
const frontMatter = findNodeOfType(tree, "FrontMatter");
|
||||
if (frontMatter) {
|
||||
const yamlText = renderToText(frontMatter.children![1].children![0]);
|
||||
try {
|
||||
let { $share } = YAML.parse(yamlText) as any;
|
||||
if (!$share) {
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray($share)) {
|
||||
$share = [$share];
|
||||
}
|
||||
for (const uri of $share) {
|
||||
if (uri.startsWith("collab:")) {
|
||||
console.log("Going to enable collab");
|
||||
const uriPieces = uri.substring("collab:".length).split("/");
|
||||
await collab.start(
|
||||
// All parts except the last one
|
||||
uriPieces.slice(0, uriPieces.length - 1).join("/"),
|
||||
// because the last one is the room ID
|
||||
uriPieces[uriPieces.length - 1],
|
||||
await ensureUsername(),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing YAML", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function readFileCollab(
|
||||
name: string,
|
||||
encoding: FileEncoding,
|
||||
): Promise<{ data: FileData; meta: FileMeta }> {
|
||||
if (!name.endsWith(".md")) {
|
||||
throw new Error("File not found");
|
||||
}
|
||||
const collabUri = name.substring(0, name.length - ".md".length);
|
||||
const text = `---\n$share: ${collabUri}\n---\n`;
|
||||
|
||||
return {
|
||||
// encoding === "arraybuffer" is not an option, so either it's "string" or "dataurl"
|
||||
data: encoding === "string" ? text : base64EncodedDataUrl(
|
||||
"text/markdown",
|
||||
new TextEncoder().encode(text),
|
||||
),
|
||||
meta: {
|
||||
name,
|
||||
contentType: "text/markdown",
|
||||
size: text.length,
|
||||
lastModified: 0,
|
||||
perm: "rw",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getFileMetaCollab(name: string): FileMeta {
|
||||
return {
|
||||
name,
|
||||
contentType: "text/markdown",
|
||||
size: -1,
|
||||
lastModified: 0,
|
||||
perm: "rw",
|
||||
};
|
||||
}
|
||||
|
||||
export function writeFileCollab(name: string): FileMeta {
|
||||
return {
|
||||
name,
|
||||
contentType: "text/markdown",
|
||||
size: -1,
|
||||
lastModified: 0,
|
||||
perm: "rw",
|
||||
};
|
||||
}
|
@ -21,7 +21,7 @@ import {
|
||||
replaceNodesMatching,
|
||||
} from "$sb/lib/tree.ts";
|
||||
import { applyQuery } from "$sb/lib/query.ts";
|
||||
import { extractMeta } from "../directive/data.ts";
|
||||
import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
|
||||
|
||||
// Key space:
|
||||
// pl:toPage:pos => pageName
|
||||
@ -31,7 +31,7 @@ export async function indexLinks({ name, tree }: IndexTreeEvent) {
|
||||
const backLinks: { key: string; value: string }[] = [];
|
||||
// [[Style Links]]
|
||||
// console.log("Now indexing", name);
|
||||
const pageMeta = extractMeta(tree);
|
||||
const pageMeta = extractFrontmatter(tree);
|
||||
if (Object.keys(pageMeta).length > 0) {
|
||||
// console.log("Extracted page meta data", pageMeta);
|
||||
// Don't index meta data starting with $
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { editor, markdown, space } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { extractMeta } from "../directive/data.ts";
|
||||
import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
|
||||
import { renderToText } from "$sb/lib/tree.ts";
|
||||
import { niceDate } from "$sb/lib/dates.ts";
|
||||
import { readSettings } from "$sb/lib/settings_page.ts";
|
||||
@ -31,7 +31,7 @@ export async function instantiateTemplateCommand() {
|
||||
);
|
||||
|
||||
const parseTree = await markdown.parseMarkdown(text);
|
||||
const additionalPageMeta = extractMeta(parseTree, [
|
||||
const additionalPageMeta = extractFrontmatter(parseTree, [
|
||||
"$name",
|
||||
"$disableDirectives",
|
||||
]);
|
||||
|
@ -2,13 +2,13 @@ import { editor, markdown, system } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { nodeAtPos } from "$sb/lib/tree.ts";
|
||||
import { replaceAsync } from "$sb/lib/util.ts";
|
||||
import { directiveRegex, renderDirectives } from "./directives.ts";
|
||||
import { extractMeta } from "./data.ts";
|
||||
import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
|
||||
|
||||
export async function updateDirectivesOnPageCommand() {
|
||||
const pageName = await editor.getCurrentPage();
|
||||
const text = await editor.getText();
|
||||
const tree = await markdown.parseMarkdown(text);
|
||||
const metaData = extractMeta(tree, ["$disableDirectives"]);
|
||||
const metaData = extractFrontmatter(tree, ["$disableDirectives"]);
|
||||
if (metaData.$disableDirectives) {
|
||||
// Not updating, directives disabled
|
||||
return;
|
||||
|
@ -3,14 +3,7 @@
|
||||
|
||||
import type { IndexTreeEvent, QueryProviderEvent } from "$sb/app_event.ts";
|
||||
import { index } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import {
|
||||
addParentPointers,
|
||||
collectNodesOfType,
|
||||
findNodeOfType,
|
||||
ParseTree,
|
||||
renderToText,
|
||||
replaceNodesMatching,
|
||||
} from "$sb/lib/tree.ts";
|
||||
import { collectNodesOfType, findNodeOfType } from "$sb/lib/tree.ts";
|
||||
import { applyQuery, removeQueries } from "$sb/lib/query.ts";
|
||||
import * as YAML from "yaml";
|
||||
|
||||
@ -56,105 +49,6 @@ export async function indexData({ name, tree }: IndexTreeEvent) {
|
||||
await index.batchSet(name, dataObjects);
|
||||
}
|
||||
|
||||
export function extractMeta(
|
||||
parseTree: ParseTree,
|
||||
removeKeys: string[] = [],
|
||||
): any {
|
||||
let data: any = {};
|
||||
addParentPointers(parseTree);
|
||||
|
||||
replaceNodesMatching(parseTree, (t) => {
|
||||
// Find top-level hash tags
|
||||
if (t.type === "Hashtag") {
|
||||
// Check if if nested directly into a Paragraph
|
||||
if (t.parent && t.parent.type === "Paragraph") {
|
||||
const tagname = t.children![0].text!.substring(1);
|
||||
if (!data.tags) {
|
||||
data.tags = [];
|
||||
}
|
||||
if (!data.tags.includes(tagname)) {
|
||||
data.tags.push(tagname);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Find FrontMatter and parse it
|
||||
if (t.type === "FrontMatter") {
|
||||
const yamlText = renderToText(t.children![1].children![0]);
|
||||
try {
|
||||
const parsedData: any = YAML.parse(yamlText);
|
||||
const newData = { ...parsedData };
|
||||
data = { ...data, ...parsedData };
|
||||
if (removeKeys.length > 0) {
|
||||
let removedOne = false;
|
||||
|
||||
for (const key of removeKeys) {
|
||||
if (key in newData) {
|
||||
delete newData[key];
|
||||
removedOne = true;
|
||||
}
|
||||
}
|
||||
if (removedOne) {
|
||||
t.children![0].text = YAML.stringify(newData);
|
||||
}
|
||||
}
|
||||
// If nothing is left, let's just delete this whole block
|
||||
if (Object.keys(newData).length === 0) {
|
||||
return null;
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error("Could not parse frontmatter", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Find a fenced code block with `meta` as the language type
|
||||
if (t.type !== "FencedCode") {
|
||||
return;
|
||||
}
|
||||
const codeInfoNode = findNodeOfType(t, "CodeInfo");
|
||||
if (!codeInfoNode) {
|
||||
return;
|
||||
}
|
||||
if (codeInfoNode.children![0].text !== "meta") {
|
||||
return;
|
||||
}
|
||||
const codeTextNode = findNodeOfType(t, "CodeText");
|
||||
if (!codeTextNode) {
|
||||
// Honestly, this shouldn't happen
|
||||
return;
|
||||
}
|
||||
const codeText = codeTextNode.children![0].text!;
|
||||
const parsedData: any = YAML.parse(codeText);
|
||||
const newData = { ...parsedData };
|
||||
data = { ...data, ...parsedData };
|
||||
if (removeKeys.length > 0) {
|
||||
let removedOne = false;
|
||||
for (const key of removeKeys) {
|
||||
if (key in newData) {
|
||||
delete newData[key];
|
||||
removedOne = true;
|
||||
}
|
||||
}
|
||||
if (removedOne) {
|
||||
codeTextNode.children![0].text = YAML.stringify(newData).trim();
|
||||
}
|
||||
}
|
||||
// If nothing is left, let's just delete this whole block
|
||||
if (Object.keys(newData).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
|
||||
if (data.name) {
|
||||
data.displayName = data.name;
|
||||
delete data.name;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function queryProvider({
|
||||
query,
|
||||
}: QueryProviderEvent): Promise<any[]> {
|
||||
|
@ -2,7 +2,6 @@ import { nodeAtPos, ParseTree, renderToText } from "$sb/lib/tree.ts";
|
||||
import { replaceAsync } from "$sb/lib/util.ts";
|
||||
import { markdown } from "$sb/silverbullet-syscall/mod.ts";
|
||||
|
||||
import { extractMeta } from "./data.ts";
|
||||
import { evalDirectiveRenderer } from "./eval_directive.ts";
|
||||
import { queryDirectiveRenderer } from "./query_directive.ts";
|
||||
import {
|
||||
|
@ -5,7 +5,7 @@ import { markdown, space } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import Handlebars from "handlebars";
|
||||
|
||||
import { replaceTemplateVars } from "../core/template.ts";
|
||||
import { extractMeta } from "./data.ts";
|
||||
import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
|
||||
import { directiveRegex, renderDirectives } from "./directives.ts";
|
||||
|
||||
const templateRegex = /\[\[([^\]]+)\]\]\s*(.*)\s*/;
|
||||
@ -44,7 +44,7 @@ export async function templateDirectiveRenderer(
|
||||
// if it's a template injection (not a literal "include")
|
||||
if (directive === "use" || directive === "use-verbose") {
|
||||
const tree = await markdown.parseMarkdown(templateText);
|
||||
extractMeta(tree, ["$disableDirectives"]);
|
||||
extractFrontmatter(tree, ["$disableDirectives"]);
|
||||
templateText = renderToText(tree);
|
||||
const templateFn = Handlebars.compile(
|
||||
replaceTemplateVars(templateText, pageName),
|
||||
|
@ -3,6 +3,8 @@ imports:
|
||||
- https://get.silverbullet.md/global.plug.json
|
||||
assets:
|
||||
- "assets/*"
|
||||
requiredPermissions:
|
||||
- fs
|
||||
functions:
|
||||
toggle:
|
||||
path: "./markdown.ts:togglePreview"
|
||||
@ -22,4 +24,10 @@ functions:
|
||||
path: "./preview.ts:previewClickHandler"
|
||||
env: client
|
||||
events:
|
||||
- preview:click
|
||||
- preview:click
|
||||
|
||||
# $share: file:* publisher for markdown files
|
||||
sharePublisher:
|
||||
path: ./share.ts:sharePublisher
|
||||
events:
|
||||
- share:file
|
21
plugs/markdown/share.ts
Normal file
21
plugs/markdown/share.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { markdown, space } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { fs } from "$sb/plugos-syscall/mod.ts";
|
||||
import { asset } from "$sb/plugos-syscall/mod.ts";
|
||||
import type { PublishEvent } from "../share/publish.ts";
|
||||
import { renderMarkdownToHtml } from "./markdown_render.ts";
|
||||
|
||||
export async function sharePublisher(event: PublishEvent) {
|
||||
const path = event.uri.split(":")[1];
|
||||
const pageName = event.name;
|
||||
const text = await space.readPage(pageName);
|
||||
const tree = await markdown.parseMarkdown(text);
|
||||
|
||||
const css = await asset.readAsset("assets/styles.css");
|
||||
const markdownHtml = renderMarkdownToHtml(tree, {
|
||||
smartHardBreak: true,
|
||||
});
|
||||
const html =
|
||||
`<html><head><style>${css}</style></head><body><div id="root">${markdownHtml}</div></body></html>`;
|
||||
await fs.writeFile(path, html, "utf8");
|
||||
return true;
|
||||
}
|
48
plugs/share/publish.ts
Normal file
48
plugs/share/publish.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { events } from "$sb/plugos-syscall/mod.ts";
|
||||
import { editor, markdown, system } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
|
||||
|
||||
export type PublishEvent = {
|
||||
uri: string;
|
||||
// Page name
|
||||
name: string;
|
||||
};
|
||||
|
||||
export async function publishCommand() {
|
||||
await editor.save();
|
||||
const text = await editor.getText();
|
||||
const pageName = await editor.getCurrentPage();
|
||||
const tree = await markdown.parseMarkdown(text);
|
||||
let { $share } = extractFrontmatter(tree);
|
||||
if (!$share) {
|
||||
await editor.flashNotification("No $share directive found", "error");
|
||||
return;
|
||||
}
|
||||
if (!Array.isArray($share)) {
|
||||
$share = [$share];
|
||||
}
|
||||
// Delegate actual publishing to the server
|
||||
try {
|
||||
await system.invokeFunction("server", "publish", pageName, $share);
|
||||
await editor.flashNotification("Done!");
|
||||
} catch (e: any) {
|
||||
await editor.flashNotification(e.message, "error");
|
||||
}
|
||||
}
|
||||
|
||||
// Runs on server side
|
||||
export async function publish(pageName: string, uris: string[]) {
|
||||
for (const uri of uris) {
|
||||
const publisher = uri.split(":")[0];
|
||||
const results = await events.dispatchEvent(
|
||||
`share:${publisher}`,
|
||||
{
|
||||
uri: uri,
|
||||
name: pageName,
|
||||
} as PublishEvent,
|
||||
);
|
||||
if (results.length === 0) {
|
||||
throw new Error(`Unsupported publisher: ${publisher} for URI: ${uri}`);
|
||||
}
|
||||
}
|
||||
}
|
9
plugs/share/share.plug.yaml
Normal file
9
plugs/share/share.plug.yaml
Normal file
@ -0,0 +1,9 @@
|
||||
name: share
|
||||
functions:
|
||||
publishCommand:
|
||||
path: publish.ts:publishCommand
|
||||
command:
|
||||
name: "Share: Publish"
|
||||
publish:
|
||||
path: publish.ts:publish
|
||||
env: server
|
@ -23,6 +23,7 @@ type SpaceFunction = {
|
||||
pattern: RegExp;
|
||||
plug: Plug<PageNamespaceHookT>;
|
||||
name: string;
|
||||
env?: string;
|
||||
};
|
||||
|
||||
export class PageNamespaceHook implements Hook<PageNamespaceHookT> {
|
||||
@ -42,10 +43,10 @@ export class PageNamespaceHook implements Hook<PageNamespaceHookT> {
|
||||
|
||||
updateCache(system: System<PageNamespaceHookT>) {
|
||||
this.spaceFunctions = [];
|
||||
for (let plug of system.loadedPlugs.values()) {
|
||||
for (const plug of system.loadedPlugs.values()) {
|
||||
if (plug.manifest?.functions) {
|
||||
for (
|
||||
let [funcName, funcDef] of Object.entries(
|
||||
const [funcName, funcDef] of Object.entries(
|
||||
plug.manifest.functions,
|
||||
)
|
||||
) {
|
||||
@ -55,6 +56,7 @@ export class PageNamespaceHook implements Hook<PageNamespaceHookT> {
|
||||
pattern: new RegExp(funcDef.pageNamespace.pattern),
|
||||
plug,
|
||||
name: funcName,
|
||||
env: funcDef.env,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -63,7 +65,7 @@ export class PageNamespaceHook implements Hook<PageNamespaceHookT> {
|
||||
}
|
||||
|
||||
validateManifest(manifest: Manifest<PageNamespaceHookT>): string[] {
|
||||
let errors: string[] = [];
|
||||
const errors: string[] = [];
|
||||
if (!manifest.functions) {
|
||||
return [];
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ export class PlugSpacePrimitives implements SpacePrimitives {
|
||||
constructor(
|
||||
private wrapped: SpacePrimitives,
|
||||
private hook: PageNamespaceHook,
|
||||
private env: string,
|
||||
) {}
|
||||
|
||||
performOperation(
|
||||
@ -19,8 +20,13 @@ export class PlugSpacePrimitives implements SpacePrimitives {
|
||||
pageName: string,
|
||||
...args: any[]
|
||||
): Promise<any> | false {
|
||||
for (const { operation, pattern, plug, name } of this.hook.spaceFunctions) {
|
||||
if (operation === type && pageName.match(pattern)) {
|
||||
for (
|
||||
const { operation, pattern, plug, name, env } of this.hook.spaceFunctions
|
||||
) {
|
||||
if (
|
||||
operation === type && pageName.match(pattern) &&
|
||||
(env ? env === this.env : true)
|
||||
) {
|
||||
return plug.invoke(name, [pageName, ...args]);
|
||||
}
|
||||
}
|
||||
|
@ -102,6 +102,7 @@ export class HttpServer {
|
||||
new PlugSpacePrimitives(
|
||||
new DiskSpacePrimitives(options.pagesPath),
|
||||
namespaceHook,
|
||||
"server",
|
||||
),
|
||||
this.eventHook,
|
||||
),
|
||||
|
27
web/boot.ts
27
web/boot.ts
@ -2,6 +2,10 @@ import { Editor } from "./editor.tsx";
|
||||
import { parseYamlSettings, safeRun } from "../common/util.ts";
|
||||
import { Space } from "../common/spaces/space.ts";
|
||||
import { HttpSpacePrimitives } from "../common/spaces/http_space_primitives.ts";
|
||||
import { PlugSpacePrimitives } from "../server/hooks/plug_space_primitives.ts";
|
||||
import { PageNamespaceHook } from "../server/hooks/page_namespace.ts";
|
||||
import { SilverBulletHooks } from "../common/manifest.ts";
|
||||
import { System } from "../plugos/system.ts";
|
||||
|
||||
safeRun(async () => {
|
||||
let password: string | undefined = localStorage.getItem("password") ||
|
||||
@ -27,7 +31,21 @@ safeRun(async () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
const serverSpace = new Space(httpPrimitives);
|
||||
|
||||
// Instantiate a PlugOS system for the client
|
||||
const system = new System<SilverBulletHooks>("client");
|
||||
|
||||
// Attach the page namespace hook
|
||||
const namespaceHook = new PageNamespaceHook();
|
||||
system.addHook(namespaceHook);
|
||||
|
||||
const spacePrimitives = new PlugSpacePrimitives(
|
||||
httpPrimitives,
|
||||
namespaceHook,
|
||||
"client",
|
||||
);
|
||||
|
||||
const serverSpace = new Space(spacePrimitives);
|
||||
serverSpace.watch();
|
||||
|
||||
console.log("Booting...");
|
||||
@ -36,6 +54,7 @@ safeRun(async () => {
|
||||
|
||||
const editor = new Editor(
|
||||
serverSpace,
|
||||
system,
|
||||
document.getElementById("sb-root")!,
|
||||
"",
|
||||
settings.indexPage || "index",
|
||||
@ -46,10 +65,9 @@ safeRun(async () => {
|
||||
await editor.init();
|
||||
});
|
||||
|
||||
// if (localStorage.getItem("disable_sw") !== "true") {
|
||||
if (navigator.serviceWorker) {
|
||||
navigator.serviceWorker
|
||||
.register(new URL("service_worker.js", location.href), {
|
||||
.register(new URL("/service_worker.js", location.href), {
|
||||
type: "module",
|
||||
})
|
||||
.then((r) => {
|
||||
@ -60,6 +78,3 @@ if (navigator.serviceWorker) {
|
||||
"No launching service worker (not present, maybe because not running on localhost or over SSL)",
|
||||
);
|
||||
}
|
||||
// } else {
|
||||
// console.log("Service worker disabled via disable_sw");
|
||||
// }
|
||||
|
@ -67,7 +67,6 @@ export function TopBar({
|
||||
value={pageName}
|
||||
className="sb-edit-page-name"
|
||||
onKeyDown={(e) => {
|
||||
console.log("Key press", e);
|
||||
e.stopPropagation();
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
|
@ -131,7 +131,7 @@ export class Editor {
|
||||
.dispatchEvent("editor:updated")
|
||||
.catch((e) => console.error("Error dispatching editor:updated event", e));
|
||||
}, 1000);
|
||||
private system = new System<SilverBulletHooks>("client");
|
||||
private system: System<SilverBulletHooks>;
|
||||
private mdExtensions: MDExt[] = [];
|
||||
urlPrefix: string;
|
||||
indexPage: string;
|
||||
@ -139,11 +139,13 @@ export class Editor {
|
||||
|
||||
constructor(
|
||||
space: Space,
|
||||
system: System<SilverBulletHooks>,
|
||||
parent: Element,
|
||||
urlPrefix: string,
|
||||
indexPage: string,
|
||||
) {
|
||||
this.space = space;
|
||||
this.system = system;
|
||||
this.urlPrefix = urlPrefix;
|
||||
this.viewState = initialViewState;
|
||||
this.viewDispatch = () => {};
|
||||
@ -223,6 +225,40 @@ export class Editor {
|
||||
async init() {
|
||||
this.focus();
|
||||
|
||||
const globalModules: any = await (
|
||||
await fetch(`${this.urlPrefix}/global.plug.json`)
|
||||
).json();
|
||||
|
||||
this.system.on({
|
||||
sandboxInitialized: async (sandbox) => {
|
||||
for (
|
||||
const [modName, code] of Object.entries(
|
||||
globalModules.dependencies,
|
||||
)
|
||||
) {
|
||||
await sandbox.loadDependency(modName, code as string);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
this.space.on({
|
||||
pageChanged: (meta) => {
|
||||
if (this.currentPage === meta.name) {
|
||||
console.log("Page changed on disk, reloading");
|
||||
this.flashNotification("Page changed on disk, reloading");
|
||||
this.reloadPage();
|
||||
}
|
||||
},
|
||||
pageListUpdated: (pages) => {
|
||||
this.viewDispatch({
|
||||
type: "pages-listed",
|
||||
pages: pages,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
await this.reloadPlugs();
|
||||
|
||||
this.pageNavigator.subscribe(async (pageName, pos: number | string) => {
|
||||
console.log("Now navigating to", pageName);
|
||||
|
||||
@ -266,39 +302,6 @@ export class Editor {
|
||||
}
|
||||
});
|
||||
|
||||
const globalModules: any = await (
|
||||
await fetch(`${this.urlPrefix}/global.plug.json`)
|
||||
).json();
|
||||
|
||||
this.system.on({
|
||||
sandboxInitialized: async (sandbox) => {
|
||||
for (
|
||||
const [modName, code] of Object.entries(
|
||||
globalModules.dependencies,
|
||||
)
|
||||
) {
|
||||
await sandbox.loadDependency(modName, code as string);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
this.space.on({
|
||||
pageChanged: (meta) => {
|
||||
if (this.currentPage === meta.name) {
|
||||
console.log("Page changed on disk, reloading");
|
||||
this.flashNotification("Page changed on disk, reloading");
|
||||
this.reloadPage();
|
||||
}
|
||||
},
|
||||
pageListUpdated: (pages) => {
|
||||
this.viewDispatch({
|
||||
type: "pages-listed",
|
||||
pages: pages,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
await this.reloadPlugs();
|
||||
await this.dispatchAppEvent("editor:init");
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user