New space script APIs (#761)
New space script APIs: registerEventListener and registerAttributeExtractorpull/774/head
parent
a98299e8b3
commit
de2d1089d4
|
@ -13,7 +13,7 @@ import { LocalShell } from "../server/shell_backend.ts";
|
||||||
import { Hono } from "../server/deps.ts";
|
import { Hono } from "../server/deps.ts";
|
||||||
import { DataStore } from "$lib/data/datastore.ts";
|
import { DataStore } from "$lib/data/datastore.ts";
|
||||||
import { DataStoreMQ } from "$lib/data/mq.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 { sleep } from "$lib/async.ts";
|
||||||
import { AssetBundle } from "$lib/asset_bundle/bundle.ts";
|
import { AssetBundle } from "$lib/asset_bundle/bundle.ts";
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,14 @@ import { PlugNamespaceHook } from "$common/hooks/plug_namespace.ts";
|
||||||
import { SilverBulletHooks } from "./manifest.ts";
|
import { SilverBulletHooks } from "./manifest.ts";
|
||||||
import { buildQueryFunctions } from "./query_functions.ts";
|
import { buildQueryFunctions } from "./query_functions.ts";
|
||||||
import { ScriptEnvironment } from "./space_script.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 { DataStore } from "$lib/data/datastore.ts";
|
||||||
import { System } from "$lib/plugos/system.ts";
|
import { System } from "$lib/plugos/system.ts";
|
||||||
import { CodeWidgetHook } from "../web/hooks/code_widget.ts";
|
import { CodeWidgetHook } from "../web/hooks/code_widget.ts";
|
||||||
import { PanelWidgetHook } from "../web/hooks/panel_widget.ts";
|
import { PanelWidgetHook } from "../web/hooks/panel_widget.ts";
|
||||||
import { SlashCommandHook } from "../web/hooks/slash_command.ts";
|
import { SlashCommandHook } from "../web/hooks/slash_command.ts";
|
||||||
import { DataStoreMQ } from "$lib/data/mq.datastore.ts";
|
import { DataStoreMQ } from "$lib/data/mq.datastore.ts";
|
||||||
|
import { ParseTree } from "$lib/tree.ts";
|
||||||
|
|
||||||
export abstract class CommonSystem {
|
export abstract class CommonSystem {
|
||||||
system!: System<SilverBulletHooks>;
|
system!: System<SilverBulletHooks>;
|
||||||
|
@ -23,6 +24,7 @@ export abstract class CommonSystem {
|
||||||
|
|
||||||
readonly allKnownPages = new Set<string>();
|
readonly allKnownPages = new Set<string>();
|
||||||
readonly spaceScriptCommands = new Map<string, AppCommand>();
|
readonly spaceScriptCommands = new Map<string, AppCommand>();
|
||||||
|
scriptEnv: ScriptEnvironment = new ScriptEnvironment();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected mq: DataStoreMQ,
|
protected mq: DataStoreMQ,
|
||||||
|
@ -42,31 +44,64 @@ export abstract class CommonSystem {
|
||||||
this.allKnownPages,
|
this.allKnownPages,
|
||||||
this.system,
|
this.system,
|
||||||
);
|
);
|
||||||
const scriptEnv = new ScriptEnvironment();
|
|
||||||
if (this.enableSpaceScript) {
|
if (this.enableSpaceScript) {
|
||||||
|
this.scriptEnv = new ScriptEnvironment();
|
||||||
try {
|
try {
|
||||||
await scriptEnv.loadFromSystem(this.system);
|
await this.scriptEnv.loadFromSystem(this.system);
|
||||||
console.log(
|
console.log(
|
||||||
"Loaded",
|
"Loaded",
|
||||||
Object.keys(scriptEnv.functions).length,
|
Object.keys(this.scriptEnv.functions).length,
|
||||||
"functions and",
|
"functions and",
|
||||||
Object.keys(scriptEnv.commands).length,
|
Object.keys(this.scriptEnv.commands).length,
|
||||||
"commands from space-script",
|
"commands from space-script",
|
||||||
);
|
);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Error loading space-script:", e.message);
|
console.error("Error loading space-script:", e.message);
|
||||||
}
|
}
|
||||||
functions = { ...functions, ...scriptEnv.functions };
|
functions = { ...functions, ...this.scriptEnv.functions };
|
||||||
|
|
||||||
// Reset the space script commands
|
// Reset the space script commands
|
||||||
this.spaceScriptCommands.clear();
|
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);
|
this.spaceScriptCommands.set(name, command);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inject the registered events in the event hook
|
||||||
|
this.eventHook.scriptEnvironment = this.scriptEnv;
|
||||||
|
|
||||||
this.commandHook.throttledBuildAllCommands();
|
this.commandHook.throttledBuildAllCommands();
|
||||||
}
|
}
|
||||||
// Swap in the expanded function map
|
// Swap in the expanded function map
|
||||||
this.ds.functionMap = functions;
|
this.ds.functionMap = functions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invokeSpaceFunction(name: string, args: any[]) {
|
||||||
|
return this.scriptEnv.functions[name](...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
async applyAttributeExtractors(
|
||||||
|
tags: string[],
|
||||||
|
text: string,
|
||||||
|
tree: ParseTree,
|
||||||
|
): Promise<Record<string, any>> {
|
||||||
|
let resultingAttributes: Record<string, any> = {};
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import type { Hook, Manifest } from "../types.ts";
|
import type { Hook, Manifest } from "../../lib/plugos/types.ts";
|
||||||
import { System } from "../system.ts";
|
import { System } from "../../lib/plugos/system.ts";
|
||||||
|
import { ScriptEnvironment } from "$common/space_script.ts";
|
||||||
|
|
||||||
// System events:
|
// System events:
|
||||||
// - plug:load (plugName: string)
|
// - plug:load (plugName: string)
|
||||||
|
@ -10,7 +11,8 @@ export type EventHookT = {
|
||||||
|
|
||||||
export class EventHook implements Hook<EventHookT> {
|
export class EventHook implements Hook<EventHookT> {
|
||||||
private system?: System<EventHookT>;
|
private system?: System<EventHookT>;
|
||||||
public localListeners: Map<string, ((...args: any[]) => any)[]> = new Map();
|
private localListeners: Map<string, ((...args: any[]) => any)[]> = new Map();
|
||||||
|
public scriptEnvironment?: ScriptEnvironment;
|
||||||
|
|
||||||
addLocalListener(eventName: string, callback: (...args: any[]) => any) {
|
addLocalListener(eventName: string, callback: (...args: any[]) => any) {
|
||||||
if (!this.localListeners.has(eventName)) {
|
if (!this.localListeners.has(eventName)) {
|
||||||
|
@ -80,6 +82,8 @@ export class EventHook implements Hook<EventHookT> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Local listeners
|
||||||
const localListeners = this.localListeners.get(eventName);
|
const localListeners = this.localListeners.get(eventName);
|
||||||
if (localListeners) {
|
if (localListeners) {
|
||||||
for (const localListener of localListeners) {
|
for (const localListener of localListeners) {
|
||||||
|
@ -93,6 +97,32 @@ export class EventHook implements Hook<EventHookT> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Wait for all promises to resolve
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as plugos from "../lib/plugos/types.ts";
|
import * as plugos from "../lib/plugos/types.ts";
|
||||||
import { CronHookT } from "../lib/plugos/hooks/cron.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 { CommandHookT } from "./hooks/command.ts";
|
||||||
import { SlashCommandHookT } from "../web/hooks/slash_command.ts";
|
import { SlashCommandHookT } from "../web/hooks/slash_command.ts";
|
||||||
import { PlugNamespaceHookT } from "./hooks/plug_namespace.ts";
|
import { PlugNamespaceHookT } from "./hooks/plug_namespace.ts";
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { SpacePrimitives } from "$common/spaces/space_primitives.ts";
|
||||||
import { plugPrefix } from "$common/spaces/constants.ts";
|
import { plugPrefix } from "$common/spaces/constants.ts";
|
||||||
|
|
||||||
import { AttachmentMeta, FileMeta, PageMeta } from "../type/types.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";
|
import { safeRun } from "../lib/async.ts";
|
||||||
|
|
||||||
const pageWatchInterval = 5000;
|
const pageWatchInterval = 5000;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { System } from "../lib/plugos/system.ts";
|
import { System } from "../lib/plugos/system.ts";
|
||||||
|
import { ParseTree } from "$lib/tree.ts";
|
||||||
import { ScriptObject } from "../plugs/index/script.ts";
|
import { ScriptObject } from "../plugs/index/script.ts";
|
||||||
import { AppCommand, CommandDef } from "./hooks/command.ts";
|
import { AppCommand, CommandDef } from "./hooks/command.ts";
|
||||||
|
|
||||||
|
@ -6,9 +7,24 @@ type FunctionDef = {
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type AttributeExtractorDef = {
|
||||||
|
tags: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type EventListenerDef = {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AttributeExtractorCallback = (
|
||||||
|
text: string,
|
||||||
|
tree: ParseTree,
|
||||||
|
) => Record<string, any> | null | Promise<Record<string, any> | null>;
|
||||||
|
|
||||||
export class ScriptEnvironment {
|
export class ScriptEnvironment {
|
||||||
functions: Record<string, (...args: any[]) => any> = {};
|
functions: Record<string, (...args: any[]) => any> = {};
|
||||||
commands: Record<string, AppCommand> = {};
|
commands: Record<string, AppCommand> = {};
|
||||||
|
attributeExtractors: Record<string, AttributeExtractorCallback[]> = {};
|
||||||
|
eventHandlers: Record<string, ((...args: any[]) => any)[]> = {};
|
||||||
|
|
||||||
// Public API
|
// 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
|
// Internal API
|
||||||
evalScript(script: string, system: System<any>) {
|
evalScript(script: string, system: System<any>) {
|
||||||
try {
|
try {
|
||||||
const fn = Function(
|
const fn = Function(
|
||||||
"silverbullet",
|
"silverbullet",
|
||||||
"syscall",
|
"syscall",
|
||||||
"Deno",
|
|
||||||
"window",
|
|
||||||
"globalThis",
|
|
||||||
"self",
|
|
||||||
script,
|
script,
|
||||||
);
|
);
|
||||||
fn.call(
|
fn.call(
|
||||||
{},
|
{},
|
||||||
this,
|
this,
|
||||||
(name: string, ...args: any[]) => system.syscall({}, name, args),
|
(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) {
|
} catch (e: any) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { FileMeta } from "../../type/types.ts";
|
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";
|
import type { SpacePrimitives } from "./space_primitives.ts";
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { CommandDef } from "../hooks/command.ts";
|
||||||
import { proxySyscall } from "../../web/syscalls/util.ts";
|
import { proxySyscall } from "../../web/syscalls/util.ts";
|
||||||
import type { CommonSystem } from "../common_system.ts";
|
import type { CommonSystem } from "../common_system.ts";
|
||||||
import { version } from "../../version.ts";
|
import { version } from "../../version.ts";
|
||||||
|
import { ParseTree } from "$lib/tree.ts";
|
||||||
|
|
||||||
export function systemSyscalls(
|
export function systemSyscalls(
|
||||||
system: System<any>,
|
system: System<any>,
|
||||||
|
@ -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<Record<string, any>> => {
|
||||||
|
return commonSystem.applyAttributeExtractors(tags, text, tree);
|
||||||
|
},
|
||||||
"system.getEnv": () => {
|
"system.getEnv": () => {
|
||||||
return system.env;
|
return system.env;
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { SysCallMapping } from "../system.ts";
|
import { SysCallMapping } from "../system.ts";
|
||||||
import { EventHook } from "../hooks/event.ts";
|
import { EventHook } from "../../../common/hooks/event.ts";
|
||||||
|
|
||||||
export function eventSyscalls(eventHook: EventHook): SysCallMapping {
|
export function eventSyscalls(eventHook: EventHook): SysCallMapping {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -27,7 +27,7 @@ Top level attributes:
|
||||||
|
|
||||||
Deno.test("Test attribute extraction", async () => {
|
Deno.test("Test attribute extraction", async () => {
|
||||||
const tree = parse(extendedMarkdownLanguage, inlineAttributeSample);
|
const tree = parse(extendedMarkdownLanguage, inlineAttributeSample);
|
||||||
const toplevelAttributes = await extractAttributes(tree, false);
|
const toplevelAttributes = await extractAttributes(["test"], tree, false);
|
||||||
// console.log("All attributes", toplevelAttributes);
|
// console.log("All attributes", toplevelAttributes);
|
||||||
assertEquals(toplevelAttributes.name, "sup");
|
assertEquals(toplevelAttributes.name, "sup");
|
||||||
assertEquals(toplevelAttributes.age, 42);
|
assertEquals(toplevelAttributes.age, 42);
|
||||||
|
@ -35,6 +35,6 @@ Deno.test("Test attribute extraction", async () => {
|
||||||
// Check if the attributes are still there
|
// Check if the attributes are still there
|
||||||
assertEquals(renderToText(tree), inlineAttributeSample);
|
assertEquals(renderToText(tree), inlineAttributeSample);
|
||||||
// Now once again with cleaning
|
// Now once again with cleaning
|
||||||
await extractAttributes(tree, true);
|
await extractAttributes(["test"], tree, true);
|
||||||
assertEquals(renderToText(tree), cleanedInlineAttributeSample);
|
assertEquals(renderToText(tree), cleanedInlineAttributeSample);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import {
|
import {
|
||||||
findNodeOfType,
|
findNodeOfType,
|
||||||
ParseTree,
|
ParseTree,
|
||||||
|
renderToText,
|
||||||
replaceNodesMatchingAsync,
|
replaceNodesMatchingAsync,
|
||||||
} from "$lib/tree.ts";
|
} 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.
|
* 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
|
* @returns mapping from attribute name to attribute value
|
||||||
*/
|
*/
|
||||||
export async function extractAttributes(
|
export async function extractAttributes(
|
||||||
|
tags: string[],
|
||||||
tree: ParseTree,
|
tree: ParseTree,
|
||||||
clean: boolean,
|
clean: boolean,
|
||||||
): Promise<Record<string, any>> {
|
): Promise<Record<string, any>> {
|
||||||
const attributes: Record<string, any> = {};
|
let attributes: Record<string, any> = {};
|
||||||
await replaceNodesMatchingAsync(tree, async (n) => {
|
await replaceNodesMatchingAsync(tree, async (n) => {
|
||||||
if (n.type === "ListItem") {
|
if (n.type === "ListItem") {
|
||||||
// Find top-level only, no nested lists
|
// Find top-level only, no nested lists
|
||||||
|
@ -44,5 +46,15 @@ export async function extractAttributes(
|
||||||
// Go on...
|
// Go on...
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
|
const text = renderToText(tree);
|
||||||
|
const spaceScriptAttributes = await system.applyAttributeExtractors(
|
||||||
|
tags,
|
||||||
|
text,
|
||||||
|
tree,
|
||||||
|
);
|
||||||
|
attributes = {
|
||||||
|
...attributes,
|
||||||
|
...spaceScriptAttributes,
|
||||||
|
};
|
||||||
return attributes;
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,7 +51,7 @@ async function nodesToFeedItem(nodes: ParseTree[]): Promise<FeedItem> {
|
||||||
const wrapperNode: ParseTree = {
|
const wrapperNode: ParseTree = {
|
||||||
children: nodes,
|
children: nodes,
|
||||||
};
|
};
|
||||||
const attributes = await extractAttributes(wrapperNode, true);
|
const attributes = await extractAttributes(["feed"], wrapperNode, true);
|
||||||
let id = attributes.id;
|
let id = attributes.id;
|
||||||
delete attributes.id;
|
delete attributes.id;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
|
|
|
@ -4,6 +4,8 @@ globalThis.syscall = (name: string, ...args: readonly any[]) => {
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case "yaml.parse":
|
case "yaml.parse":
|
||||||
return Promise.resolve(YAML.load(args[0]));
|
return Promise.resolve(YAML.load(args[0]));
|
||||||
|
case "system.applyAttributeExtractors":
|
||||||
|
return Promise.resolve({});
|
||||||
default:
|
default:
|
||||||
throw Error(`Not implemented in tests: ${name}`);
|
throw Error(`Not implemented in tests: ${name}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import type { CommandDef } from "$common/hooks/command.ts";
|
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";
|
import { syscall } from "../syscall.ts";
|
||||||
|
|
||||||
export function invokeFunction(
|
export function invokeFunction(
|
||||||
|
@ -23,8 +24,23 @@ export function listSyscalls(): Promise<SyscallMeta[]> {
|
||||||
return syscall("system.listSyscalls");
|
return syscall("system.listSyscalls");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function invokeSpaceFunction(
|
||||||
|
name: string,
|
||||||
|
...args: any[]
|
||||||
|
): Promise<any> {
|
||||||
|
return syscall("system.invokeSpaceFunction", name, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyAttributeExtractors(
|
||||||
|
tags: string[],
|
||||||
|
text: string,
|
||||||
|
tree: ParseTree,
|
||||||
|
): Promise<Record<string, any>[]> {
|
||||||
|
return syscall("system.applyAttributeExtractors", tags, text, tree);
|
||||||
|
}
|
||||||
|
|
||||||
export function reloadPlugs() {
|
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)
|
// 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)
|
||||||
|
|
|
@ -23,7 +23,7 @@ export async function indexItems({ name, tree }: IndexTreeEvent) {
|
||||||
|
|
||||||
const coll = collectNodesOfType(tree, "ListItem");
|
const coll = collectNodesOfType(tree, "ListItem");
|
||||||
|
|
||||||
for (const n of coll) {
|
for (let n of coll) {
|
||||||
if (!n.children) {
|
if (!n.children) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -46,19 +46,21 @@ export async function indexItems({ name, tree }: IndexTreeEvent) {
|
||||||
collectNodesOfType(n, "Hashtag").forEach((h) => {
|
collectNodesOfType(n, "Hashtag").forEach((h) => {
|
||||||
// Push tag to the list, removing the initial #
|
// Push tag to the list, removing the initial #
|
||||||
tags.add(h.children![0].text!.substring(1));
|
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)) {
|
for (const child of n.children!.slice(1)) {
|
||||||
rewritePageRefs(child, name);
|
rewritePageRefs(child, name);
|
||||||
if (child.type === "OrderedList" || child.type === "BulletList") {
|
if (child.type === "OrderedList" || child.type === "BulletList") {
|
||||||
break;
|
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);
|
textNodes.push(child);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,6 +69,12 @@ export async function indexItems({ name, tree }: IndexTreeEvent) {
|
||||||
item.tags = [...tags];
|
item.tags = [...tags];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (
|
||||||
|
const [key, value] of Object.entries(extractedAttributes)
|
||||||
|
) {
|
||||||
|
item[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
updateITags(item, frontmatter);
|
updateITags(item, frontmatter);
|
||||||
|
|
||||||
items.push(item);
|
items.push(item);
|
||||||
|
|
|
@ -15,7 +15,11 @@ export async function indexPage({ name, tree }: IndexTreeEvent) {
|
||||||
}
|
}
|
||||||
const pageMeta = await space.getPageMeta(name);
|
const pageMeta = await space.getPageMeta(name);
|
||||||
const frontmatter = await extractFrontmatter(tree);
|
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
|
// Push them all into the page object
|
||||||
// Note the order here, making sure that the actual page meta data overrules
|
// Note the order here, making sure that the actual page meta data overrules
|
||||||
|
|
|
@ -35,20 +35,19 @@ export async function indexParagraphs({ name: page, tree }: IndexTreeEvent) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const attrs = await extractAttributes(p, true);
|
|
||||||
const tags = new Set<string>();
|
|
||||||
const text = renderToText(p);
|
|
||||||
|
|
||||||
// So we're looking at indexable a paragraph now
|
// So we're looking at indexable a paragraph now
|
||||||
|
const tags = new Set<string>();
|
||||||
collectNodesOfType(p, "Hashtag").forEach((tagNode) => {
|
collectNodesOfType(p, "Hashtag").forEach((tagNode) => {
|
||||||
tags.add(tagNode.children![0].text!.substring(1));
|
tags.add(tagNode.children![0].text!.substring(1));
|
||||||
// Hacky way to remove the hashtag
|
// Hacky way to remove the hashtag
|
||||||
tagNode.children = [];
|
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
|
// Empty paragraph, just tags and attributes maybe
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,17 +91,23 @@ export async function indexTasks({ name, tree }: IndexTreeEvent) {
|
||||||
task.tags = [];
|
task.tags = [];
|
||||||
}
|
}
|
||||||
task.tags.push(tagName);
|
task.tags.push(tagName);
|
||||||
|
tree.children = [];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Extract attributes and remove from tree
|
// 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)) {
|
for (const [key, value] of Object.entries(extractedAttributes)) {
|
||||||
task[key] = value;
|
task[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
task.name = n.children!.slice(1).map(renderToText).join("").trim();
|
|
||||||
|
|
||||||
updateITags(task, frontmatter);
|
updateITags(task, frontmatter);
|
||||||
|
|
||||||
tasks.push(task);
|
tasks.push(task);
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { FilteredSpacePrimitives } from "$common/spaces/filtered_space_primitive
|
||||||
import { ReadOnlySpacePrimitives } from "$common/spaces/ro_space_primitives.ts";
|
import { ReadOnlySpacePrimitives } from "$common/spaces/ro_space_primitives.ts";
|
||||||
import { SpacePrimitives } from "$common/spaces/space_primitives.ts";
|
import { SpacePrimitives } from "$common/spaces/space_primitives.ts";
|
||||||
import { AssetBundle } from "../lib/asset_bundle/bundle.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 { DataStore } from "$lib/data/datastore.ts";
|
||||||
import { KvPrimitives } from "$lib/data/kv_primitives.ts";
|
import { KvPrimitives } from "$lib/data/kv_primitives.ts";
|
||||||
import { DataStoreMQ } from "$lib/data/mq.datastore.ts";
|
import { DataStoreMQ } from "$lib/data/mq.datastore.ts";
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { EventedSpacePrimitives } from "$common/spaces/evented_space_primitives.
|
||||||
import { PlugSpacePrimitives } from "$common/spaces/plug_space_primitives.ts";
|
import { PlugSpacePrimitives } from "$common/spaces/plug_space_primitives.ts";
|
||||||
import { createSandbox } from "../lib/plugos/sandboxes/web_worker_sandbox.ts";
|
import { createSandbox } from "../lib/plugos/sandboxes/web_worker_sandbox.ts";
|
||||||
import { CronHook } from "../lib/plugos/hooks/cron.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 { MQHook } from "../lib/plugos/hooks/mq.ts";
|
||||||
import assetSyscalls from "../lib/plugos/syscalls/asset.ts";
|
import assetSyscalls from "../lib/plugos/syscalls/asset.ts";
|
||||||
import { eventSyscalls } from "../lib/plugos/syscalls/event.ts";
|
import { eventSyscalls } from "../lib/plugos/syscalls/event.ts";
|
||||||
|
@ -75,8 +75,7 @@ export class ServerSystem extends CommonSystem {
|
||||||
this.ds = new DataStore(this.kvPrimitives);
|
this.ds = new DataStore(this.kvPrimitives);
|
||||||
|
|
||||||
// Event hook
|
// Event hook
|
||||||
const eventHook = new EventHook();
|
this.system.addHook(this.eventHook);
|
||||||
this.system.addHook(eventHook);
|
|
||||||
|
|
||||||
// Command hook, just for introspection
|
// Command hook, just for introspection
|
||||||
this.commandHook = new CommandHook(
|
this.commandHook = new CommandHook(
|
||||||
|
@ -103,14 +102,14 @@ export class ServerSystem extends CommonSystem {
|
||||||
this.spacePrimitives,
|
this.spacePrimitives,
|
||||||
plugNamespaceHook,
|
plugNamespaceHook,
|
||||||
),
|
),
|
||||||
eventHook,
|
this.eventHook,
|
||||||
);
|
);
|
||||||
const space = new Space(this.spacePrimitives, eventHook);
|
const space = new Space(this.spacePrimitives, this.eventHook);
|
||||||
|
|
||||||
// Add syscalls
|
// Add syscalls
|
||||||
this.system.registerSyscalls(
|
this.system.registerSyscalls(
|
||||||
[],
|
[],
|
||||||
eventSyscalls(eventHook),
|
eventSyscalls(this.eventHook),
|
||||||
spaceReadSyscalls(space),
|
spaceReadSyscalls(space),
|
||||||
assetSyscalls(this.system),
|
assetSyscalls(this.system),
|
||||||
yamlSyscalls(),
|
yamlSyscalls(),
|
||||||
|
@ -151,26 +150,29 @@ export class ServerSystem extends CommonSystem {
|
||||||
space.updatePageList().catch(console.error);
|
space.updatePageList().catch(console.error);
|
||||||
}, fileListInterval);
|
}, fileListInterval);
|
||||||
|
|
||||||
eventHook.addLocalListener("file:changed", async (path, localChange) => {
|
this.eventHook.addLocalListener(
|
||||||
if (!localChange && path.endsWith(".md")) {
|
"file:changed",
|
||||||
const pageName = path.slice(0, -3);
|
async (path, localChange) => {
|
||||||
const data = await this.spacePrimitives.readFile(path);
|
if (!localChange && path.endsWith(".md")) {
|
||||||
console.log("Outside page change: reindexing", pageName);
|
const pageName = path.slice(0, -3);
|
||||||
// Change made outside of editor, trigger reindex
|
const data = await this.spacePrimitives.readFile(path);
|
||||||
await eventHook.dispatchEvent("page:index_text", {
|
console.log("Outside page change: reindexing", pageName);
|
||||||
name: pageName,
|
// Change made outside of editor, trigger reindex
|
||||||
text: new TextDecoder().decode(data.data),
|
await this.eventHook.dispatchEvent("page:index_text", {
|
||||||
});
|
name: pageName,
|
||||||
}
|
text: new TextDecoder().decode(data.data),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (path.startsWith(plugPrefix) && path.endsWith(".plug.js")) {
|
if (path.startsWith(plugPrefix) && path.endsWith(".plug.js")) {
|
||||||
console.log("Plug updated, reloading:", path);
|
console.log("Plug updated, reloading:", path);
|
||||||
this.system.unload(path);
|
this.system.unload(path);
|
||||||
await this.loadPlugFromSpace(path);
|
await this.loadPlugFromSpace(path);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
eventHook.addLocalListener(
|
this.eventHook.addLocalListener(
|
||||||
"file:listed",
|
"file:listed",
|
||||||
(allFiles: FileMeta[]) => {
|
(allFiles: FileMeta[]) => {
|
||||||
// Update list of known pages
|
// Update list of known pages
|
||||||
|
@ -189,7 +191,7 @@ export class ServerSystem extends CommonSystem {
|
||||||
await indexPromise;
|
await indexPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
await eventHook.dispatchEvent("system:ready");
|
await this.eventHook.dispatchEvent("system:ready");
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadPlugs() {
|
async loadPlugs() {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
} from "./deps.ts";
|
} from "./deps.ts";
|
||||||
import { Space } from "../common/space.ts";
|
import { Space } from "../common/space.ts";
|
||||||
import { FilterOption } from "../type/web.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 { AppCommand } from "$common/hooks/command.ts";
|
||||||
import {
|
import {
|
||||||
PageState,
|
PageState,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { PlugNamespaceHook } from "$common/hooks/plug_namespace.ts";
|
import { PlugNamespaceHook } from "$common/hooks/plug_namespace.ts";
|
||||||
import { SilverBulletHooks } from "$common/manifest.ts";
|
import { SilverBulletHooks } from "$common/manifest.ts";
|
||||||
import { CronHook } from "../lib/plugos/hooks/cron.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 { createSandbox } from "../lib/plugos/sandboxes/web_worker_sandbox.ts";
|
||||||
|
|
||||||
import assetSyscalls from "../lib/plugos/syscalls/asset.ts";
|
import assetSyscalls from "../lib/plugos/syscalls/asset.ts";
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { plugPrefix } from "$common/spaces/constants.ts";
|
||||||
import type { SpacePrimitives } from "$common/spaces/space_primitives.ts";
|
import type { SpacePrimitives } from "$common/spaces/space_primitives.ts";
|
||||||
import { SpaceSync, SyncStatus, SyncStatusItem } from "$common/spaces/sync.ts";
|
import { SpaceSync, SyncStatus, SyncStatusItem } from "$common/spaces/sync.ts";
|
||||||
import { sleep } from "$lib/async.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 { DataStore } from "$lib/data/datastore.ts";
|
||||||
import { Space } from "../common/space.ts";
|
import { Space } from "../common/space.ts";
|
||||||
|
|
||||||
|
|
|
@ -31,12 +31,12 @@ If you use things like `console.log` in your script, you will see this output ei
|
||||||
# Runtime Environment & API
|
# Runtime Environment & API
|
||||||
Space script is loaded directly in the browser environment on the client, and the Deno environment on the server.
|
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:
|
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.registerFunction(def, callback)`: registers a custom function (see [[#Custom functions]]).
|
||||||
* `silverbullet.registerCommand(definition, callback)`: registers a custom command (see [[#Custom commands]]).
|
* `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]]).
|
* `syscall(name, args...)`: invoke a syscall (see [[#Syscalls]]).
|
||||||
|
|
||||||
Many useful standard JavaScript APIs are available, such as:
|
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:
|
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
|
* `name`: the name of the function to register
|
||||||
* `callback`: the callback function to invoke (can be `async` or not)
|
* `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:
|
The `silverbullet.registerCommand` API takes two arguments:
|
||||||
|
|
||||||
* `options`:
|
* `def`:
|
||||||
* `name`: Name of the command
|
* `name`: Name of the command
|
||||||
* `key` (optional): Keyboard shortcut for the command (Windows/Linux)
|
* `key` (optional): Keyboard shortcut for the command (Windows/Linux)
|
||||||
* `mac` (optional): Mac keyboard shortcut for the command
|
* `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.
|
* `requireMode` (optional): Only make this command available in `ro` or `rw` mode.
|
||||||
* `callback`: the callback function to invoke (can be `async` or not)
|
* `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
|
# 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.
|
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.
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue