SilverBullet pivot to become an offline-first PWA (#403)
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
// TODO: Figure out how to keep this up-to-date automatically
|
||||
export const builtinPlugNames = [
|
||||
"collab",
|
||||
"core",
|
||||
"directive",
|
||||
"emoji",
|
||||
"markdown",
|
||||
"share",
|
||||
"tasks",
|
||||
"search",
|
||||
];
|
||||
@@ -1,6 +1,4 @@
|
||||
name: collab
|
||||
imports:
|
||||
- https://get.silverbullet.md/global.plug.json
|
||||
functions:
|
||||
detectCollabPage:
|
||||
path: "./collab.ts:detectPage"
|
||||
@@ -23,19 +21,16 @@ functions:
|
||||
# Space extension
|
||||
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
|
||||
|
||||
+11
-21
@@ -9,26 +9,17 @@ import {
|
||||
extractFrontmatter,
|
||||
prepareFrontmatterDispatch,
|
||||
} from "$sb/lib/frontmatter.ts";
|
||||
import * as YAML from "yaml";
|
||||
import {
|
||||
clientStore,
|
||||
collab,
|
||||
editor,
|
||||
markdown,
|
||||
} from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { store, YAML } from "$sb/plugos-syscall/mod.ts";
|
||||
import { collab, editor, markdown } 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");
|
||||
let username = await store.get("collabUsername");
|
||||
if (!username) {
|
||||
username = await editor.prompt(
|
||||
"Please enter a publicly visible user name (or cancel for 'anonymous'):",
|
||||
@@ -36,7 +27,7 @@ async function ensureUsername(): Promise<string> {
|
||||
if (!username) {
|
||||
return "anonymous";
|
||||
} else {
|
||||
await clientStore.set("collabUsername", username);
|
||||
await store.set("collabUsername", username);
|
||||
}
|
||||
}
|
||||
return username;
|
||||
@@ -67,7 +58,7 @@ export async function shareCommand() {
|
||||
await editor.save();
|
||||
const text = await editor.getText();
|
||||
const tree = await markdown.parseMarkdown(text);
|
||||
let { $share } = extractFrontmatter(tree);
|
||||
let { $share } = await extractFrontmatter(tree);
|
||||
if (!$share) {
|
||||
$share = [];
|
||||
}
|
||||
@@ -76,7 +67,7 @@ export async function shareCommand() {
|
||||
}
|
||||
|
||||
removeParentPointers(tree);
|
||||
const dispatchData = prepareFrontmatterDispatch(tree, {
|
||||
const dispatchData = await prepareFrontmatterDispatch(tree, {
|
||||
$share: [...$share, `collab:${serverUrl}/${roomId}`],
|
||||
});
|
||||
|
||||
@@ -95,7 +86,7 @@ export async function detectPage() {
|
||||
if (frontMatter) {
|
||||
const yamlText = renderToText(frontMatter.children![1].children![0]);
|
||||
try {
|
||||
let { $share } = YAML.parse(yamlText) as any;
|
||||
let { $share } = await YAML.parse(yamlText) as any;
|
||||
if (!$share) {
|
||||
return;
|
||||
}
|
||||
@@ -125,19 +116,18 @@ export function shareNoop() {
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function readFileCollab(
|
||||
export function readFileCollab(
|
||||
name: string,
|
||||
encoding: FileEncoding,
|
||||
): Promise<{ data: FileData; meta: FileMeta }> {
|
||||
): { data: string; meta: FileMeta } {
|
||||
if (!name.endsWith(".md")) {
|
||||
throw new Error("File not found");
|
||||
throw new Error("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 "utf8" or "dataurl"
|
||||
data: encoding === "utf8" ? text : base64EncodedDataUrl(
|
||||
data: base64EncodedDataUrl(
|
||||
"text/markdown",
|
||||
new TextEncoder().encode(text),
|
||||
),
|
||||
|
||||
+9
-7
@@ -1,7 +1,3 @@
|
||||
import type {
|
||||
FileData,
|
||||
FileEncoding,
|
||||
} from "../../common/spaces/space_primitives.ts";
|
||||
import { renderToText, replaceNodesMatching } from "$sb/lib/tree.ts";
|
||||
import type { FileMeta } from "../../common/types.ts";
|
||||
import { parseMarkdown } from "$sb/silverbullet-syscall/markdown.ts";
|
||||
@@ -11,8 +7,7 @@ export const cloudPrefix = "💭 ";
|
||||
|
||||
export async function readFileCloud(
|
||||
name: string,
|
||||
encoding: FileEncoding,
|
||||
): Promise<{ data: FileData; meta: FileMeta } | undefined> {
|
||||
): Promise<{ data: string; meta: FileMeta } | undefined> {
|
||||
const originalUrl = name.substring(
|
||||
cloudPrefix.length,
|
||||
name.length - ".md".length,
|
||||
@@ -43,7 +38,7 @@ export async function readFileCloud(
|
||||
`${cloudPrefix}${originalUrl.split("/")[0]}/`,
|
||||
);
|
||||
return {
|
||||
data: encoding === "utf8" ? text : base64EncodedDataUrl(
|
||||
data: base64EncodedDataUrl(
|
||||
"text/markdown",
|
||||
new TextEncoder().encode(text),
|
||||
),
|
||||
@@ -57,6 +52,13 @@ export async function readFileCloud(
|
||||
};
|
||||
}
|
||||
|
||||
export function writeFileCloud(
|
||||
name: string,
|
||||
): Promise<FileMeta> {
|
||||
console.log("Writing cloud file", name);
|
||||
return getFileMetaCloud(name);
|
||||
}
|
||||
|
||||
async function translateLinksWithPrefix(
|
||||
text: string,
|
||||
prefix: string,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name: core
|
||||
imports:
|
||||
- https://get.silverbullet.md/global.plug.json
|
||||
requiredPermissions:
|
||||
- fetch
|
||||
syntax:
|
||||
Hashtag:
|
||||
firstCharacters:
|
||||
@@ -26,10 +26,8 @@ functions:
|
||||
path: "./editor.ts:toggleDarkMode"
|
||||
command:
|
||||
name: "Editor: Toggle Dark Mode"
|
||||
|
||||
clearPageIndex:
|
||||
path: "./page.ts:clearPageIndex"
|
||||
env: server
|
||||
events:
|
||||
- page:saved
|
||||
- page:deleted
|
||||
@@ -45,9 +43,6 @@ functions:
|
||||
path: "./page.ts:reindexCommand"
|
||||
command:
|
||||
name: "Space: Reindex"
|
||||
reindexSpace:
|
||||
path: "./page.ts:reindexSpace"
|
||||
env: server
|
||||
deletePage:
|
||||
path: "./page.ts:deletePage"
|
||||
command:
|
||||
@@ -138,40 +133,7 @@ functions:
|
||||
path: "./anchor.ts:anchorComplete"
|
||||
events:
|
||||
- editor:complete
|
||||
|
||||
# Full text search
|
||||
searchIndex:
|
||||
path: ./search.ts:pageIndex
|
||||
events:
|
||||
- page:index
|
||||
searchUnindex:
|
||||
path: "./search.ts:pageUnindex"
|
||||
env: server
|
||||
events:
|
||||
- page:deleted
|
||||
searchQueryProvider:
|
||||
path: ./search.ts:queryProvider
|
||||
events:
|
||||
- query:full-text
|
||||
searchCommand:
|
||||
path: ./search.ts:searchCommand
|
||||
command:
|
||||
name: "Search Space"
|
||||
key: Ctrl-Shift-f
|
||||
mac: Cmd-Shift-f
|
||||
readPageSearch:
|
||||
path: ./search.ts:readFileSearch
|
||||
env: server
|
||||
pageNamespace:
|
||||
pattern: "🔍 .+"
|
||||
operation: readFile
|
||||
getPageMetaSearch:
|
||||
path: ./search.ts:getFileMetaSearch
|
||||
env: server
|
||||
pageNamespace:
|
||||
pattern: "🔍 .+"
|
||||
operation: getFileMeta
|
||||
|
||||
|
||||
# Template commands
|
||||
insertTemplateText:
|
||||
path: "./template.ts:insertTemplateText"
|
||||
@@ -342,9 +304,6 @@ functions:
|
||||
name: "Plugs: Update"
|
||||
key: "Ctrl-Shift-p"
|
||||
mac: "Cmd-Shift-p"
|
||||
updatePlugs:
|
||||
path: ./plugmanager.ts:updatePlugs
|
||||
env: server
|
||||
getPlugHTTPS:
|
||||
path: "./plugmanager.ts:getPlugHTTPS"
|
||||
events:
|
||||
@@ -367,20 +326,6 @@ functions:
|
||||
path: ./debug.ts:parsePageCommand
|
||||
command:
|
||||
name: "Debug: Parse Document"
|
||||
showLogsCommand:
|
||||
path: ./debug.ts:showLogsCommand
|
||||
command:
|
||||
name: "Show Logs"
|
||||
key: "Ctrl-Alt-l"
|
||||
mac: "Cmd-Alt-l"
|
||||
hideBhsCommand:
|
||||
path: ./debug.ts:hideBhsCommand
|
||||
command:
|
||||
name: "UI: Hide BHS"
|
||||
key: "Ctrl-Alt-b"
|
||||
mac: "Cmd-Alt-b"
|
||||
events:
|
||||
- log:hide
|
||||
|
||||
# Link unfurl infrastructure
|
||||
unfurlLink:
|
||||
@@ -391,9 +336,6 @@ functions:
|
||||
mac: "Cmd-Shift-u"
|
||||
contexts:
|
||||
- NakedURL
|
||||
unfurlExec:
|
||||
env: server
|
||||
path: ./link.ts:unfurlExec
|
||||
|
||||
# Title-based link unfurl
|
||||
titleUnfurlOptions:
|
||||
@@ -418,13 +360,16 @@ functions:
|
||||
# Cloud pages
|
||||
readPageCloud:
|
||||
path: ./cloud.ts:readFileCloud
|
||||
env: server
|
||||
pageNamespace:
|
||||
pattern: "💭 .+"
|
||||
operation: readFile
|
||||
writePageCloud:
|
||||
path: ./cloud.ts:writeFileCloud
|
||||
pageNamespace:
|
||||
pattern: "💭 .+"
|
||||
operation: writeFile
|
||||
getPageMetaCloud:
|
||||
path: ./cloud.ts:getFileMetaCloud
|
||||
env: server
|
||||
pageNamespace:
|
||||
pattern: "💭 .+"
|
||||
operation: getFileMeta
|
||||
|
||||
@@ -10,83 +10,3 @@ export async function parsePageCommand() {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export async function showLogsCommand() {
|
||||
await editor.showPanel(
|
||||
"bhs",
|
||||
1,
|
||||
`
|
||||
<style>
|
||||
#close {
|
||||
width: 100%;
|
||||
}
|
||||
#client-log-header {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 35px;
|
||||
}
|
||||
#server-log-header {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 35px;
|
||||
width: 50%;
|
||||
}
|
||||
#client-log {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 60px;
|
||||
bottom: 0;
|
||||
width: 50%;
|
||||
overflow: scroll;
|
||||
}
|
||||
#server-log {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 60px;
|
||||
bottom: 0;
|
||||
width: 50%;
|
||||
overflow: scroll;
|
||||
}
|
||||
</style>
|
||||
<button onclick="self.close()" id="close">Close</button>
|
||||
<div id="client-log-header">Client logs (max 100)</div>
|
||||
<div id="client-log">Loading...</div>
|
||||
<div id="server-log-header">Server logs (max 100)</div>
|
||||
<div id="server-log">Loading...</div>`,
|
||||
`
|
||||
const clientDiv = document.getElementById("client-log");
|
||||
clientDiv.scrollTop = clientDiv.scrollHeight;
|
||||
const serverDiv = document.getElementById("server-log");
|
||||
serverDiv.scrollTop = serverDiv.scrollHeight;
|
||||
|
||||
self.close = () => {
|
||||
syscall("event.dispatch", "log:hide");
|
||||
};
|
||||
|
||||
syscall("system.getEnv").then((env) => {
|
||||
const clientServerMode = !!env;
|
||||
if (!clientServerMode) {
|
||||
// Running in hybrid mode (mobile), so let's ignore server logs (they're the same as client logs)
|
||||
serverDiv.style.display = "none";
|
||||
clientDiv.style.width = "100%";
|
||||
document.getElementById("server-log-header").style.display = "none";
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
Promise.resolve().then(async () => {
|
||||
if(clientServerMode) {
|
||||
const serverLogs = await syscall("sandbox.getServerLogs");
|
||||
serverDiv.innerHTML = "<pre>" + serverLogs.map((le) => "[" + le.level + "] " + le.message).join("\\n") + "</pre>";
|
||||
}
|
||||
const clientLogs = await syscall("sandbox.getLogs");
|
||||
clientDiv.innerHTML = "<pre>" + clientLogs.map((le) => "[" + le.level + "] " + le.message).join("\\n") + "</pre>";
|
||||
}).catch(console.error);
|
||||
}, 1000);
|
||||
});
|
||||
`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function hideBhsCommand() {
|
||||
await editor.hidePanel("bhs");
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { clientStore, editor } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { editor } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { store } from "$sb/plugos-syscall/mod.ts";
|
||||
|
||||
// Run on "editor:init"
|
||||
export async function setEditorMode() {
|
||||
if (await clientStore.get("vimMode")) {
|
||||
if (await store.get("vimMode")) {
|
||||
await editor.setUiOption("vimMode", true);
|
||||
}
|
||||
if (await clientStore.get("darkMode")) {
|
||||
if (await store.get("darkMode")) {
|
||||
await editor.setUiOption("darkMode", true);
|
||||
}
|
||||
}
|
||||
|
||||
export async function toggleDarkMode() {
|
||||
let darkMode = await clientStore.get("darkMode");
|
||||
let darkMode = await store.get("darkMode");
|
||||
darkMode = !darkMode;
|
||||
await editor.setUiOption("darkMode", darkMode);
|
||||
await clientStore.set("darkMode", darkMode);
|
||||
await store.set("darkMode", darkMode);
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
import * as YAML from "yaml";
|
||||
import { YAML } from "$sb/plugos-syscall/mod.ts";
|
||||
import type { WidgetContent } from "$sb/app_event.ts";
|
||||
|
||||
type EmbedConfig = {
|
||||
@@ -20,11 +20,11 @@ function extractYoutubeVideoId(url: string) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export function embedWidget(
|
||||
export async function embedWidget(
|
||||
bodyText: string,
|
||||
): WidgetContent {
|
||||
): Promise<WidgetContent> {
|
||||
try {
|
||||
const data: EmbedConfig = YAML.parse(bodyText) as any;
|
||||
const data: EmbedConfig = await YAML.parse(bodyText) as any;
|
||||
let url = data.url;
|
||||
const youtubeVideoId = extractYoutubeVideoId(url);
|
||||
if (youtubeVideoId) {
|
||||
|
||||
+7
-16
@@ -1,5 +1,5 @@
|
||||
import { nodeAtPos } from "$sb/lib/tree.ts";
|
||||
import { editor, markdown, system } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { editor, markdown } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { events } from "$sb/plugos-syscall/mod.ts";
|
||||
|
||||
type UnfurlOption = {
|
||||
@@ -26,16 +26,17 @@ export async function unfurlCommand() {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const replacement = await system.invokeFunction(
|
||||
"server",
|
||||
"unfurlExec",
|
||||
selectedUnfurl.id,
|
||||
const replacement = await events.dispatchEvent(
|
||||
`unfurl:${selectedUnfurl.id}`,
|
||||
url,
|
||||
);
|
||||
if (replacement.length === 0) {
|
||||
throw new Error("Unfurl failed");
|
||||
}
|
||||
await editor.replaceRange(
|
||||
nakedUrlNode?.from!,
|
||||
nakedUrlNode?.to!,
|
||||
replacement,
|
||||
replacement[0],
|
||||
);
|
||||
} catch (e: any) {
|
||||
await editor.flashNotification(e.message, "error");
|
||||
@@ -51,16 +52,6 @@ export function titleUnfurlOptions(): UnfurlOption[] {
|
||||
];
|
||||
}
|
||||
|
||||
// Run on the server because plugs will likely rely on fetch for this
|
||||
export async function unfurlExec(id: string, url: string): Promise<string> {
|
||||
const replacement = await events.dispatchEvent(`unfurl:${id}`, url);
|
||||
if (replacement.length === 0) {
|
||||
throw new Error("Unfurl failed");
|
||||
} else {
|
||||
return replacement[0];
|
||||
}
|
||||
}
|
||||
|
||||
const titleRegex = /<title[^>]*>\s*([^<]+)\s*<\/title\s*>/i;
|
||||
|
||||
export async function titleUnfurl(url: string): Promise<string> {
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import type { ClickEvent } from "$sb/app_event.ts";
|
||||
import {
|
||||
editor,
|
||||
markdown,
|
||||
space,
|
||||
system,
|
||||
} from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { editor, markdown, system } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import {
|
||||
addParentPointers,
|
||||
findNodeOfType,
|
||||
@@ -82,9 +77,10 @@ async function actionClickOrActionEnter(
|
||||
}
|
||||
if (url.indexOf("://") === -1 && !url.startsWith("mailto:")) {
|
||||
url = decodeURIComponent(url);
|
||||
// attachment URL, let's fetch as a data url
|
||||
const dataUrl = await space.readAttachment(url);
|
||||
return editor.downloadFile(url, dataUrl);
|
||||
// // attachment URL, let's fetch as a data url
|
||||
// const dataUrl = await space.readAttachment(url);
|
||||
// return editor.downloadFile(url, dataUrl);
|
||||
return editor.openUrl(`/.fs/${url}`);
|
||||
} else {
|
||||
await editor.openUrl(url);
|
||||
}
|
||||
|
||||
+10
-6
@@ -23,6 +23,7 @@ import {
|
||||
} from "$sb/lib/tree.ts";
|
||||
import { applyQuery } from "$sb/lib/query.ts";
|
||||
import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
|
||||
import { invokeFunction } from "../../plug-api/silverbullet-syscall/system.ts";
|
||||
|
||||
// Key space:
|
||||
// pl:toPage:pos => pageName
|
||||
@@ -31,8 +32,8 @@ import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
|
||||
export async function indexLinks({ name, tree }: IndexTreeEvent) {
|
||||
const backLinks: { key: string; value: string }[] = [];
|
||||
// [[Style Links]]
|
||||
// console.log("Now indexing", name);
|
||||
const pageMeta = extractFrontmatter(tree);
|
||||
// console.log("Now indexing links for", name);
|
||||
const pageMeta = await extractFrontmatter(tree);
|
||||
if (Object.keys(pageMeta).length > 0) {
|
||||
// console.log("Extracted page meta data", pageMeta);
|
||||
// Don't index meta data starting with $
|
||||
@@ -44,6 +45,8 @@ export async function indexLinks({ name, tree }: IndexTreeEvent) {
|
||||
await index.set(name, "meta:", pageMeta);
|
||||
}
|
||||
|
||||
// throw new Error("Boom");
|
||||
|
||||
collectNodesMatching(tree, (n) => n.type === "WikiLinkPage").forEach((n) => {
|
||||
let toPage = n.children![0].text!;
|
||||
if (toPage.includes("@")) {
|
||||
@@ -118,7 +121,7 @@ export async function renamePage(cmdDef: any) {
|
||||
`Page ${newName} already exists, cannot rename to existing page.`,
|
||||
);
|
||||
} catch (e: any) {
|
||||
if (e.message.includes("not found")) {
|
||||
if (e.message === "Not found") {
|
||||
// Expected not found error, so we can continue
|
||||
} else {
|
||||
await editor.flashNotification(e.message, "error");
|
||||
@@ -224,7 +227,7 @@ async function getBackLinks(pageName: string): Promise<BackLink[]> {
|
||||
|
||||
export async function reindexCommand() {
|
||||
await editor.flashNotification("Reindexing...");
|
||||
await system.invokeFunction("server", "reindexSpace");
|
||||
await reindexSpace();
|
||||
await editor.flashNotification("Reindexing done");
|
||||
}
|
||||
|
||||
@@ -245,10 +248,11 @@ export async function pageComplete(completeEvent: CompleteEvent) {
|
||||
};
|
||||
}
|
||||
|
||||
// Server functions
|
||||
export async function reindexSpace() {
|
||||
console.log("Clearing page index...");
|
||||
await index.clearPageIndex();
|
||||
// Executed this way to not have to embed the search plug code here
|
||||
await invokeFunction("client", "search.clearIndex");
|
||||
console.log("Listing all pages");
|
||||
const pages = await space.listPages();
|
||||
let counter = 0;
|
||||
@@ -272,7 +276,7 @@ export async function clearPageIndex(page: string) {
|
||||
}
|
||||
|
||||
export async function parseIndexTextRepublish({ name, text }: IndexEvent) {
|
||||
console.log("Reindexing", name);
|
||||
// console.log("Reindexing", name);
|
||||
await events.dispatchEvent("page:index", {
|
||||
name,
|
||||
tree: await markdown.parseMarkdown(text),
|
||||
|
||||
+71
-62
@@ -2,6 +2,7 @@ import { events } from "$sb/plugos-syscall/mod.ts";
|
||||
import type { Manifest } from "../../common/manifest.ts";
|
||||
import { editor, space, system } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { readYamlPage } from "$sb/lib/yaml_page.ts";
|
||||
import { builtinPlugNames } from "../builtin_plugs.ts";
|
||||
|
||||
const plugsPrelude =
|
||||
"This file lists all plugs that SilverBullet will load. Run the {[Plugs: Update]} command to update and reload this list of plugs.\n\n";
|
||||
@@ -10,9 +11,69 @@ export async function updatePlugsCommand() {
|
||||
await editor.save();
|
||||
await editor.flashNotification("Updating plugs...");
|
||||
try {
|
||||
await system.invokeFunction("server", "updatePlugs");
|
||||
let plugList: string[] = [];
|
||||
try {
|
||||
const plugListRead: any[] = await readYamlPage("PLUGS");
|
||||
plugList = plugListRead.filter((plug) => typeof plug === "string");
|
||||
if (plugList.length !== plugListRead.length) {
|
||||
throw new Error(
|
||||
`Some of the plugs were not in a yaml list format, they were ignored`,
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e.message.includes("Could not read file")) {
|
||||
console.warn("No PLUGS page found, not loading anything");
|
||||
return;
|
||||
}
|
||||
throw new Error(`Error processing PLUGS: ${e.message}`);
|
||||
}
|
||||
console.log("Plug YAML", plugList);
|
||||
const allCustomPlugNames: string[] = [];
|
||||
for (const plugUri of plugList) {
|
||||
const [protocol, ...rest] = plugUri.split(":");
|
||||
|
||||
const plugNameMatch = /\/([^\/]+)\.plug\.js$/.exec(plugUri);
|
||||
if (!plugNameMatch) {
|
||||
console.error(
|
||||
"Could not extract plug name from ",
|
||||
plugUri,
|
||||
"ignoring...",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const plugName = plugNameMatch[1];
|
||||
|
||||
const manifests = await events.dispatchEvent(
|
||||
`get-plug:${protocol}`,
|
||||
rest.join(":"),
|
||||
);
|
||||
if (manifests.length === 0) {
|
||||
console.error("Could not resolve plug", plugUri);
|
||||
}
|
||||
// console.log("Got manifests", plugUri, protocol, manifests);
|
||||
const workerCode = manifests[0] as string;
|
||||
allCustomPlugNames.push(plugName);
|
||||
// console.log("Writing", `_plug/${plugName}.plug.js`, workerCode);
|
||||
await space.writeAttachment(
|
||||
`_plug/${plugName}.plug.js`,
|
||||
"utf8",
|
||||
workerCode,
|
||||
);
|
||||
}
|
||||
|
||||
const allPlugNames = [...builtinPlugNames, ...allCustomPlugNames];
|
||||
// And delete extra ones
|
||||
for (const existingPlug of await space.listPlugs()) {
|
||||
const plugName = existingPlug.substring(
|
||||
"_plug/".length,
|
||||
existingPlug.length - ".plug.js".length,
|
||||
);
|
||||
if (!allPlugNames.includes(plugName)) {
|
||||
await space.deleteAttachment(existingPlug);
|
||||
}
|
||||
}
|
||||
await editor.flashNotification("And... done!");
|
||||
system.reloadPlugs();
|
||||
} catch (e: any) {
|
||||
editor.flashNotification("Error updating plugs: " + e.message, "error");
|
||||
}
|
||||
@@ -45,74 +106,22 @@ export async function addPlugCommand() {
|
||||
"\n```",
|
||||
);
|
||||
await editor.navigate("PLUGS");
|
||||
await system.invokeFunction("server", "updatePlugs");
|
||||
await updatePlugsCommand();
|
||||
await editor.flashNotification("Plug added!");
|
||||
system.reloadPlugs();
|
||||
}
|
||||
|
||||
export async function updatePlugs() {
|
||||
let plugList: string[] = [];
|
||||
try {
|
||||
const plugListRead: any[] = await readYamlPage("PLUGS");
|
||||
plugList = plugListRead.filter((plug) => typeof plug === "string");
|
||||
if (plugList.length !== plugListRead.length) {
|
||||
throw new Error(
|
||||
`Some of the plugs were not in a yaml list format, they were ignored`,
|
||||
);
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e.message.includes("Could not read file")) {
|
||||
console.warn("No PLUGS page found, not loading anything");
|
||||
return;
|
||||
}
|
||||
throw new Error(`Error processing PLUGS: ${e.message}`);
|
||||
}
|
||||
console.log("Plug YAML", plugList);
|
||||
const allPlugNames: string[] = [];
|
||||
for (const plugUri of plugList) {
|
||||
const [protocol, ...rest] = plugUri.split(":");
|
||||
const manifests = await events.dispatchEvent(
|
||||
`get-plug:${protocol}`,
|
||||
rest.join(":"),
|
||||
);
|
||||
if (manifests.length === 0) {
|
||||
console.error("Could not resolve plug", plugUri);
|
||||
}
|
||||
// console.log("Got manifests", plugUri, protocol, manifests);
|
||||
const manifest = manifests[0];
|
||||
allPlugNames.push(manifest.name);
|
||||
// console.log("Writing", `_plug/${manifest.name}`);
|
||||
await space.writeAttachment(
|
||||
`_plug/${manifest.name}.plug.json`,
|
||||
"utf8",
|
||||
JSON.stringify(manifest),
|
||||
);
|
||||
}
|
||||
|
||||
// And delete extra ones
|
||||
for (const existingPlug of await space.listPlugs()) {
|
||||
const plugName = existingPlug.substring(
|
||||
"_plug/".length,
|
||||
existingPlug.length - ".plug.json".length,
|
||||
);
|
||||
if (!allPlugNames.includes(plugName)) {
|
||||
await space.deleteAttachment(existingPlug);
|
||||
}
|
||||
}
|
||||
system.reloadPlugs();
|
||||
}
|
||||
|
||||
export async function getPlugHTTPS(url: string): Promise<Manifest> {
|
||||
export async function getPlugHTTPS(url: string): Promise<string> {
|
||||
const fullUrl = `https:${url}`;
|
||||
console.log("Now fetching plug manifest from", fullUrl);
|
||||
console.log("Now fetching plug code from", fullUrl);
|
||||
const req = await fetch(fullUrl);
|
||||
if (req.status !== 200) {
|
||||
throw new Error(`Could not fetch plug manifest from ${fullUrl}`);
|
||||
throw new Error(`Could not fetch plug code from ${fullUrl}`);
|
||||
}
|
||||
return req.json();
|
||||
return req.text();
|
||||
}
|
||||
|
||||
export function getPlugGithub(identifier: string): Promise<Manifest> {
|
||||
export function getPlugGithub(identifier: string): Promise<string> {
|
||||
const [owner, repo, path] = identifier.split("/");
|
||||
let [repoClean, branch] = repo.split("@");
|
||||
if (!branch) {
|
||||
@@ -125,7 +134,7 @@ export function getPlugGithub(identifier: string): Promise<Manifest> {
|
||||
|
||||
export async function getPlugGithubRelease(
|
||||
identifier: string,
|
||||
): Promise<Manifest> {
|
||||
): Promise<string> {
|
||||
let [owner, repo, version] = identifier.split("/");
|
||||
if (!version || version === "latest") {
|
||||
console.log("fetching the latest version");
|
||||
@@ -141,6 +150,6 @@ export async function getPlugGithubRelease(
|
||||
version = result.name;
|
||||
}
|
||||
const finalUrl =
|
||||
`//github.com/${owner}/${repo}/releases/download/${version}/${repo}.plug.json`;
|
||||
`//github.com/${owner}/${repo}/releases/download/${version}/${repo}.plug.js`;
|
||||
return getPlugHTTPS(finalUrl);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export async function extractToPage() {
|
||||
`Page ${newName} already exists, cannot rename to existing page.`,
|
||||
);
|
||||
} catch (e: any) {
|
||||
if (e.message.includes("not found")) {
|
||||
if (e.message === "Not found") {
|
||||
// Expected not found error, so we can continue
|
||||
} else {
|
||||
await editor.flashNotification(e.message, "error");
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import { extractFrontmatter } from "../../plug-api/lib/frontmatter.ts";
|
||||
export async function indexTags({ name, tree }: IndexTreeEvent) {
|
||||
removeQueries(tree);
|
||||
const allTags = new Set<string>();
|
||||
const { tags } = extractFrontmatter(tree);
|
||||
const { tags } = await extractFrontmatter(tree);
|
||||
if (Array.isArray(tags)) {
|
||||
tags.forEach((t) => allTags.add(t));
|
||||
}
|
||||
|
||||
+14
-17
@@ -31,7 +31,7 @@ export async function instantiateTemplateCommand() {
|
||||
);
|
||||
|
||||
const parseTree = await markdown.parseMarkdown(text);
|
||||
const additionalPageMeta = extractFrontmatter(parseTree, [
|
||||
const additionalPageMeta = await extractFrontmatter(parseTree, [
|
||||
"$name",
|
||||
"$disableDirectives",
|
||||
]);
|
||||
@@ -157,28 +157,25 @@ export async function dailyNoteCommand() {
|
||||
dailyNoteTemplate: "template/page/Daily Note",
|
||||
dailyNotePrefix: "📅 ",
|
||||
});
|
||||
let dailyNoteTemplateText = "";
|
||||
try {
|
||||
dailyNoteTemplateText = await space.readPage(dailyNoteTemplate);
|
||||
} catch {
|
||||
console.warn(`No daily note template found at ${dailyNoteTemplate}`);
|
||||
}
|
||||
const date = niceDate(new Date());
|
||||
const pageName = `${dailyNotePrefix}${date}`;
|
||||
if (dailyNoteTemplateText) {
|
||||
|
||||
try {
|
||||
await space.getPageMeta(pageName);
|
||||
} catch {
|
||||
// Doesn't exist, let's create
|
||||
let dailyNoteTemplateText = "";
|
||||
try {
|
||||
await space.getPageMeta(pageName);
|
||||
dailyNoteTemplateText = await space.readPage(dailyNoteTemplate);
|
||||
} catch {
|
||||
// Doesn't exist, let's create
|
||||
await space.writePage(
|
||||
pageName,
|
||||
replaceTemplateVars(dailyNoteTemplateText, pageName),
|
||||
);
|
||||
console.warn(`No daily note template found at ${dailyNoteTemplate}`);
|
||||
}
|
||||
await editor.navigate(pageName);
|
||||
} else {
|
||||
await editor.navigate(pageName);
|
||||
await space.writePage(
|
||||
pageName,
|
||||
replaceTemplateVars(dailyNoteTemplateText, pageName),
|
||||
);
|
||||
}
|
||||
await editor.navigate(pageName);
|
||||
}
|
||||
|
||||
function getWeekStartDate(monday = false) {
|
||||
|
||||
+4
-3
@@ -1,11 +1,12 @@
|
||||
import { readCodeBlockPage } from "../../plug-api/lib/yaml_page.ts";
|
||||
import { clientStore, editor } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { editor } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { store } from "$sb/plugos-syscall/mod.ts";
|
||||
|
||||
export async function toggleVimMode() {
|
||||
let vimMode = await clientStore.get("vimMode");
|
||||
let vimMode = await store.get("vimMode");
|
||||
vimMode = !vimMode;
|
||||
await editor.setUiOption("vimMode", vimMode);
|
||||
await clientStore.set("vimMode", vimMode);
|
||||
await store.set("vimMode", vimMode);
|
||||
}
|
||||
|
||||
export async function loadVimRc() {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { editor, markdown, system } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { editor, markdown, sync } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import {
|
||||
ParseTree,
|
||||
removeParentPointers,
|
||||
renderToText,
|
||||
traverseTree,
|
||||
@@ -14,12 +13,17 @@ export async function updateDirectivesOnPageCommand(arg: any) {
|
||||
const pageName = await editor.getCurrentPage();
|
||||
const text = await editor.getText();
|
||||
const tree = await markdown.parseMarkdown(text);
|
||||
const metaData = extractFrontmatter(tree, ["$disableDirectives"]);
|
||||
const metaData = await extractFrontmatter(tree, ["$disableDirectives"]);
|
||||
if (metaData.$disableDirectives) {
|
||||
// Not updating, directives disabled
|
||||
return;
|
||||
}
|
||||
|
||||
// if (!(await sync.hasInitialSyncCompleted())) {
|
||||
// console.info("Initial sync hasn't completed yet, not updating directives.");
|
||||
// return;
|
||||
// }
|
||||
|
||||
// If this page is shared ($share) via collab: disable directives as well
|
||||
// due to security concerns
|
||||
if (metaData.$share) {
|
||||
@@ -51,12 +55,7 @@ export async function updateDirectivesOnPageCommand(arg: any) {
|
||||
}
|
||||
const fullMatch = text.substring(tree.from!, tree.to!);
|
||||
try {
|
||||
const promise = system.invokeFunction(
|
||||
"server",
|
||||
"serverRenderDirective",
|
||||
pageName,
|
||||
tree,
|
||||
);
|
||||
const promise = renderDirectives(pageName, tree);
|
||||
replacements.push({
|
||||
textPromise: promise,
|
||||
fullMatch,
|
||||
@@ -116,17 +115,8 @@ export async function updateDirectivesOnPageCommand(arg: any) {
|
||||
}
|
||||
}
|
||||
|
||||
// Called from client, running on server
|
||||
// The text passed here is going to be a single directive block (not a full page)
|
||||
export function serverRenderDirective(
|
||||
pageName: string,
|
||||
tree: ParseTree,
|
||||
): Promise<string> {
|
||||
return renderDirectives(pageName, tree);
|
||||
}
|
||||
|
||||
// Pure server driven implementation of directive updating
|
||||
export async function serverUpdateDirectives(
|
||||
export async function updateDirectives(
|
||||
pageName: string,
|
||||
text: string,
|
||||
) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { events } from "$sb/plugos-syscall/mod.ts";
|
||||
import { CompleteEvent } from "../../plug-api/app_event.ts";
|
||||
import { CompleteEvent } from "$sb/app_event.ts";
|
||||
|
||||
export async function queryComplete(completeEvent: CompleteEvent) {
|
||||
const match = /#query ([\w\-_]+)*$/.exec(completeEvent.linePrefix);
|
||||
|
||||
+35
-33
@@ -5,46 +5,48 @@ import type { IndexTreeEvent, QueryProviderEvent } from "$sb/app_event.ts";
|
||||
import { index } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { collectNodesOfType, findNodeOfType } from "$sb/lib/tree.ts";
|
||||
import { applyQuery, removeQueries } from "$sb/lib/query.ts";
|
||||
import * as YAML from "yaml";
|
||||
import { YAML } from "$sb/plugos-syscall/mod.ts";
|
||||
|
||||
export async function indexData({ name, tree }: IndexTreeEvent) {
|
||||
const dataObjects: { key: string; value: any }[] = [];
|
||||
|
||||
removeQueries(tree);
|
||||
|
||||
collectNodesOfType(tree, "FencedCode").forEach((t) => {
|
||||
const codeInfoNode = findNodeOfType(t, "CodeInfo");
|
||||
if (!codeInfoNode) {
|
||||
return;
|
||||
}
|
||||
if (codeInfoNode.children![0].text !== "data") {
|
||||
return;
|
||||
}
|
||||
const codeTextNode = findNodeOfType(t, "CodeText");
|
||||
if (!codeTextNode) {
|
||||
// Honestly, this shouldn't happen
|
||||
return;
|
||||
}
|
||||
const codeText = codeTextNode.children![0].text!;
|
||||
try {
|
||||
const docs = codeText.split("---").map((d) => YAML.parse(d));
|
||||
// We support multiple YAML documents in one block
|
||||
for (let i = 0; i < docs.length; i++) {
|
||||
const doc = docs[i];
|
||||
if (!doc) {
|
||||
continue;
|
||||
}
|
||||
dataObjects.push({
|
||||
key: `data:${name}@${t.from! + i}`,
|
||||
value: doc,
|
||||
});
|
||||
await Promise.all(
|
||||
collectNodesOfType(tree, "FencedCode").map(async (t) => {
|
||||
const codeInfoNode = findNodeOfType(t, "CodeInfo");
|
||||
if (!codeInfoNode) {
|
||||
return;
|
||||
}
|
||||
// console.log("Parsed data", parsedData);
|
||||
} catch (e) {
|
||||
console.error("Could not parse data", codeText, "error:", e);
|
||||
return;
|
||||
}
|
||||
});
|
||||
if (codeInfoNode.children![0].text !== "data") {
|
||||
return;
|
||||
}
|
||||
const codeTextNode = findNodeOfType(t, "CodeText");
|
||||
if (!codeTextNode) {
|
||||
// Honestly, this shouldn't happen
|
||||
return;
|
||||
}
|
||||
const codeText = codeTextNode.children![0].text!;
|
||||
try {
|
||||
const docs = codeText.split("---");
|
||||
// We support multiple YAML documents in one block
|
||||
for (let i = 0; i < docs.length; i++) {
|
||||
const doc = await YAML.parse(docs[i]);
|
||||
if (!doc) {
|
||||
continue;
|
||||
}
|
||||
dataObjects.push({
|
||||
key: `data:${name}@${t.from! + i}`,
|
||||
value: doc,
|
||||
});
|
||||
}
|
||||
// console.log("Parsed data", parsedData);
|
||||
} catch (e) {
|
||||
console.error("Could not parse data", codeText, "error:", e);
|
||||
return;
|
||||
}
|
||||
}),
|
||||
);
|
||||
// console.log("Found", dataObjects.length, "data objects");
|
||||
await index.batchSet(name, dataObjects);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
name: directive
|
||||
imports:
|
||||
- https://get.silverbullet.md/global.plug.json
|
||||
requiredPermissions:
|
||||
- fetch
|
||||
functions:
|
||||
serverRenderDirective:
|
||||
path: ./command.ts:serverRenderDirective
|
||||
updateDirectivesOnPageCommand:
|
||||
path: ./command.ts:updateDirectivesOnPageCommand
|
||||
command:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ParseTree, renderToText } from "$sb/lib/tree.ts";
|
||||
import { sync } from "../../plug-api/silverbullet-syscall/mod.ts";
|
||||
|
||||
import { evalDirectiveRenderer } from "./eval_directive.ts";
|
||||
import { queryDirectiveRenderer } from "./query_directive.ts";
|
||||
@@ -52,6 +53,14 @@ export async function directiveDispatcher(
|
||||
}
|
||||
} else {
|
||||
// #query
|
||||
if (!(await sync.hasInitialSyncCompleted())) {
|
||||
console.info(
|
||||
"Initial sync hasn't completed yet, not updating query directives.",
|
||||
);
|
||||
// Render the query directive as-is
|
||||
return renderToText(directiveTree);
|
||||
}
|
||||
|
||||
const newBody = await directiveRenderers["query"](
|
||||
"query",
|
||||
pageName,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// This is some shocking stuff. My profession would kill me for this.
|
||||
|
||||
import * as YAML from "yaml";
|
||||
import { YAML } from "$sb/plugos-syscall/mod.ts";
|
||||
import { ParseTree } from "$sb/lib/tree.ts";
|
||||
import { jsonToMDTable, renderTemplate } from "./util.ts";
|
||||
|
||||
@@ -57,7 +57,7 @@ export async function evalDirectiveRenderer(
|
||||
} else if (Array.isArray(result)) {
|
||||
return jsonToMDTable(result);
|
||||
}
|
||||
return YAML.stringify(result);
|
||||
return await YAML.stringify(result);
|
||||
} catch (e: any) {
|
||||
return `**ERROR:** ${e.message}`;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { replaceTemplateVars } from "../core/template.ts";
|
||||
import { renderTemplate } from "./util.ts";
|
||||
import { parseQuery } from "./parser.ts";
|
||||
import { jsonToMDTable } from "./util.ts";
|
||||
import { ParseTree } from "../../plug-api/lib/tree.ts";
|
||||
import { ParseTree } from "$sb/lib/tree.ts";
|
||||
|
||||
export async function queryDirectiveRenderer(
|
||||
_directive: string,
|
||||
@@ -31,8 +31,12 @@ export async function queryDirectiveRenderer(
|
||||
// This means there was no handler for the event which means it's unsupported
|
||||
return `**Error:** Unsupported query source '${parsedQuery.table}'`;
|
||||
} else if (results.length === 1) {
|
||||
// console.log("Parsed query", parsedQuery);
|
||||
if (parsedQuery.render) {
|
||||
const rendered = await renderTemplate(parsedQuery.render, results[0]);
|
||||
const rendered = await renderTemplate(
|
||||
parsedQuery.render,
|
||||
results[0],
|
||||
);
|
||||
return rendered.trim();
|
||||
} else {
|
||||
if (results[0].length === 0) {
|
||||
|
||||
@@ -7,7 +7,8 @@ import Handlebars from "handlebars";
|
||||
import { replaceTemplateVars } from "../core/template.ts";
|
||||
import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
|
||||
import { directiveRegex } from "./directives.ts";
|
||||
import { serverUpdateDirectives } from "./command.ts";
|
||||
import { updateDirectives } from "./command.ts";
|
||||
import { registerHandlebarsHelpers } from "./util.ts";
|
||||
|
||||
const templateRegex = /\[\[([^\]]+)\]\]\s*(.*)\s*/;
|
||||
|
||||
@@ -48,16 +49,20 @@ export async function templateDirectiveRenderer(
|
||||
// if it's a template injection (not a literal "include")
|
||||
if (directive === "use") {
|
||||
const tree = await markdown.parseMarkdown(templateText);
|
||||
extractFrontmatter(tree, ["$disableDirectives"]);
|
||||
await extractFrontmatter(tree, ["$disableDirectives"]);
|
||||
templateText = renderToText(tree);
|
||||
registerHandlebarsHelpers();
|
||||
const templateFn = Handlebars.compile(
|
||||
replaceTemplateVars(templateText, pageName),
|
||||
{ noEscape: true },
|
||||
);
|
||||
if (typeof parsedArgs !== "string") {
|
||||
(parsedArgs as any).page = pageName;
|
||||
}
|
||||
newBody = templateFn(parsedArgs);
|
||||
|
||||
// Recursively render directives
|
||||
newBody = await serverUpdateDirectives(pageName, newBody);
|
||||
newBody = await updateDirectives(pageName, newBody);
|
||||
}
|
||||
return newBody.trim();
|
||||
}
|
||||
|
||||
+27
-21
@@ -1,5 +1,4 @@
|
||||
import Handlebars from "handlebars";
|
||||
import * as YAML from "yaml";
|
||||
|
||||
import { space } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { niceDate } from "$sb/lib/dates.ts";
|
||||
@@ -83,8 +82,35 @@ export async function renderTemplate(
|
||||
renderTemplate: string,
|
||||
data: any[],
|
||||
): Promise<string> {
|
||||
registerHandlebarsHelpers();
|
||||
|
||||
// Handlebars.registerHelper("yaml", (v: any, prefix: string) => {
|
||||
// if (typeof prefix === "string") {
|
||||
// let yaml = (await YAML.stringify(v))
|
||||
// .split("\n")
|
||||
// .join("\n" + prefix)
|
||||
// .trim();
|
||||
// if (Array.isArray(v)) {
|
||||
// return "\n" + prefix + yaml;
|
||||
// } else {
|
||||
// return yaml;
|
||||
// }
|
||||
// } else {
|
||||
// return YAML.stringify(v).trim();
|
||||
// }
|
||||
// });
|
||||
let templateText = await space.readPage(renderTemplate);
|
||||
templateText = `{{#each .}}\n${templateText}\n{{/each}}`;
|
||||
const template = Handlebars.compile(templateText, { noEscape: true });
|
||||
return template(data);
|
||||
}
|
||||
|
||||
export function registerHandlebarsHelpers() {
|
||||
Handlebars.registerHelper("json", (v: any) => JSON.stringify(v));
|
||||
Handlebars.registerHelper("niceDate", (ts: any) => niceDate(new Date(ts)));
|
||||
Handlebars.registerHelper("escapeRegexp", (ts: any) => {
|
||||
return ts.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
|
||||
});
|
||||
Handlebars.registerHelper("prefixLines", (v: string, prefix: string) =>
|
||||
v
|
||||
.split("\n")
|
||||
@@ -96,24 +122,4 @@ export async function renderTemplate(
|
||||
(s: string, from: number, to: number, elipsis = "") =>
|
||||
s.length > to - from ? s.substring(from, to) + elipsis : s,
|
||||
);
|
||||
|
||||
Handlebars.registerHelper("yaml", (v: any, prefix: string) => {
|
||||
if (typeof prefix === "string") {
|
||||
let yaml = YAML.stringify(v)
|
||||
.split("\n")
|
||||
.join("\n" + prefix)
|
||||
.trim();
|
||||
if (Array.isArray(v)) {
|
||||
return "\n" + prefix + yaml;
|
||||
} else {
|
||||
return yaml;
|
||||
}
|
||||
} else {
|
||||
return YAML.stringify(v).trim();
|
||||
}
|
||||
});
|
||||
let templateText = await space.readPage(renderTemplate);
|
||||
templateText = `{{#each .}}\n${templateText}\n{{/each}}`;
|
||||
const template = Handlebars.compile(templateText, { noEscape: true });
|
||||
return template(data);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
name: emoji
|
||||
imports:
|
||||
- https://get.silverbullet.md/global.plug.json
|
||||
functions:
|
||||
emojiCompleter:
|
||||
path: "./emoji.ts:emojiCompleter"
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
name: global
|
||||
dependencies:
|
||||
"https://esm.sh/handlebars": "https://esm.sh/handlebars@4.7.7"
|
||||
"https://deno.land/std@0.184.0/yaml/mod.ts": "https://deno.land/std@0.184.0/yaml/mod.ts"
|
||||
@@ -1,6 +1,4 @@
|
||||
name: markdown
|
||||
imports:
|
||||
- https://get.silverbullet.md/global.plug.json
|
||||
assets:
|
||||
- "assets/*"
|
||||
requiredPermissions:
|
||||
@@ -12,9 +10,9 @@ functions:
|
||||
name: "Markdown Preview: Toggle"
|
||||
key: Ctrl-p
|
||||
mac: Cmd-p
|
||||
|
||||
preview:
|
||||
path: "./preview.ts:updateMarkdownPreview"
|
||||
env: client
|
||||
events:
|
||||
- plug:load
|
||||
- editor:updated
|
||||
@@ -22,16 +20,9 @@ functions:
|
||||
- editor:pageReloaded
|
||||
previewClickHandler:
|
||||
path: "./preview.ts:previewClickHandler"
|
||||
env: client
|
||||
events:
|
||||
- preview:click
|
||||
|
||||
# $share: file:* publisher for markdown files
|
||||
sharePublisher:
|
||||
path: ./share.ts:sharePublisher
|
||||
events:
|
||||
- share:file
|
||||
|
||||
markdownWidget:
|
||||
path: ./widget.ts:markdownWidget
|
||||
codeWidget: markdown
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { clientStore, editor, system } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { editor } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { readSettings } from "$sb/lib/settings_page.ts";
|
||||
import { updateMarkdownPreview } from "./preview.ts";
|
||||
import { store } from "$sb/plugos-syscall/mod.ts";
|
||||
|
||||
export async function togglePreview() {
|
||||
const currentValue = !!(await clientStore.get("enableMarkdownPreview"));
|
||||
await clientStore.set("enableMarkdownPreview", !currentValue);
|
||||
const currentValue = !!(await store.get("enableMarkdownPreview"));
|
||||
await store.set("enableMarkdownPreview", !currentValue);
|
||||
if (!currentValue) {
|
||||
await system.invokeFunction("client", "preview");
|
||||
await updateMarkdownPreview();
|
||||
} else {
|
||||
await hideMarkdownPreview();
|
||||
}
|
||||
|
||||
@@ -2,32 +2,31 @@ import buildMarkdown from "../../common/markdown_parser/parser.ts";
|
||||
import { parse } from "../../common/markdown_parser/parse_tree.ts";
|
||||
import { System } from "../../plugos/system.ts";
|
||||
|
||||
import corePlug from "../../dist_bundle/_plug/core.plug.json" assert {
|
||||
type: "json",
|
||||
};
|
||||
import tasksPlug from "../../dist_bundle/_plug/tasks.plug.json" assert {
|
||||
type: "json",
|
||||
};
|
||||
import { createSandbox } from "../../plugos/environments/deno_sandbox.ts";
|
||||
import { loadMarkdownExtensions } from "../../common/markdown_parser/markdown_ext.ts";
|
||||
import { renderMarkdownToHtml } from "./markdown_render.ts";
|
||||
import { assertEquals } from "../../test_deps.ts";
|
||||
import { urlToPathname } from "../../plugos/util.ts";
|
||||
|
||||
Deno.test("Markdown render", async () => {
|
||||
const system = new System<any>("server");
|
||||
await system.load(corePlug, createSandbox);
|
||||
await system.load(tasksPlug, createSandbox);
|
||||
await system.load(
|
||||
new URL("../../dist_plug_bundle/_plug/core.plug.js", import.meta.url),
|
||||
createSandbox,
|
||||
);
|
||||
await system.load(
|
||||
new URL("../../dist_plug_bundle/_plug/tasks.plug.js", import.meta.url),
|
||||
createSandbox,
|
||||
);
|
||||
const lang = buildMarkdown(loadMarkdownExtensions(system));
|
||||
const testFile = Deno.readTextFileSync(
|
||||
urlToPathname(new URL("test/example.md", import.meta.url)),
|
||||
new URL("test/example.md", import.meta.url).pathname,
|
||||
);
|
||||
const tree = parse(lang, testFile);
|
||||
renderMarkdownToHtml(tree, {
|
||||
await renderMarkdownToHtml(tree, {
|
||||
failOnUnknown: true,
|
||||
renderFrontMatter: true,
|
||||
});
|
||||
// console.log("HTML", html);
|
||||
await system.unloadAll();
|
||||
});
|
||||
|
||||
Deno.test("Smart hard break test", async () => {
|
||||
|
||||
@@ -4,14 +4,12 @@ import {
|
||||
renderToText,
|
||||
traverseTree,
|
||||
} from "$sb/lib/tree.ts";
|
||||
import * as YAML from "yaml";
|
||||
import { Fragment, renderHtml, Tag } from "./html_render.ts";
|
||||
|
||||
type MarkdownRenderOptions = {
|
||||
failOnUnknown?: true;
|
||||
smartHardBreak?: true;
|
||||
annotationPositions?: true;
|
||||
renderFrontMatter?: true;
|
||||
attachmentUrlPrefix?: string;
|
||||
// When defined, use to inline images as data: urls
|
||||
inlineAttachments?: (url: string) => Promise<string>;
|
||||
@@ -78,33 +76,7 @@ function render(
|
||||
body: cleanTags(mapRender(t.children!)),
|
||||
};
|
||||
case "FrontMatter":
|
||||
if (options.renderFrontMatter) {
|
||||
const yamlCode = renderToText(t.children![1]);
|
||||
const parsedYaml = YAML.parse(yamlCode) as Record<string, any>;
|
||||
const rows: Tag[] = [];
|
||||
for (const [k, v] of Object.entries(parsedYaml)) {
|
||||
rows.push({
|
||||
name: "tr",
|
||||
body: [
|
||||
{ name: "td", attrs: { class: "key" }, body: k },
|
||||
{
|
||||
name: "td",
|
||||
attrs: { class: "value" },
|
||||
body: YAML.stringify(v),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
return {
|
||||
name: "table",
|
||||
attrs: {
|
||||
class: "front-matter",
|
||||
},
|
||||
body: rows,
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
case "CommentBlock":
|
||||
// Remove, for now
|
||||
return null;
|
||||
@@ -261,7 +233,7 @@ function render(
|
||||
return {
|
||||
name: "a",
|
||||
attrs: {
|
||||
href: `/${ref.replaceAll(" ", "_").replace("@", "#")}`,
|
||||
href: `/${ref.replace("@", "#")}`,
|
||||
},
|
||||
body: linkText,
|
||||
};
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import {
|
||||
clientStore,
|
||||
editor,
|
||||
space,
|
||||
system,
|
||||
} from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { asset } from "$sb/plugos-syscall/mod.ts";
|
||||
import { parseMarkdown } from "../../plug-api/silverbullet-syscall/markdown.ts";
|
||||
import { editor, space, system } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { asset, store } from "$sb/plugos-syscall/mod.ts";
|
||||
import { parseMarkdown } from "$sb/silverbullet-syscall/markdown.ts";
|
||||
import { renderMarkdownToHtml } from "./markdown_render.ts";
|
||||
|
||||
export async function updateMarkdownPreview() {
|
||||
if (!(await clientStore.get("enableMarkdownPreview"))) {
|
||||
if (!(await store.get("enableMarkdownPreview"))) {
|
||||
return;
|
||||
}
|
||||
const text = await editor.getText();
|
||||
@@ -20,7 +15,6 @@ export async function updateMarkdownPreview() {
|
||||
const html = await renderMarkdownToHtml(mdTree, {
|
||||
smartHardBreak: true,
|
||||
annotationPositions: true,
|
||||
renderFrontMatter: true,
|
||||
inlineAttachments: async (url): Promise<string> => {
|
||||
if (!url.includes("://")) {
|
||||
try {
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { markdown, space } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { LocalFileSystem } from "$sb/plugos-syscall/mod.ts";
|
||||
import { asset } from "$sb/plugos-syscall/mod.ts";
|
||||
import { renderMarkdownToHtml } from "./markdown_render.ts";
|
||||
import { PublishEvent } from "$sb/app_event.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 rootFS = new LocalFileSystem("");
|
||||
|
||||
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 rootFS.writeFile(path, html, "utf8");
|
||||
return true;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
import { markdown } from "$sb/silverbullet-syscall/mod.ts";
|
||||
|
||||
export function encodePageUrl(name: string): string {
|
||||
return name.replaceAll(" ", "_");
|
||||
return name;
|
||||
}
|
||||
|
||||
export async function cleanMarkdown(
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
name: plugmd
|
||||
imports:
|
||||
- https://get.silverbullet.md/global.plug.json
|
||||
functions:
|
||||
check:
|
||||
path: "./plugmd.ts:checkCommand"
|
||||
command:
|
||||
name: "Plug: Check"
|
||||
mac: "Cmd-Alt-c"
|
||||
key: "Ctrl-Alt-c"
|
||||
compile:
|
||||
path: "./plugmd.ts:compileCommand"
|
||||
command:
|
||||
name: "Plug: Compile"
|
||||
mac: "Cmd-Shift-c"
|
||||
key: "Ctrl-Shift-c"
|
||||
compileJS:
|
||||
path: "./plugmd.ts:compileJS"
|
||||
env: server
|
||||
compileModule:
|
||||
path: "./plugmd.ts:compileModule"
|
||||
env: server
|
||||
getPlugPlugMd:
|
||||
path: "./plugmd.ts:getPlugPlugMd"
|
||||
events:
|
||||
- get-plug:plugmd
|
||||
@@ -1,125 +0,0 @@
|
||||
import { collectNodesOfType, findNodeOfType } from "$sb/lib/tree.ts";
|
||||
import {
|
||||
editor,
|
||||
markdown,
|
||||
space,
|
||||
system,
|
||||
} from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { syscall } from "$sb/plugos-syscall/mod.ts";
|
||||
import * as YAML from "yaml";
|
||||
|
||||
import type { Manifest } from "../../common/manifest.ts";
|
||||
|
||||
export async function compileCommand() {
|
||||
const text = await editor.getText();
|
||||
try {
|
||||
const manifest = await compileDefinition(text);
|
||||
await space.writePage(
|
||||
`_plug/${manifest.name}`,
|
||||
JSON.stringify(manifest, null, 2),
|
||||
);
|
||||
console.log("Wrote this plug", manifest);
|
||||
await editor.hidePanel("bhs");
|
||||
|
||||
system.reloadPlugs();
|
||||
} catch (e: any) {
|
||||
await editor.showPanel("bhs", 1, e.message);
|
||||
// console.error("Got this error from compiler", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkCommand() {
|
||||
const text = await editor.getText();
|
||||
try {
|
||||
await compileDefinition(text);
|
||||
await editor.hidePanel("bhs");
|
||||
system.reloadPlugs();
|
||||
} catch (e: any) {
|
||||
await editor.showPanel("bhs", 1, e.message);
|
||||
// console.error("Got this error from compiler", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function compileDefinition(text: string): Promise<Manifest> {
|
||||
const tree = await markdown.parseMarkdown(text);
|
||||
|
||||
const codeNodes = collectNodesOfType(tree, "FencedCode");
|
||||
let manifest: Manifest | undefined;
|
||||
let code: string | undefined;
|
||||
let language = "js";
|
||||
for (const codeNode of codeNodes) {
|
||||
const codeInfo = findNodeOfType(codeNode, "CodeInfo")!.children![0].text!;
|
||||
const codeText = findNodeOfType(codeNode, "CodeText")!.children![0].text!;
|
||||
if (codeInfo === "yaml") {
|
||||
manifest = YAML.parse(codeText) as Manifest;
|
||||
continue;
|
||||
}
|
||||
if (codeInfo === "typescript" || codeInfo === "ts") {
|
||||
language = "ts";
|
||||
}
|
||||
code = codeText;
|
||||
}
|
||||
|
||||
if (!manifest) {
|
||||
throw new Error("No meta found");
|
||||
}
|
||||
|
||||
if (!code) {
|
||||
throw new Error("No code found");
|
||||
}
|
||||
|
||||
manifest.dependencies = manifest.dependencies || {};
|
||||
|
||||
for (const [dep, depSpec] of Object.entries(manifest.dependencies)) {
|
||||
const compiled = await system.invokeFunction(
|
||||
"server",
|
||||
"compileModule",
|
||||
depSpec,
|
||||
);
|
||||
manifest.dependencies![dep] = compiled;
|
||||
}
|
||||
|
||||
manifest.functions = manifest.functions || {};
|
||||
|
||||
for (const [name, func] of Object.entries(manifest.functions)) {
|
||||
const compiled = await system.invokeFunction(
|
||||
"server",
|
||||
"compileJS",
|
||||
`file.${language}`,
|
||||
code,
|
||||
name,
|
||||
Object.keys(manifest.dependencies),
|
||||
);
|
||||
func.code = compiled;
|
||||
}
|
||||
|
||||
console.log("Doing the whole manifest thing");
|
||||
|
||||
return manifest;
|
||||
}
|
||||
|
||||
export function compileJS(
|
||||
filename: string,
|
||||
code: string,
|
||||
functionName: string,
|
||||
excludeModules: string[],
|
||||
): Promise<string> {
|
||||
// console.log("Compiling JS", filename, excludeModules);
|
||||
return syscall(
|
||||
"esbuild.compile",
|
||||
filename,
|
||||
code,
|
||||
functionName,
|
||||
excludeModules,
|
||||
);
|
||||
}
|
||||
|
||||
export function compileModule(moduleName: string): Promise<string> {
|
||||
return syscall("esbuild.compileModule", moduleName);
|
||||
}
|
||||
|
||||
export async function getPlugPlugMd(pageName: string): Promise<Manifest> {
|
||||
const text = await space.readPage(pageName);
|
||||
console.log("Compiling", pageName);
|
||||
return compileDefinition(text);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { assertEquals } from "../../test_deps.ts";
|
||||
import { BatchKVStore, SimpleSearchEngine } from "./engine.ts";
|
||||
|
||||
class InMemoryBatchKVStore<K, V> implements BatchKVStore<K, V> {
|
||||
private store = new Map<K, V>();
|
||||
|
||||
get(keys: K[]): Promise<(V | undefined)[]> {
|
||||
const results: (V | undefined)[] = keys.map((key) => this.store.get(key));
|
||||
return Promise.resolve(results);
|
||||
}
|
||||
|
||||
set(entries: Map<K, V>): Promise<void> {
|
||||
for (const [key, value] of entries) {
|
||||
this.store.set(key, value);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
delete(keys: K[]): Promise<void> {
|
||||
for (const key of keys) {
|
||||
this.store.delete(key);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
Deno.test("Test full text search", async () => {
|
||||
const engine = new SimpleSearchEngine(
|
||||
new InMemoryBatchKVStore(),
|
||||
new InMemoryBatchKVStore(),
|
||||
);
|
||||
|
||||
await engine.indexDocument({ id: "1", text: "The quick brown fox" });
|
||||
await engine.indexDocument({ id: "2", text: "jumps over the lazy dogs" });
|
||||
await engine.indexDocument({
|
||||
id: "3",
|
||||
text: "Hello world, jumping jump jumps",
|
||||
});
|
||||
await engine.indexDocument({ id: "4", text: "TypeScript is awesome" });
|
||||
await engine.indexDocument({ id: "5", text: "The brown dogs jumps zęf" });
|
||||
|
||||
console.log(engine.index);
|
||||
|
||||
const results = await engine.search("Brown fox");
|
||||
console.log(results);
|
||||
assertEquals(results.length, 2);
|
||||
assertEquals(results[0].id, "1");
|
||||
assertEquals(results[0].score, 2);
|
||||
assertEquals(results[1].id, "5");
|
||||
assertEquals(results[1].score, 1);
|
||||
|
||||
const results2 = await engine.search("jump");
|
||||
console.log(results2);
|
||||
assertEquals(results2.length, 3);
|
||||
|
||||
await engine.deleteDocument("3");
|
||||
const results3 = await engine.search("jump");
|
||||
console.log(results3);
|
||||
assertEquals(results3.length, 2);
|
||||
|
||||
const results4 = await engine.search("zęf");
|
||||
console.log(results4);
|
||||
assertEquals(results4.length, 1);
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
import { c } from "https://esm.sh/@codemirror/legacy-modes@6.3.1/mode/clike?external=@codemirror/language";
|
||||
import { stemmer } from "https://esm.sh/porter-stemmer@0.9.1";
|
||||
|
||||
export type Document = {
|
||||
id: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export interface BatchKVStore<K, V> {
|
||||
get(keys: K[]): Promise<(V | undefined)[]>;
|
||||
set(entries: Map<K, V>): Promise<void>;
|
||||
delete(keys: K[]): Promise<void>;
|
||||
}
|
||||
|
||||
type ResultObject = {
|
||||
score: number;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export class SimpleSearchEngine {
|
||||
private stopWords = ["and", "or", "the", "a", "an"];
|
||||
|
||||
constructor(
|
||||
public index: BatchKVStore<string, string[]>,
|
||||
public reverseIndex: BatchKVStore<string, string[]>,
|
||||
) {
|
||||
}
|
||||
|
||||
// Tokenize text into words
|
||||
private tokenize(text: string): string[] {
|
||||
return text.toLowerCase().split(/[^\p{L}]+/u);
|
||||
}
|
||||
|
||||
// Remove stop words from array of words
|
||||
private removeStopWords(words: string[]): string[] {
|
||||
return words.filter((word) =>
|
||||
word.length > 2 &&
|
||||
!this.stopWords.includes(word) && /^\p{L}+$/u.test(word)
|
||||
);
|
||||
}
|
||||
|
||||
// Basic stemming function
|
||||
private stem(word: string): string {
|
||||
return stemmer(word);
|
||||
}
|
||||
|
||||
// Index an array of documents
|
||||
public async indexDocument(document: Document): Promise<void> {
|
||||
const updateIndexMap = new Map<string, string[]>();
|
||||
const updateReverseIndexMap = new Map<string, string[]>();
|
||||
|
||||
const words = this.tokenize(document.text);
|
||||
const filteredWords = this.removeStopWords(words);
|
||||
const stemmedWords = filteredWords.map((word) => this.stem(word));
|
||||
|
||||
// Get the current IDs for these stemmed words
|
||||
const uniqueStemmedWords = [...new Set(stemmedWords)];
|
||||
const currentIdsArray = await this.index.get(uniqueStemmedWords);
|
||||
|
||||
stemmedWords.forEach((stemmedWord, i) => {
|
||||
const currentIds =
|
||||
currentIdsArray[uniqueStemmedWords.indexOf(stemmedWord)] || [];
|
||||
|
||||
currentIds.push(document.id);
|
||||
updateIndexMap.set(stemmedWord, currentIds);
|
||||
|
||||
if (!updateReverseIndexMap.has(document.id)) {
|
||||
updateReverseIndexMap.set(document.id, []);
|
||||
}
|
||||
|
||||
if (!updateReverseIndexMap.get(document.id)!.includes(stemmedWord)) {
|
||||
updateReverseIndexMap.get(document.id)!.push(stemmedWord);
|
||||
}
|
||||
});
|
||||
|
||||
// console.log("updateIndexMap", updateIndexMap);
|
||||
|
||||
await this.index.set(updateIndexMap);
|
||||
await this.reverseIndex.set(updateReverseIndexMap);
|
||||
}
|
||||
|
||||
// Search for a phrase and return document ids sorted by match count
|
||||
public async search(phrase: string): Promise<ResultObject[]> {
|
||||
const words = this.tokenize(phrase);
|
||||
const filteredWords = this.removeStopWords(words);
|
||||
const stemmedWords = filteredWords.map((word) => this.stem(word));
|
||||
|
||||
const wordIdsArray = await this.index.get(stemmedWords);
|
||||
const matchCounts: Map<string, number> = new Map();
|
||||
|
||||
wordIdsArray.forEach((wordIds) => {
|
||||
if (wordIds) {
|
||||
wordIds.forEach((id) => {
|
||||
if (matchCounts.has(id)) {
|
||||
matchCounts.set(id, matchCounts.get(id)! + 1);
|
||||
} else {
|
||||
matchCounts.set(id, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const results = Array.from(matchCounts.entries()).map(
|
||||
([id, score]) => ({ id, score }),
|
||||
);
|
||||
|
||||
return results.sort((a, b) => b.score - a.score);
|
||||
}
|
||||
|
||||
// Delete a document from the index
|
||||
public async deleteDocument(documentId: string): Promise<void> {
|
||||
const words = await this.reverseIndex.get([documentId]);
|
||||
if (words && words[0]) {
|
||||
const currentIdsArray = await this.index.get(words[0]);
|
||||
const deleteKeys: string[] = [];
|
||||
const updateMap = new Map<string, string[]>();
|
||||
|
||||
words[0].forEach((word, i) => {
|
||||
const currentIds = currentIdsArray[i];
|
||||
if (currentIds) {
|
||||
const updatedIds = currentIds.filter((id) => id !== documentId);
|
||||
if (updatedIds.length > 0) {
|
||||
updateMap.set(word, updatedIds);
|
||||
} else {
|
||||
deleteKeys.push(word);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (deleteKeys.length > 0) {
|
||||
await this.index.delete(deleteKeys);
|
||||
}
|
||||
if (updateMap.size > 0) {
|
||||
await this.index.set(updateMap);
|
||||
}
|
||||
|
||||
await this.reverseIndex.delete([documentId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
name: search
|
||||
functions:
|
||||
indexPage:
|
||||
path: search.ts:indexPage
|
||||
events:
|
||||
- page:index
|
||||
|
||||
clearIndex:
|
||||
path: search.ts:clearIndex
|
||||
|
||||
searchUnindex:
|
||||
path: "./search.ts:pageUnindex"
|
||||
events:
|
||||
- page:deleted
|
||||
searchQueryProvider:
|
||||
path: ./search.ts:queryProvider
|
||||
events:
|
||||
- query:full-text
|
||||
searchCommand:
|
||||
path: ./search.ts:searchCommand
|
||||
command:
|
||||
name: "Search Space"
|
||||
key: Ctrl-Shift-f
|
||||
mac: Cmd-Shift-f
|
||||
|
||||
readPageSearch:
|
||||
path: ./search.ts:readFileSearch
|
||||
pageNamespace:
|
||||
pattern: "🔍 .+"
|
||||
operation: readFile
|
||||
|
||||
writePageSearch:
|
||||
path: ./search.ts:writeFileSearch
|
||||
pageNamespace:
|
||||
pattern: "🔍 .+"
|
||||
operation: readFile
|
||||
|
||||
getPageMetaSearch:
|
||||
path: ./search.ts:getFileMetaSearch
|
||||
pageNamespace:
|
||||
pattern: "🔍 .+"
|
||||
operation: getFileMeta
|
||||
@@ -1,25 +1,51 @@
|
||||
import { fulltext } from "$sb/plugos-syscall/mod.ts";
|
||||
import { renderToText } from "$sb/lib/tree.ts";
|
||||
import type { FileMeta } from "../../common/types.ts";
|
||||
import { editor, index } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { IndexTreeEvent, QueryProviderEvent } from "$sb/app_event.ts";
|
||||
import { applyQuery, removeQueries } from "$sb/lib/query.ts";
|
||||
import {
|
||||
FileData,
|
||||
FileEncoding,
|
||||
} from "../../common/spaces/space_primitives.ts";
|
||||
import { renderToText } from "$sb/lib/tree.ts";
|
||||
import { store } from "$sb/plugos-syscall/mod.ts";
|
||||
import { applyQuery } from "$sb/lib/query.ts";
|
||||
import { editor, index } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { base64EncodedDataUrl } from "../../plugos/asset_bundle/base64.ts";
|
||||
import { BatchKVStore, SimpleSearchEngine } from "./engine.ts";
|
||||
import { FileMeta } from "../../common/types.ts";
|
||||
|
||||
const searchPrefix = "🔍 ";
|
||||
|
||||
export async function pageIndex(data: IndexTreeEvent) {
|
||||
removeQueries(data.tree);
|
||||
const cleanText = renderToText(data.tree);
|
||||
await fulltext.fullTextIndex(data.name, cleanText);
|
||||
class StoreKVStore implements BatchKVStore<string, string[]> {
|
||||
constructor(private prefix: string) {
|
||||
}
|
||||
get(keys: string[]): Promise<(string[] | undefined)[]> {
|
||||
return store.batchGet(keys.map((key) => this.prefix + key));
|
||||
}
|
||||
set(entries: Map<string, string[]>): Promise<void> {
|
||||
return store.batchSet(
|
||||
Array.from(entries.entries()).map((
|
||||
[key, value],
|
||||
) => ({ key: this.prefix + key, value })),
|
||||
);
|
||||
}
|
||||
delete(keys: string[]): Promise<void> {
|
||||
return store.batchDel(keys.map((key) => this.prefix + key));
|
||||
}
|
||||
}
|
||||
|
||||
const engine = new SimpleSearchEngine(
|
||||
new StoreKVStore("fts:"),
|
||||
new StoreKVStore("fts_rev:"),
|
||||
);
|
||||
|
||||
export async function indexPage({ name, tree }: IndexTreeEvent) {
|
||||
const text = renderToText(tree);
|
||||
// console.log("Now FTS indexing", name);
|
||||
await engine.deleteDocument(name);
|
||||
await engine.indexDocument({ id: name, text });
|
||||
}
|
||||
|
||||
export async function clearIndex() {
|
||||
await store.deletePrefix("fts:");
|
||||
await store.deletePrefix("fts_rev:");
|
||||
}
|
||||
|
||||
export async function pageUnindex(pageName: string) {
|
||||
await fulltext.fullTextDelete(pageName);
|
||||
await engine.deleteDocument(pageName);
|
||||
}
|
||||
|
||||
export async function queryProvider({
|
||||
@@ -29,10 +55,13 @@ export async function queryProvider({
|
||||
if (!phraseFilter) {
|
||||
throw Error("No 'phrase' filter specified, this is mandatory");
|
||||
}
|
||||
let results = await fulltext.fullTextSearch(phraseFilter.value, {
|
||||
highlightEllipsis: "...",
|
||||
limit: 100,
|
||||
});
|
||||
let results: any[] = await engine.search(phraseFilter.value);
|
||||
|
||||
// Patch the object to a format that users expect (translate id to name)
|
||||
for (const r of results) {
|
||||
r.name = r.id;
|
||||
delete r.id;
|
||||
}
|
||||
|
||||
const allPageMap: Map<string, any> = new Map(
|
||||
results.map((r: any) => [r.name, r]),
|
||||
@@ -62,31 +91,22 @@ export async function searchCommand() {
|
||||
|
||||
export async function readFileSearch(
|
||||
name: string,
|
||||
encoding: FileEncoding,
|
||||
): Promise<{ data: FileData; meta: FileMeta }> {
|
||||
): Promise<{ data: string; meta: FileMeta }> {
|
||||
const phrase = name.substring(
|
||||
searchPrefix.length,
|
||||
name.length - ".md".length,
|
||||
);
|
||||
const results = await fulltext.fullTextSearch(phrase, {
|
||||
highlightEllipsis: "...",
|
||||
highlightPostfix: "==",
|
||||
highlightPrefix: "==",
|
||||
summaryMaxLength: 30,
|
||||
limit: 100,
|
||||
});
|
||||
const results = await engine.search(phrase);
|
||||
const text = `# Search results for "${phrase}"\n${
|
||||
results
|
||||
.map((r: any) =>
|
||||
`[[${r.name}]]:\n> ${r.snippet.split("\n").join("\n> ")}`
|
||||
)
|
||||
.join("\n\n")
|
||||
.map((r) => `* [[${r.id}]] (score ${r.score})`)
|
||||
.join("\n")
|
||||
}
|
||||
`;
|
||||
`;
|
||||
|
||||
return {
|
||||
// encoding === "arraybuffer" is not an option, so either it's "utf8" or "dataurl"
|
||||
data: encoding === "utf8" ? text : base64EncodedDataUrl(
|
||||
data: base64EncodedDataUrl(
|
||||
"text/markdown",
|
||||
new TextEncoder().encode(text),
|
||||
),
|
||||
@@ -100,6 +120,13 @@ export async function readFileSearch(
|
||||
};
|
||||
}
|
||||
|
||||
export function writeFileSearch(
|
||||
name: string,
|
||||
): FileMeta {
|
||||
// Never actually writing this
|
||||
return getFileMetaSearch(name);
|
||||
}
|
||||
|
||||
export function getFileMetaSearch(name: string): FileMeta {
|
||||
return {
|
||||
name,
|
||||
@@ -1,5 +1,5 @@
|
||||
import { events } from "$sb/plugos-syscall/mod.ts";
|
||||
import { editor, markdown, system } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { editor, markdown } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
|
||||
import { PublishEvent } from "$sb/app_event.ts";
|
||||
|
||||
@@ -8,7 +8,7 @@ export async function publishCommand() {
|
||||
const text = await editor.getText();
|
||||
const pageName = await editor.getCurrentPage();
|
||||
const tree = await markdown.parseMarkdown(text);
|
||||
const { $share } = extractFrontmatter(tree);
|
||||
const { $share } = await extractFrontmatter(tree);
|
||||
if (!$share) {
|
||||
await editor.flashNotification("Saved.");
|
||||
return;
|
||||
@@ -23,15 +23,14 @@ export async function publishCommand() {
|
||||
await editor.flashNotification("Sharing...");
|
||||
// Delegate actual publishing to the server
|
||||
try {
|
||||
await system.invokeFunction("server", "publish", pageName, $share);
|
||||
await 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[]) {
|
||||
async function publish(pageName: string, uris: string[]) {
|
||||
for (const uri of uris) {
|
||||
const publisher = uri.split(":")[0];
|
||||
const results = await events.dispatchEvent(
|
||||
|
||||
@@ -5,7 +5,4 @@ functions:
|
||||
command:
|
||||
name: "Share: Publish"
|
||||
key: "Ctrl-s"
|
||||
mac: "Cmd-s"
|
||||
publish:
|
||||
path: publish.ts:publish
|
||||
env: server
|
||||
mac: "Cmd-s"
|
||||
@@ -1,44 +0,0 @@
|
||||
name: sync
|
||||
functions:
|
||||
configureCommand:
|
||||
path: sync.ts:configureCommand
|
||||
command:
|
||||
name: "Sync: Configure"
|
||||
|
||||
disableCommand:
|
||||
path: sync.ts:disableCommand
|
||||
command:
|
||||
name: "Sync: Disable"
|
||||
|
||||
syncCommand:
|
||||
path: sync.ts:syncCommand
|
||||
command:
|
||||
name: "Sync: Sync"
|
||||
key: "Shift-Alt-s"
|
||||
|
||||
wipeAndSyncCommand:
|
||||
path: sync.ts:localWipeAndSyncCommand
|
||||
command:
|
||||
name: "Sync: Wipe Local Space and Sync"
|
||||
|
||||
syncOpenedPage:
|
||||
path: sync.ts:syncOpenedPage
|
||||
events:
|
||||
- editor:pageLoaded
|
||||
|
||||
check:
|
||||
env: server
|
||||
path: sync.ts:check
|
||||
|
||||
performSync:
|
||||
env: server
|
||||
path: sync.ts:performSync
|
||||
# Sync every minute
|
||||
cron: "* * * * *"
|
||||
|
||||
# Automatically sync the current page upon change
|
||||
syncPage:
|
||||
path: sync.ts:syncPage
|
||||
env: server
|
||||
events:
|
||||
- page:saved
|
||||
@@ -1,251 +0,0 @@
|
||||
import { store } from "$sb/plugos-syscall/mod.ts";
|
||||
import { editor, space, sync, system } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import type { SyncEndpoint } from "$sb/silverbullet-syscall/sync.ts";
|
||||
import { readSetting } from "$sb/lib/settings_page.ts";
|
||||
|
||||
export async function configureCommand() {
|
||||
const url = await editor.prompt(
|
||||
"Enter the URL of the remote space to sync with",
|
||||
"https://",
|
||||
);
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await editor.prompt("Username (if any):");
|
||||
let password = undefined;
|
||||
if (user) {
|
||||
password = await editor.prompt("Password:");
|
||||
}
|
||||
|
||||
const syncConfig: SyncEndpoint = {
|
||||
url,
|
||||
user,
|
||||
password,
|
||||
};
|
||||
|
||||
try {
|
||||
await system.invokeFunction("server", "check", syncConfig);
|
||||
} catch (e: any) {
|
||||
await editor.flashNotification(
|
||||
`Sync configuration failed: ${e.message}`,
|
||||
"error",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await store.batchSet([
|
||||
{ key: "sync.config", value: syncConfig },
|
||||
// Empty initial snapshot
|
||||
{ key: "sync.snapshot", value: {} },
|
||||
]);
|
||||
|
||||
await editor.flashNotification("Sync configuration saved.");
|
||||
|
||||
return syncConfig;
|
||||
}
|
||||
|
||||
export async function syncCommand() {
|
||||
let config: SyncEndpoint | undefined = await store.get("sync.config");
|
||||
if (!config) {
|
||||
config = await configureCommand();
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
await editor.flashNotification("Starting sync...");
|
||||
try {
|
||||
await system.invokeFunction("server", "check", config);
|
||||
|
||||
const operations = await system.invokeFunction("server", "performSync");
|
||||
await editor.flashNotification(
|
||||
`Sync complete. Performed ${operations} operations.`,
|
||||
);
|
||||
} catch (e: any) {
|
||||
await editor.flashNotification(
|
||||
`Sync failed: ${e.message}`,
|
||||
"error",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function disableCommand() {
|
||||
if (
|
||||
!(await editor.confirm(
|
||||
"Are you sure you want to disable sync?",
|
||||
))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove all sync related keys from the store
|
||||
await store.deletePrefix("sync.");
|
||||
await editor.flashNotification("Sync disabled.");
|
||||
}
|
||||
|
||||
export async function localWipeAndSyncCommand() {
|
||||
let config: SyncEndpoint | undefined = await store.get("sync.config");
|
||||
if (!config) {
|
||||
config = await configureCommand();
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!(await editor.confirm(
|
||||
"Are you sure you want to wipe your local space and sync with the remote?",
|
||||
))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!(await editor.confirm(
|
||||
"To be clear: this means all local content will be deleted with no way to recover it. Are you sure?",
|
||||
))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Wiping local pages");
|
||||
await editor.flashNotification("Now wiping all pages");
|
||||
for (const page of await space.listPages()) {
|
||||
console.log("Deleting page", page.name);
|
||||
await space.deletePage(page.name);
|
||||
}
|
||||
|
||||
console.log("Wiping local attachments");
|
||||
await editor.flashNotification("Now wiping all attachments");
|
||||
for (const attachment of await space.listAttachments()) {
|
||||
console.log("Deleting attachment", attachment.name);
|
||||
await space.deleteAttachment(attachment.name);
|
||||
}
|
||||
|
||||
console.log("Wiping local sync state");
|
||||
await store.set("sync.snapshot", {});
|
||||
|
||||
// Starting actual sync
|
||||
await syncCommand();
|
||||
|
||||
// And finally loading all plugs
|
||||
await system.invokeFunction("client", "core.updatePlugsCommand");
|
||||
}
|
||||
|
||||
export async function syncOpenedPage() {
|
||||
// Is sync on?
|
||||
if (!(await store.has("sync.config"))) {
|
||||
// Nope -> exit
|
||||
return;
|
||||
}
|
||||
await system.invokeFunction(
|
||||
"server",
|
||||
"syncPage",
|
||||
await editor.getCurrentPage(),
|
||||
);
|
||||
}
|
||||
|
||||
// Run on server
|
||||
export function check(config: SyncEndpoint) {
|
||||
return sync.check(config);
|
||||
}
|
||||
|
||||
// If a sync takes longer than this, we'll consider it timed out
|
||||
const syncTimeout = 1000 * 60 * 10; // 10 minutes
|
||||
|
||||
// Run on server
|
||||
export async function performSync() {
|
||||
const config: SyncEndpoint = await store.get("sync.config");
|
||||
if (!config) {
|
||||
// Sync not configured
|
||||
return;
|
||||
}
|
||||
|
||||
await augmentSettings(config);
|
||||
|
||||
// Check if sync not already in progress
|
||||
const ongoingSync: number | undefined = await store.get("sync.startTime");
|
||||
if (ongoingSync) {
|
||||
if (Date.now() - ongoingSync > syncTimeout) {
|
||||
console.log("Sync timed out, continuing");
|
||||
} else {
|
||||
console.log("Sync already in progress");
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Keep track of sync start time
|
||||
await store.set("sync.startTime", Date.now());
|
||||
|
||||
try {
|
||||
// Perform actual sync
|
||||
const snapshot = await store.get("sync.snapshot");
|
||||
const { snapshot: newSnapshot, operations, error } = await sync.syncAll(
|
||||
config,
|
||||
snapshot,
|
||||
);
|
||||
// Store snapshot
|
||||
await store.set("sync.snapshot", newSnapshot);
|
||||
// Clear sync start time
|
||||
await store.del("sync.startTime");
|
||||
if (error) {
|
||||
console.error("Sync error", error);
|
||||
throw new Error(error);
|
||||
}
|
||||
return operations;
|
||||
} catch (e: any) {
|
||||
// Clear sync start time
|
||||
await store.del("sync.startTime");
|
||||
console.error("Sync error", e);
|
||||
}
|
||||
}
|
||||
|
||||
async function augmentSettings(endpoint: SyncEndpoint) {
|
||||
const syncSettings = await readSetting("sync", {});
|
||||
if (syncSettings.excludePrefixes) {
|
||||
endpoint.excludePrefixes = syncSettings.excludePrefixes;
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncPage(page: string) {
|
||||
const config: SyncEndpoint = await store.get("sync.config");
|
||||
if (!config) {
|
||||
// Sync not configured
|
||||
return;
|
||||
}
|
||||
|
||||
await augmentSettings(config);
|
||||
|
||||
// Check if sync not already in progress
|
||||
const ongoingSync: number | undefined = await store.get("sync.startTime");
|
||||
if (ongoingSync) {
|
||||
if (Date.now() - ongoingSync > syncTimeout) {
|
||||
console.log("Sync timed out, continuing");
|
||||
} else {
|
||||
console.log("Sync already in progress");
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Keep track of sync start time
|
||||
await store.set("sync.startTime", Date.now());
|
||||
const snapshot = await store.get("sync.snapshot");
|
||||
console.log("Syncing page", page);
|
||||
try {
|
||||
const { snapshot: newSnapshot, error } = await sync.syncFile(
|
||||
config,
|
||||
snapshot,
|
||||
`${page}.md`,
|
||||
);
|
||||
// Store snapshot
|
||||
await store.set("sync.snapshot", newSnapshot);
|
||||
// Clear sync start time
|
||||
await store.del("sync.startTime");
|
||||
if (error) {
|
||||
console.error("Sync error", error);
|
||||
throw new Error(error);
|
||||
}
|
||||
} catch (e: any) {
|
||||
// Clear sync start time
|
||||
await store.del("sync.startTime");
|
||||
console.error("Sync error", e);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
name: tasks
|
||||
imports:
|
||||
- https://get.silverbullet.md/global.plug.json
|
||||
syntax:
|
||||
DeadlineDate:
|
||||
firstCharacters:
|
||||
@@ -56,7 +54,6 @@ functions:
|
||||
contexts:
|
||||
- DeadlineDate
|
||||
previewTaskToggle:
|
||||
env: client
|
||||
path: ./task.ts:previewTaskToggle
|
||||
events:
|
||||
- preview:click
|
||||
Reference in New Issue
Block a user