Work on $share
parent
c5c6cd3af0
commit
5b1ec14891
|
@ -65,6 +65,7 @@ export async function publishCommand(options: {
|
||||||
new PlugSpacePrimitives(
|
new PlugSpacePrimitives(
|
||||||
new DiskSpacePrimitives(pagesPath),
|
new DiskSpacePrimitives(pagesPath),
|
||||||
namespaceHook,
|
namespaceHook,
|
||||||
|
"server",
|
||||||
),
|
),
|
||||||
eventHook,
|
eventHook,
|
||||||
),
|
),
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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
|
|
@ -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,
|
replaceNodesMatching,
|
||||||
} from "$sb/lib/tree.ts";
|
} from "$sb/lib/tree.ts";
|
||||||
import { applyQuery } from "$sb/lib/query.ts";
|
import { applyQuery } from "$sb/lib/query.ts";
|
||||||
import { extractMeta } from "../directive/data.ts";
|
import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
|
||||||
|
|
||||||
// Key space:
|
// Key space:
|
||||||
// pl:toPage:pos => pageName
|
// pl:toPage:pos => pageName
|
||||||
|
@ -31,7 +31,7 @@ export async function indexLinks({ name, tree }: IndexTreeEvent) {
|
||||||
const backLinks: { key: string; value: string }[] = [];
|
const backLinks: { key: string; value: string }[] = [];
|
||||||
// [[Style Links]]
|
// [[Style Links]]
|
||||||
// console.log("Now indexing", name);
|
// console.log("Now indexing", name);
|
||||||
const pageMeta = extractMeta(tree);
|
const pageMeta = extractFrontmatter(tree);
|
||||||
if (Object.keys(pageMeta).length > 0) {
|
if (Object.keys(pageMeta).length > 0) {
|
||||||
// console.log("Extracted page meta data", pageMeta);
|
// console.log("Extracted page meta data", pageMeta);
|
||||||
// Don't index meta data starting with $
|
// Don't index meta data starting with $
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { editor, markdown, space } from "$sb/silverbullet-syscall/mod.ts";
|
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 { renderToText } from "$sb/lib/tree.ts";
|
||||||
import { niceDate } from "$sb/lib/dates.ts";
|
import { niceDate } from "$sb/lib/dates.ts";
|
||||||
import { readSettings } from "$sb/lib/settings_page.ts";
|
import { readSettings } from "$sb/lib/settings_page.ts";
|
||||||
|
@ -31,7 +31,7 @@ export async function instantiateTemplateCommand() {
|
||||||
);
|
);
|
||||||
|
|
||||||
const parseTree = await markdown.parseMarkdown(text);
|
const parseTree = await markdown.parseMarkdown(text);
|
||||||
const additionalPageMeta = extractMeta(parseTree, [
|
const additionalPageMeta = extractFrontmatter(parseTree, [
|
||||||
"$name",
|
"$name",
|
||||||
"$disableDirectives",
|
"$disableDirectives",
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -2,13 +2,13 @@ import { editor, markdown, system } from "$sb/silverbullet-syscall/mod.ts";
|
||||||
import { nodeAtPos } from "$sb/lib/tree.ts";
|
import { nodeAtPos } from "$sb/lib/tree.ts";
|
||||||
import { replaceAsync } from "$sb/lib/util.ts";
|
import { replaceAsync } from "$sb/lib/util.ts";
|
||||||
import { directiveRegex, renderDirectives } from "./directives.ts";
|
import { directiveRegex, renderDirectives } from "./directives.ts";
|
||||||
import { extractMeta } from "./data.ts";
|
import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
|
||||||
|
|
||||||
export async function updateDirectivesOnPageCommand() {
|
export async function updateDirectivesOnPageCommand() {
|
||||||
const pageName = await editor.getCurrentPage();
|
const pageName = await editor.getCurrentPage();
|
||||||
const text = await editor.getText();
|
const text = await editor.getText();
|
||||||
const tree = await markdown.parseMarkdown(text);
|
const tree = await markdown.parseMarkdown(text);
|
||||||
const metaData = extractMeta(tree, ["$disableDirectives"]);
|
const metaData = extractFrontmatter(tree, ["$disableDirectives"]);
|
||||||
if (metaData.$disableDirectives) {
|
if (metaData.$disableDirectives) {
|
||||||
// Not updating, directives disabled
|
// Not updating, directives disabled
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -3,14 +3,7 @@
|
||||||
|
|
||||||
import type { IndexTreeEvent, QueryProviderEvent } from "$sb/app_event.ts";
|
import type { IndexTreeEvent, QueryProviderEvent } from "$sb/app_event.ts";
|
||||||
import { index } from "$sb/silverbullet-syscall/mod.ts";
|
import { index } from "$sb/silverbullet-syscall/mod.ts";
|
||||||
import {
|
import { collectNodesOfType, findNodeOfType } from "$sb/lib/tree.ts";
|
||||||
addParentPointers,
|
|
||||||
collectNodesOfType,
|
|
||||||
findNodeOfType,
|
|
||||||
ParseTree,
|
|
||||||
renderToText,
|
|
||||||
replaceNodesMatching,
|
|
||||||
} from "$sb/lib/tree.ts";
|
|
||||||
import { applyQuery, removeQueries } from "$sb/lib/query.ts";
|
import { applyQuery, removeQueries } from "$sb/lib/query.ts";
|
||||||
import * as YAML from "yaml";
|
import * as YAML from "yaml";
|
||||||
|
|
||||||
|
@ -56,105 +49,6 @@ export async function indexData({ name, tree }: IndexTreeEvent) {
|
||||||
await index.batchSet(name, dataObjects);
|
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({
|
export async function queryProvider({
|
||||||
query,
|
query,
|
||||||
}: QueryProviderEvent): Promise<any[]> {
|
}: QueryProviderEvent): Promise<any[]> {
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { nodeAtPos, ParseTree, renderToText } from "$sb/lib/tree.ts";
|
||||||
import { replaceAsync } from "$sb/lib/util.ts";
|
import { replaceAsync } from "$sb/lib/util.ts";
|
||||||
import { markdown } from "$sb/silverbullet-syscall/mod.ts";
|
import { markdown } from "$sb/silverbullet-syscall/mod.ts";
|
||||||
|
|
||||||
import { extractMeta } from "./data.ts";
|
|
||||||
import { evalDirectiveRenderer } from "./eval_directive.ts";
|
import { evalDirectiveRenderer } from "./eval_directive.ts";
|
||||||
import { queryDirectiveRenderer } from "./query_directive.ts";
|
import { queryDirectiveRenderer } from "./query_directive.ts";
|
||||||
import {
|
import {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { markdown, space } from "$sb/silverbullet-syscall/mod.ts";
|
||||||
import Handlebars from "handlebars";
|
import Handlebars from "handlebars";
|
||||||
|
|
||||||
import { replaceTemplateVars } from "../core/template.ts";
|
import { replaceTemplateVars } from "../core/template.ts";
|
||||||
import { extractMeta } from "./data.ts";
|
import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
|
||||||
import { directiveRegex, renderDirectives } from "./directives.ts";
|
import { directiveRegex, renderDirectives } from "./directives.ts";
|
||||||
|
|
||||||
const templateRegex = /\[\[([^\]]+)\]\]\s*(.*)\s*/;
|
const templateRegex = /\[\[([^\]]+)\]\]\s*(.*)\s*/;
|
||||||
|
@ -44,7 +44,7 @@ export async function templateDirectiveRenderer(
|
||||||
// if it's a template injection (not a literal "include")
|
// if it's a template injection (not a literal "include")
|
||||||
if (directive === "use" || directive === "use-verbose") {
|
if (directive === "use" || directive === "use-verbose") {
|
||||||
const tree = await markdown.parseMarkdown(templateText);
|
const tree = await markdown.parseMarkdown(templateText);
|
||||||
extractMeta(tree, ["$disableDirectives"]);
|
extractFrontmatter(tree, ["$disableDirectives"]);
|
||||||
templateText = renderToText(tree);
|
templateText = renderToText(tree);
|
||||||
const templateFn = Handlebars.compile(
|
const templateFn = Handlebars.compile(
|
||||||
replaceTemplateVars(templateText, pageName),
|
replaceTemplateVars(templateText, pageName),
|
||||||
|
|
|
@ -3,6 +3,8 @@ imports:
|
||||||
- https://get.silverbullet.md/global.plug.json
|
- https://get.silverbullet.md/global.plug.json
|
||||||
assets:
|
assets:
|
||||||
- "assets/*"
|
- "assets/*"
|
||||||
|
requiredPermissions:
|
||||||
|
- fs
|
||||||
functions:
|
functions:
|
||||||
toggle:
|
toggle:
|
||||||
path: "./markdown.ts:togglePreview"
|
path: "./markdown.ts:togglePreview"
|
||||||
|
@ -23,3 +25,9 @@ functions:
|
||||||
env: client
|
env: client
|
||||||
events:
|
events:
|
||||||
- preview:click
|
- preview:click
|
||||||
|
|
||||||
|
# $share: file:* publisher for markdown files
|
||||||
|
sharePublisher:
|
||||||
|
path: ./share.ts:sharePublisher
|
||||||
|
events:
|
||||||
|
- share: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;
|
||||||
|
}
|
|
@ -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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
pattern: RegExp;
|
||||||
plug: Plug<PageNamespaceHookT>;
|
plug: Plug<PageNamespaceHookT>;
|
||||||
name: string;
|
name: string;
|
||||||
|
env?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class PageNamespaceHook implements Hook<PageNamespaceHookT> {
|
export class PageNamespaceHook implements Hook<PageNamespaceHookT> {
|
||||||
|
@ -42,10 +43,10 @@ export class PageNamespaceHook implements Hook<PageNamespaceHookT> {
|
||||||
|
|
||||||
updateCache(system: System<PageNamespaceHookT>) {
|
updateCache(system: System<PageNamespaceHookT>) {
|
||||||
this.spaceFunctions = [];
|
this.spaceFunctions = [];
|
||||||
for (let plug of system.loadedPlugs.values()) {
|
for (const plug of system.loadedPlugs.values()) {
|
||||||
if (plug.manifest?.functions) {
|
if (plug.manifest?.functions) {
|
||||||
for (
|
for (
|
||||||
let [funcName, funcDef] of Object.entries(
|
const [funcName, funcDef] of Object.entries(
|
||||||
plug.manifest.functions,
|
plug.manifest.functions,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
@ -55,6 +56,7 @@ export class PageNamespaceHook implements Hook<PageNamespaceHookT> {
|
||||||
pattern: new RegExp(funcDef.pageNamespace.pattern),
|
pattern: new RegExp(funcDef.pageNamespace.pattern),
|
||||||
plug,
|
plug,
|
||||||
name: funcName,
|
name: funcName,
|
||||||
|
env: funcDef.env,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,7 +65,7 @@ export class PageNamespaceHook implements Hook<PageNamespaceHookT> {
|
||||||
}
|
}
|
||||||
|
|
||||||
validateManifest(manifest: Manifest<PageNamespaceHookT>): string[] {
|
validateManifest(manifest: Manifest<PageNamespaceHookT>): string[] {
|
||||||
let errors: string[] = [];
|
const errors: string[] = [];
|
||||||
if (!manifest.functions) {
|
if (!manifest.functions) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ export class PlugSpacePrimitives implements SpacePrimitives {
|
||||||
constructor(
|
constructor(
|
||||||
private wrapped: SpacePrimitives,
|
private wrapped: SpacePrimitives,
|
||||||
private hook: PageNamespaceHook,
|
private hook: PageNamespaceHook,
|
||||||
|
private env: string,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
performOperation(
|
performOperation(
|
||||||
|
@ -19,8 +20,13 @@ export class PlugSpacePrimitives implements SpacePrimitives {
|
||||||
pageName: string,
|
pageName: string,
|
||||||
...args: any[]
|
...args: any[]
|
||||||
): Promise<any> | false {
|
): Promise<any> | false {
|
||||||
for (const { operation, pattern, plug, name } of this.hook.spaceFunctions) {
|
for (
|
||||||
if (operation === type && pageName.match(pattern)) {
|
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]);
|
return plug.invoke(name, [pageName, ...args]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,6 +102,7 @@ export class HttpServer {
|
||||||
new PlugSpacePrimitives(
|
new PlugSpacePrimitives(
|
||||||
new DiskSpacePrimitives(options.pagesPath),
|
new DiskSpacePrimitives(options.pagesPath),
|
||||||
namespaceHook,
|
namespaceHook,
|
||||||
|
"server",
|
||||||
),
|
),
|
||||||
this.eventHook,
|
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 { parseYamlSettings, safeRun } from "../common/util.ts";
|
||||||
import { Space } from "../common/spaces/space.ts";
|
import { Space } from "../common/spaces/space.ts";
|
||||||
import { HttpSpacePrimitives } from "../common/spaces/http_space_primitives.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 () => {
|
safeRun(async () => {
|
||||||
let password: string | undefined = localStorage.getItem("password") ||
|
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();
|
serverSpace.watch();
|
||||||
|
|
||||||
console.log("Booting...");
|
console.log("Booting...");
|
||||||
|
@ -36,6 +54,7 @@ safeRun(async () => {
|
||||||
|
|
||||||
const editor = new Editor(
|
const editor = new Editor(
|
||||||
serverSpace,
|
serverSpace,
|
||||||
|
system,
|
||||||
document.getElementById("sb-root")!,
|
document.getElementById("sb-root")!,
|
||||||
"",
|
"",
|
||||||
settings.indexPage || "index",
|
settings.indexPage || "index",
|
||||||
|
@ -46,10 +65,9 @@ safeRun(async () => {
|
||||||
await editor.init();
|
await editor.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
// if (localStorage.getItem("disable_sw") !== "true") {
|
|
||||||
if (navigator.serviceWorker) {
|
if (navigator.serviceWorker) {
|
||||||
navigator.serviceWorker
|
navigator.serviceWorker
|
||||||
.register(new URL("service_worker.js", location.href), {
|
.register(new URL("/service_worker.js", location.href), {
|
||||||
type: "module",
|
type: "module",
|
||||||
})
|
})
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
|
@ -60,6 +78,3 @@ if (navigator.serviceWorker) {
|
||||||
"No launching service worker (not present, maybe because not running on localhost or over SSL)",
|
"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}
|
value={pageName}
|
||||||
className="sb-edit-page-name"
|
className="sb-edit-page-name"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
console.log("Key press", e);
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
|
@ -131,7 +131,7 @@ export class Editor {
|
||||||
.dispatchEvent("editor:updated")
|
.dispatchEvent("editor:updated")
|
||||||
.catch((e) => console.error("Error dispatching editor:updated event", e));
|
.catch((e) => console.error("Error dispatching editor:updated event", e));
|
||||||
}, 1000);
|
}, 1000);
|
||||||
private system = new System<SilverBulletHooks>("client");
|
private system: System<SilverBulletHooks>;
|
||||||
private mdExtensions: MDExt[] = [];
|
private mdExtensions: MDExt[] = [];
|
||||||
urlPrefix: string;
|
urlPrefix: string;
|
||||||
indexPage: string;
|
indexPage: string;
|
||||||
|
@ -139,11 +139,13 @@ export class Editor {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
space: Space,
|
space: Space,
|
||||||
|
system: System<SilverBulletHooks>,
|
||||||
parent: Element,
|
parent: Element,
|
||||||
urlPrefix: string,
|
urlPrefix: string,
|
||||||
indexPage: string,
|
indexPage: string,
|
||||||
) {
|
) {
|
||||||
this.space = space;
|
this.space = space;
|
||||||
|
this.system = system;
|
||||||
this.urlPrefix = urlPrefix;
|
this.urlPrefix = urlPrefix;
|
||||||
this.viewState = initialViewState;
|
this.viewState = initialViewState;
|
||||||
this.viewDispatch = () => {};
|
this.viewDispatch = () => {};
|
||||||
|
@ -223,6 +225,40 @@ export class Editor {
|
||||||
async init() {
|
async init() {
|
||||||
this.focus();
|
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) => {
|
this.pageNavigator.subscribe(async (pageName, pos: number | string) => {
|
||||||
console.log("Now navigating to", pageName);
|
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");
|
await this.dispatchAppEvent("editor:init");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue