diff --git a/cmd/plug_run.ts b/cmd/plug_run.ts index 44529986..529bb886 100644 --- a/cmd/plug_run.ts +++ b/cmd/plug_run.ts @@ -13,7 +13,7 @@ import { LocalShell } from "../server/shell_backend.ts"; import { Hono } from "../server/deps.ts"; import { DataStore } from "$lib/data/datastore.ts"; import { DataStoreMQ } from "$lib/data/mq.datastore.ts"; -import { EventHook } from "$lib/plugos/hooks/event.ts"; +import { EventHook } from "../common/hooks/event.ts"; import { sleep } from "$lib/async.ts"; import { AssetBundle } from "$lib/asset_bundle/bundle.ts"; diff --git a/common/common_system.ts b/common/common_system.ts index 8b4a070f..f4911a17 100644 --- a/common/common_system.ts +++ b/common/common_system.ts @@ -3,13 +3,14 @@ import { PlugNamespaceHook } from "$common/hooks/plug_namespace.ts"; import { SilverBulletHooks } from "./manifest.ts"; import { buildQueryFunctions } from "./query_functions.ts"; import { ScriptEnvironment } from "./space_script.ts"; -import { EventHook } from "../lib/plugos/hooks/event.ts"; +import { EventHook } from "./hooks/event.ts"; import { DataStore } from "$lib/data/datastore.ts"; import { System } from "$lib/plugos/system.ts"; import { CodeWidgetHook } from "../web/hooks/code_widget.ts"; import { PanelWidgetHook } from "../web/hooks/panel_widget.ts"; import { SlashCommandHook } from "../web/hooks/slash_command.ts"; import { DataStoreMQ } from "$lib/data/mq.datastore.ts"; +import { ParseTree } from "$lib/tree.ts"; export abstract class CommonSystem { system!: System; @@ -23,6 +24,7 @@ export abstract class CommonSystem { readonly allKnownPages = new Set(); readonly spaceScriptCommands = new Map(); + scriptEnv: ScriptEnvironment = new ScriptEnvironment(); constructor( protected mq: DataStoreMQ, @@ -42,31 +44,64 @@ export abstract class CommonSystem { this.allKnownPages, this.system, ); - const scriptEnv = new ScriptEnvironment(); if (this.enableSpaceScript) { + this.scriptEnv = new ScriptEnvironment(); try { - await scriptEnv.loadFromSystem(this.system); + await this.scriptEnv.loadFromSystem(this.system); console.log( "Loaded", - Object.keys(scriptEnv.functions).length, + Object.keys(this.scriptEnv.functions).length, "functions and", - Object.keys(scriptEnv.commands).length, + Object.keys(this.scriptEnv.commands).length, "commands from space-script", ); } catch (e: any) { console.error("Error loading space-script:", e.message); } - functions = { ...functions, ...scriptEnv.functions }; + functions = { ...functions, ...this.scriptEnv.functions }; // Reset the space script commands this.spaceScriptCommands.clear(); - for (const [name, command] of Object.entries(scriptEnv.commands)) { + for (const [name, command] of Object.entries(this.scriptEnv.commands)) { this.spaceScriptCommands.set(name, command); } + // Inject the registered events in the event hook + this.eventHook.scriptEnvironment = this.scriptEnv; + this.commandHook.throttledBuildAllCommands(); } // Swap in the expanded function map this.ds.functionMap = functions; } + + invokeSpaceFunction(name: string, args: any[]) { + return this.scriptEnv.functions[name](...args); + } + + async applyAttributeExtractors( + tags: string[], + text: string, + tree: ParseTree, + ): Promise> { + let resultingAttributes: Record = {}; + for (const tag of tags) { + const extractors = this.scriptEnv.attributeExtractors[tag]; + if (!extractors) { + continue; + } + for (const fn of extractors) { + const extractorResult = await fn(text, tree); + if (extractorResult) { + // Merge the attributes in + resultingAttributes = { + ...resultingAttributes, + ...extractorResult, + }; + } + } + } + + return resultingAttributes; + } } diff --git a/lib/plugos/hooks/event.ts b/common/hooks/event.ts similarity index 75% rename from lib/plugos/hooks/event.ts rename to common/hooks/event.ts index f1053c73..734d738f 100644 --- a/lib/plugos/hooks/event.ts +++ b/common/hooks/event.ts @@ -1,5 +1,6 @@ -import type { Hook, Manifest } from "../types.ts"; -import { System } from "../system.ts"; +import type { Hook, Manifest } from "../../lib/plugos/types.ts"; +import { System } from "../../lib/plugos/system.ts"; +import { ScriptEnvironment } from "$common/space_script.ts"; // System events: // - plug:load (plugName: string) @@ -10,7 +11,8 @@ export type EventHookT = { export class EventHook implements Hook { private system?: System; - public localListeners: Map any)[]> = new Map(); + private localListeners: Map any)[]> = new Map(); + public scriptEnvironment?: ScriptEnvironment; addLocalListener(eventName: string, callback: (...args: any[]) => any) { if (!this.localListeners.has(eventName)) { @@ -80,6 +82,8 @@ export class EventHook implements Hook { } } } + + // Local listeners const localListeners = this.localListeners.get(eventName); if (localListeners) { for (const localListener of localListeners) { @@ -93,6 +97,32 @@ export class EventHook implements Hook { } } + // Space script listeners + if (this.scriptEnvironment) { + for ( + const [name, listeners] of Object.entries( + this.scriptEnvironment.eventHandlers, + ) + ) { + if (eventNameToRegex(name).test(eventName)) { + for (const listener of listeners) { + promises.push((async () => { + const result = await Promise.resolve( + listener({ + name: eventName, + // Most events have a single argument, so let's optimize for that, otherwise pass all arguments as an array + data: args.length === 1 ? args[0] : args, + }), + ); + if (result) { + responses.push(result); + } + })()); + } + } + } + } + // Wait for all promises to resolve await Promise.all(promises); diff --git a/common/manifest.ts b/common/manifest.ts index 952a21df..ea5bab37 100644 --- a/common/manifest.ts +++ b/common/manifest.ts @@ -1,6 +1,6 @@ import * as plugos from "../lib/plugos/types.ts"; import { CronHookT } from "../lib/plugos/hooks/cron.ts"; -import { EventHookT } from "../lib/plugos/hooks/event.ts"; +import { EventHookT } from "./hooks/event.ts"; import { CommandHookT } from "./hooks/command.ts"; import { SlashCommandHookT } from "../web/hooks/slash_command.ts"; import { PlugNamespaceHookT } from "./hooks/plug_namespace.ts"; diff --git a/common/space.ts b/common/space.ts index d3047529..107f2657 100644 --- a/common/space.ts +++ b/common/space.ts @@ -2,7 +2,7 @@ import { SpacePrimitives } from "$common/spaces/space_primitives.ts"; import { plugPrefix } from "$common/spaces/constants.ts"; import { AttachmentMeta, FileMeta, PageMeta } from "../type/types.ts"; -import { EventHook } from "../lib/plugos/hooks/event.ts"; +import { EventHook } from "./hooks/event.ts"; import { safeRun } from "../lib/async.ts"; const pageWatchInterval = 5000; diff --git a/common/space_script.ts b/common/space_script.ts index 83da4d4a..e57dfc2d 100644 --- a/common/space_script.ts +++ b/common/space_script.ts @@ -1,4 +1,5 @@ import { System } from "../lib/plugos/system.ts"; +import { ParseTree } from "$lib/tree.ts"; import { ScriptObject } from "../plugs/index/script.ts"; import { AppCommand, CommandDef } from "./hooks/command.ts"; @@ -6,9 +7,24 @@ type FunctionDef = { name: string; }; +type AttributeExtractorDef = { + tags: string[]; +}; + +type EventListenerDef = { + name: string; +}; + +type AttributeExtractorCallback = ( + text: string, + tree: ParseTree, +) => Record | null | Promise | null>; + export class ScriptEnvironment { functions: Record any> = {}; commands: Record = {}; + attributeExtractors: Record = {}; + eventHandlers: Record any)[]> = {}; // Public API @@ -43,23 +59,40 @@ export class ScriptEnvironment { }; } + registerAttributeExtractor( + def: AttributeExtractorDef, + callback: AttributeExtractorCallback, + ) { + for (const tag of def.tags) { + if (!this.attributeExtractors[tag]) { + this.attributeExtractors[tag] = []; + } + this.attributeExtractors[tag].push(callback); + } + } + + registerEventListener( + def: EventListenerDef, + callback: (...args: any[]) => any, + ) { + if (!this.eventHandlers[def.name]) { + this.eventHandlers[def.name] = []; + } + this.eventHandlers[def.name].push(callback); + } + // Internal API evalScript(script: string, system: System) { try { const fn = Function( "silverbullet", "syscall", - "Deno", - "window", - "globalThis", - "self", script, ); fn.call( {}, this, (name: string, ...args: any[]) => system.syscall({}, name, args), - // The rest is explicitly left to be undefined to prevent access to the global scope ); } catch (e: any) { throw new Error( diff --git a/common/spaces/evented_space_primitives.ts b/common/spaces/evented_space_primitives.ts index 5fbfdd1b..bbe13986 100644 --- a/common/spaces/evented_space_primitives.ts +++ b/common/spaces/evented_space_primitives.ts @@ -1,5 +1,5 @@ import { FileMeta } from "../../type/types.ts"; -import { EventHook } from "../../lib/plugos/hooks/event.ts"; +import { EventHook } from "../hooks/event.ts"; import type { SpacePrimitives } from "./space_primitives.ts"; diff --git a/common/syscalls/system.ts b/common/syscalls/system.ts index d2604866..a59f0c5b 100644 --- a/common/syscalls/system.ts +++ b/common/syscalls/system.ts @@ -5,6 +5,7 @@ import { CommandDef } from "../hooks/command.ts"; import { proxySyscall } from "../../web/syscalls/util.ts"; import type { CommonSystem } from "../common_system.ts"; import { version } from "../../version.ts"; +import { ParseTree } from "$lib/tree.ts"; export function systemSyscalls( system: System, @@ -93,6 +94,17 @@ export function systemSyscalls( } } }, + "system.invokeSpaceFunction": (_ctx, name: string, ...args: any[]) => { + return commonSystem.invokeSpaceFunction(name, args); + }, + "system.applyAttributeExtractors": ( + _ctx, + tags: string[], + text: string, + tree: ParseTree, + ): Promise> => { + return commonSystem.applyAttributeExtractors(tags, text, tree); + }, "system.getEnv": () => { return system.env; }, diff --git a/lib/plugos/syscalls/event.ts b/lib/plugos/syscalls/event.ts index 2eb41f24..69f25dd5 100644 --- a/lib/plugos/syscalls/event.ts +++ b/lib/plugos/syscalls/event.ts @@ -1,5 +1,5 @@ import { SysCallMapping } from "../system.ts"; -import { EventHook } from "../hooks/event.ts"; +import { EventHook } from "../../../common/hooks/event.ts"; export function eventSyscalls(eventHook: EventHook): SysCallMapping { return { diff --git a/plug-api/lib/attribute.test.ts b/plug-api/lib/attribute.test.ts index f98e66c2..c8f01ed1 100644 --- a/plug-api/lib/attribute.test.ts +++ b/plug-api/lib/attribute.test.ts @@ -27,7 +27,7 @@ Top level attributes: Deno.test("Test attribute extraction", async () => { const tree = parse(extendedMarkdownLanguage, inlineAttributeSample); - const toplevelAttributes = await extractAttributes(tree, false); + const toplevelAttributes = await extractAttributes(["test"], tree, false); // console.log("All attributes", toplevelAttributes); assertEquals(toplevelAttributes.name, "sup"); assertEquals(toplevelAttributes.age, 42); @@ -35,6 +35,6 @@ Deno.test("Test attribute extraction", async () => { // Check if the attributes are still there assertEquals(renderToText(tree), inlineAttributeSample); // Now once again with cleaning - await extractAttributes(tree, true); + await extractAttributes(["test"], tree, true); assertEquals(renderToText(tree), cleanedInlineAttributeSample); }); diff --git a/plug-api/lib/attribute.ts b/plug-api/lib/attribute.ts index 25d43112..2db47b85 100644 --- a/plug-api/lib/attribute.ts +++ b/plug-api/lib/attribute.ts @@ -1,10 +1,11 @@ import { findNodeOfType, ParseTree, + renderToText, replaceNodesMatchingAsync, } from "$lib/tree.ts"; -import { YAML } from "$sb/syscalls.ts"; +import { system, YAML } from "$sb/syscalls.ts"; /** * Extracts attributes from a tree, optionally cleaning them out of the tree. @@ -13,10 +14,11 @@ import { YAML } from "$sb/syscalls.ts"; * @returns mapping from attribute name to attribute value */ export async function extractAttributes( + tags: string[], tree: ParseTree, clean: boolean, ): Promise> { - const attributes: Record = {}; + let attributes: Record = {}; await replaceNodesMatchingAsync(tree, async (n) => { if (n.type === "ListItem") { // Find top-level only, no nested lists @@ -44,5 +46,15 @@ export async function extractAttributes( // Go on... return undefined; }); + const text = renderToText(tree); + const spaceScriptAttributes = await system.applyAttributeExtractors( + tags, + text, + tree, + ); + attributes = { + ...attributes, + ...spaceScriptAttributes, + }; return attributes; } diff --git a/plug-api/lib/feed.ts b/plug-api/lib/feed.ts index 2bbc97c8..83ba08ac 100644 --- a/plug-api/lib/feed.ts +++ b/plug-api/lib/feed.ts @@ -51,7 +51,7 @@ async function nodesToFeedItem(nodes: ParseTree[]): Promise { const wrapperNode: ParseTree = { children: nodes, }; - const attributes = await extractAttributes(wrapperNode, true); + const attributes = await extractAttributes(["feed"], wrapperNode, true); let id = attributes.id; delete attributes.id; if (!id) { diff --git a/plug-api/lib/syscall_mock.ts b/plug-api/lib/syscall_mock.ts index f42544ca..4470e8bb 100644 --- a/plug-api/lib/syscall_mock.ts +++ b/plug-api/lib/syscall_mock.ts @@ -4,6 +4,8 @@ globalThis.syscall = (name: string, ...args: readonly any[]) => { switch (name) { case "yaml.parse": return Promise.resolve(YAML.load(args[0])); + case "system.applyAttributeExtractors": + return Promise.resolve({}); default: throw Error(`Not implemented in tests: ${name}`); } diff --git a/plug-api/syscalls/system.ts b/plug-api/syscalls/system.ts index 745a181d..362f8c97 100644 --- a/plug-api/syscalls/system.ts +++ b/plug-api/syscalls/system.ts @@ -1,5 +1,6 @@ import type { CommandDef } from "$common/hooks/command.ts"; -import { SyscallMeta } from "$type/types.ts"; +import type { SyscallMeta } from "$type/types.ts"; +import type { ParseTree } from "$lib/tree.ts"; import { syscall } from "../syscall.ts"; export function invokeFunction( @@ -23,8 +24,23 @@ export function listSyscalls(): Promise { return syscall("system.listSyscalls"); } +export function invokeSpaceFunction( + name: string, + ...args: any[] +): Promise { + return syscall("system.invokeSpaceFunction", name, ...args); +} + +export function applyAttributeExtractors( + tags: string[], + text: string, + tree: ParseTree, +): Promise[]> { + return syscall("system.applyAttributeExtractors", tags, text, tree); +} + export function reloadPlugs() { - syscall("system.reloadPlugs"); + return syscall("system.reloadPlugs"); } // Returns what runtime environment this plug is run in, e.g. "server" or "client" can be undefined, which would mean a hybrid environment (such as mobile) diff --git a/plugs/index/item.ts b/plugs/index/item.ts index 46f2d844..bbf7798c 100644 --- a/plugs/index/item.ts +++ b/plugs/index/item.ts @@ -23,7 +23,7 @@ export async function indexItems({ name, tree }: IndexTreeEvent) { const coll = collectNodesOfType(tree, "ListItem"); - for (const n of coll) { + for (let n of coll) { if (!n.children) { continue; } @@ -46,19 +46,21 @@ export async function indexItems({ name, tree }: IndexTreeEvent) { collectNodesOfType(n, "Hashtag").forEach((h) => { // Push tag to the list, removing the initial # tags.add(h.children![0].text!.substring(1)); + h.children = []; }); + // Extract attributes and remove from tree + const extractedAttributes = await extractAttributes( + ["item", ...tags], + n, + true, + ); + for (const child of n.children!.slice(1)) { rewritePageRefs(child, name); if (child.type === "OrderedList" || child.type === "BulletList") { break; } - // Extract attributes and remove from tree - const extractedAttributes = await extractAttributes(child, true); - - for (const [key, value] of Object.entries(extractedAttributes)) { - item[key] = value; - } textNodes.push(child); } @@ -67,6 +69,12 @@ export async function indexItems({ name, tree }: IndexTreeEvent) { item.tags = [...tags]; } + for ( + const [key, value] of Object.entries(extractedAttributes) + ) { + item[key] = value; + } + updateITags(item, frontmatter); items.push(item); diff --git a/plugs/index/page.ts b/plugs/index/page.ts index 2145af1c..14a0bfab 100644 --- a/plugs/index/page.ts +++ b/plugs/index/page.ts @@ -15,7 +15,11 @@ export async function indexPage({ name, tree }: IndexTreeEvent) { } const pageMeta = await space.getPageMeta(name); const frontmatter = await extractFrontmatter(tree); - const toplevelAttributes = await extractAttributes(tree, false); + const toplevelAttributes = await extractAttributes( + ["page", ...frontmatter.tags || []], + tree, + false, + ); // Push them all into the page object // Note the order here, making sure that the actual page meta data overrules diff --git a/plugs/index/paragraph.ts b/plugs/index/paragraph.ts index be774c8c..cb112ceb 100644 --- a/plugs/index/paragraph.ts +++ b/plugs/index/paragraph.ts @@ -35,20 +35,19 @@ export async function indexParagraphs({ name: page, tree }: IndexTreeEvent) { return false; } - const attrs = await extractAttributes(p, true); - const tags = new Set(); - const text = renderToText(p); - // So we're looking at indexable a paragraph now + const tags = new Set(); collectNodesOfType(p, "Hashtag").forEach((tagNode) => { tags.add(tagNode.children![0].text!.substring(1)); // Hacky way to remove the hashtag tagNode.children = []; }); - const textWithoutTags = renderToText(p); + // Extract attributes and remove from tree + const attrs = await extractAttributes(["paragraph", ...tags], p, true); + const text = renderToText(p); - if (!textWithoutTags.trim()) { + if (!text.trim()) { // Empty paragraph, just tags and attributes maybe return true; } diff --git a/plugs/tasks/task.ts b/plugs/tasks/task.ts index 23b4c44f..7c7830b6 100644 --- a/plugs/tasks/task.ts +++ b/plugs/tasks/task.ts @@ -91,17 +91,23 @@ export async function indexTasks({ name, tree }: IndexTreeEvent) { task.tags = []; } task.tags.push(tagName); + tree.children = []; } }); // Extract attributes and remove from tree - const extractedAttributes = await extractAttributes(n, true); + task.name = n.children!.slice(1).map(renderToText).join("").trim(); + const extractedAttributes = await extractAttributes( + ["task", ...task.tags || []], + n, + true, + ); + task.name = n.children!.slice(1).map(renderToText).join("").trim(); + for (const [key, value] of Object.entries(extractedAttributes)) { task[key] = value; } - task.name = n.children!.slice(1).map(renderToText).join("").trim(); - updateITags(task, frontmatter); tasks.push(task); diff --git a/server/instance.ts b/server/instance.ts index 9e200199..885439f3 100644 --- a/server/instance.ts +++ b/server/instance.ts @@ -5,7 +5,7 @@ import { FilteredSpacePrimitives } from "$common/spaces/filtered_space_primitive import { ReadOnlySpacePrimitives } from "$common/spaces/ro_space_primitives.ts"; import { SpacePrimitives } from "$common/spaces/space_primitives.ts"; import { AssetBundle } from "../lib/asset_bundle/bundle.ts"; -import { EventHook } from "../lib/plugos/hooks/event.ts"; +import { EventHook } from "../common/hooks/event.ts"; import { DataStore } from "$lib/data/datastore.ts"; import { KvPrimitives } from "$lib/data/kv_primitives.ts"; import { DataStoreMQ } from "$lib/data/mq.datastore.ts"; diff --git a/server/server_system.ts b/server/server_system.ts index 2cfbc533..7cdcc12b 100644 --- a/server/server_system.ts +++ b/server/server_system.ts @@ -4,7 +4,7 @@ import { EventedSpacePrimitives } from "$common/spaces/evented_space_primitives. import { PlugSpacePrimitives } from "$common/spaces/plug_space_primitives.ts"; import { createSandbox } from "../lib/plugos/sandboxes/web_worker_sandbox.ts"; import { CronHook } from "../lib/plugos/hooks/cron.ts"; -import { EventHook } from "../lib/plugos/hooks/event.ts"; +import { EventHook } from "../common/hooks/event.ts"; import { MQHook } from "../lib/plugos/hooks/mq.ts"; import assetSyscalls from "../lib/plugos/syscalls/asset.ts"; import { eventSyscalls } from "../lib/plugos/syscalls/event.ts"; @@ -75,8 +75,7 @@ export class ServerSystem extends CommonSystem { this.ds = new DataStore(this.kvPrimitives); // Event hook - const eventHook = new EventHook(); - this.system.addHook(eventHook); + this.system.addHook(this.eventHook); // Command hook, just for introspection this.commandHook = new CommandHook( @@ -103,14 +102,14 @@ export class ServerSystem extends CommonSystem { this.spacePrimitives, plugNamespaceHook, ), - eventHook, + this.eventHook, ); - const space = new Space(this.spacePrimitives, eventHook); + const space = new Space(this.spacePrimitives, this.eventHook); // Add syscalls this.system.registerSyscalls( [], - eventSyscalls(eventHook), + eventSyscalls(this.eventHook), spaceReadSyscalls(space), assetSyscalls(this.system), yamlSyscalls(), @@ -151,26 +150,29 @@ export class ServerSystem extends CommonSystem { space.updatePageList().catch(console.error); }, fileListInterval); - eventHook.addLocalListener("file:changed", async (path, localChange) => { - if (!localChange && path.endsWith(".md")) { - const pageName = path.slice(0, -3); - const data = await this.spacePrimitives.readFile(path); - console.log("Outside page change: reindexing", pageName); - // Change made outside of editor, trigger reindex - await eventHook.dispatchEvent("page:index_text", { - name: pageName, - text: new TextDecoder().decode(data.data), - }); - } + this.eventHook.addLocalListener( + "file:changed", + async (path, localChange) => { + if (!localChange && path.endsWith(".md")) { + const pageName = path.slice(0, -3); + const data = await this.spacePrimitives.readFile(path); + console.log("Outside page change: reindexing", pageName); + // Change made outside of editor, trigger reindex + await this.eventHook.dispatchEvent("page:index_text", { + name: pageName, + text: new TextDecoder().decode(data.data), + }); + } - if (path.startsWith(plugPrefix) && path.endsWith(".plug.js")) { - console.log("Plug updated, reloading:", path); - this.system.unload(path); - await this.loadPlugFromSpace(path); - } - }); + if (path.startsWith(plugPrefix) && path.endsWith(".plug.js")) { + console.log("Plug updated, reloading:", path); + this.system.unload(path); + await this.loadPlugFromSpace(path); + } + }, + ); - eventHook.addLocalListener( + this.eventHook.addLocalListener( "file:listed", (allFiles: FileMeta[]) => { // Update list of known pages @@ -189,7 +191,7 @@ export class ServerSystem extends CommonSystem { await indexPromise; } - await eventHook.dispatchEvent("system:ready"); + await this.eventHook.dispatchEvent("system:ready"); } async loadPlugs() { diff --git a/web/client.ts b/web/client.ts index f336328a..4e4776e6 100644 --- a/web/client.ts +++ b/web/client.ts @@ -10,7 +10,7 @@ import { } from "./deps.ts"; import { Space } from "../common/space.ts"; import { FilterOption } from "../type/web.ts"; -import { EventHook } from "../lib/plugos/hooks/event.ts"; +import { EventHook } from "../common/hooks/event.ts"; import { AppCommand } from "$common/hooks/command.ts"; import { PageState, diff --git a/web/client_system.ts b/web/client_system.ts index 5769668b..d8dfbb96 100644 --- a/web/client_system.ts +++ b/web/client_system.ts @@ -1,7 +1,7 @@ import { PlugNamespaceHook } from "$common/hooks/plug_namespace.ts"; import { SilverBulletHooks } from "$common/manifest.ts"; import { CronHook } from "../lib/plugos/hooks/cron.ts"; -import { EventHook } from "../lib/plugos/hooks/event.ts"; +import { EventHook } from "../common/hooks/event.ts"; import { createSandbox } from "../lib/plugos/sandboxes/web_worker_sandbox.ts"; import assetSyscalls from "../lib/plugos/syscalls/asset.ts"; diff --git a/web/sync_service.ts b/web/sync_service.ts index 3dc8f074..5fcc1b49 100644 --- a/web/sync_service.ts +++ b/web/sync_service.ts @@ -2,7 +2,7 @@ import { plugPrefix } from "$common/spaces/constants.ts"; import type { SpacePrimitives } from "$common/spaces/space_primitives.ts"; import { SpaceSync, SyncStatus, SyncStatusItem } from "$common/spaces/sync.ts"; import { sleep } from "$lib/async.ts"; -import { EventHook } from "$lib/plugos/hooks/event.ts"; +import { EventHook } from "../common/hooks/event.ts"; import { DataStore } from "$lib/data/datastore.ts"; import { Space } from "../common/space.ts"; diff --git a/website/Space Script.md b/website/Space Script.md index 04aec725..e2ed4763 100644 --- a/website/Space Script.md +++ b/website/Space Script.md @@ -31,12 +31,12 @@ If you use things like `console.log` in your script, you will see this output ei # Runtime Environment & API Space script is loaded directly in the browser environment on the client, and the Deno environment on the server. -While not very secure, some effort is put into running this code in a clean JavaScript environment, as such the following global variables are not available: `this`, `self`, `Deno`, `window`, and `globalThis`. - Depending on where code is run (client or server), a slightly different JavaScript API will be available. However, code should ideally primarily rely on the following explicitly exposed APIs: -* `silverbullet.registerFunction(definition, callback)`: registers a custom function (see [[#Custom functions]]). -* `silverbullet.registerCommand(definition, callback)`: registers a custom command (see [[#Custom commands]]). +* `silverbullet.registerFunction(def, callback)`: registers a custom function (see [[#Custom functions]]). +* `silverbullet.registerCommand(def, callback)`: registers a custom command (see [[#Custom commands]]). +* `silverbullet.registerEventListener`: registers an event listener (see [[#Custom event listeners]]). +* `silverbullet.registerAttributeExtractor(def, callback)`: registers a custom attribute extractor. * `syscall(name, args...)`: invoke a syscall (see [[#Syscalls]]). Many useful standard JavaScript APIs are available, such as: @@ -51,7 +51,7 @@ Since template rendering happens on the server (except in [[Client Modes#Synced The `silverbullet.registerFunction` API takes two arguments: -* `options`: with currently just one option: +* `def`: with currently just one option: * `name`: the name of the function to register * `callback`: the callback function to invoke (can be `async` or not) @@ -88,7 +88,7 @@ You can run it via the command palette, or by pushing this [[Markdown/Command li The `silverbullet.registerCommand` API takes two arguments: -* `options`: +* `def`: * `name`: Name of the command * `key` (optional): Keyboard shortcut for the command (Windows/Linux) * `mac` (optional): Mac keyboard shortcut for the command @@ -96,6 +96,71 @@ The `silverbullet.registerCommand` API takes two arguments: * `requireMode` (optional): Only make this command available in `ro` or `rw` mode. * `callback`: the callback function to invoke (can be `async` or not) +# Custom event listeners +Various interesting events are triggered on SilverBullet’s central event bus. Space script can listen to these events and do something with them. + +The `silverbullet.registerEventListener` API takes two arguments: + +* `def`, currently just one option: + * `name`: Name of the event. This name can contain `*` as a wildcard. +* `callback`: the callback function to invoke (can be `async` or not). This callback is passed an object with two keys: + * `name`: the name of the event triggered (useful if you use a wildcard event listener) + * `data`: the event data + +To discover what events exist, you can do something like the following to listen to all events and log them to the JavaScript console. Note that different events are triggered on the client and server, so watch both logs: + +```space-script +silverbullet.registerEventListener({name: "*"}, (event) => { + // To avoid excessive logging this line comment it out, uncomment it in your code code to see the event stream + // console.log("Received event in space script:", event); +}); +``` + +# Custom attribute extractors +SilverBullet indexes various types of content as [[Objects]]. There are various ways to define [[Attributes]] for these objects, such as the [attribute: my value] syntax. However, using space script you can write your own code to extract attribute values not natively supported. + +The `silverbullet.registerAttributeExtractor` API takes two arguments: + +* `def`, currently just one option: + * `tags`: Array of tags this extractor should be applied to, could be a built-in tag such as `item`, `page` or `task`, but also any custom tags you define +* `callback`: the callback function to invoke (can be `async` or not). This callback is passed two arguments: + * `text`: the text of the object to extract attributes for + * `tree`: the ParseTree of the object to extract attributes for (you can ignore this one if you don’t need it) + This callback should return an object of attribute mappings. + +## Example +Let’s say you want to use the syntax `✅ 2024-02-27` in a task to signify when that task was completed: + +* [x] I’ve done this ✅ 2024-02-27 + +The following attribute extractor will accomplish this: + +```space-script +silverbullet.registerAttributeExtractor({tags: ["task"]}, (text) => { + // Find the completion date using a regular expression + const completionRegex = /✅\s*(\w{4}-\w{2}-\w{2})/; + const match = completionRegex.exec(text); + if (match) { + // Let's customize the task name by stripping this completion date + // First strip the checkbox bit + let taskName = text.replace(/\[[^\]]+\]\s*/, ""); + // Then remove the completion date and clean it up + taskName = taskName.replace(completionRegex, "").trim(); + return { + name: taskName, + completed: match[1] + }; + } +}); +``` + +Note that built-in attributes can also be overridden (like `name` in this case). + +Result: +```template +{{{task where page = @page.name select name, completed}}} +``` + # Syscalls The primary way to interact with the SilverBullet environment is using “syscalls”. Syscalls expose SilverBullet functionality largely available both on the client and server in a safe way.