diff --git a/common/markdown_parser/customtags.ts b/common/markdown_parser/customtags.ts index bdc97e0..bafe2ef 100644 --- a/common/markdown_parser/customtags.ts +++ b/common/markdown_parser/customtags.ts @@ -2,6 +2,7 @@ import { Tag } from "../deps.ts"; export const CommandLinkTag = Tag.define(); export const CommandLinkNameTag = Tag.define(); +export const CommandLinkArgsTag = Tag.define(); export const WikiLinkTag = Tag.define(); export const WikiLinkPageTag = Tag.define(); export const CodeInfoTag = Tag.define(); diff --git a/common/markdown_parser/parser.test.ts b/common/markdown_parser/parser.test.ts index e873b37..f7da9a8 100644 --- a/common/markdown_parser/parser.test.ts +++ b/common/markdown_parser/parser.test.ts @@ -121,3 +121,40 @@ Deno.test("Test multi-status tasks", () => { assertEquals(tasks[1].children![0].children![1].text, "x"); assertEquals(tasks[2].children![0].children![1].text, "TODO"); }); + +const commandLinkSample = ` +{[Some: Command]} +{[Other: Command|Alias]} +{[Command: Space | Spaces ]} +`; + +Deno.test("Test command links", () => { + const lang = buildMarkdown([]); + const tree = parse(lang, commandLinkSample); + const commands = collectNodesOfType(tree, "CommandLink"); + console.log("Command links parsed", JSON.stringify(commands, null, 2)); + assertEquals(commands.length, 3); + assertEquals(commands[0].children![1].children![0].text, "Some: Command"); + assertEquals(commands[1].children![1].children![0].text, "Other: Command"); + assertEquals(commands[1].children![3].children![0].text, "Alias"); + assertEquals(commands[2].children![1].children![0].text, "Command: Space "); + assertEquals(commands[2].children![3].children![0].text, " Spaces "); +}); + +const commandLinkArgsSample = ` +{[Args: Command]("with", "args")} +{[Othargs: Command|Args alias]("other", "args", 123)} +`; + +Deno.test("Test command link arguments", () => { + const lang = buildMarkdown([]); + const tree = parse(lang, commandLinkArgsSample); + const commands = collectNodesOfType(tree, "CommandLink"); + assertEquals(commands.length, 2); + + const args1 = findNodeOfType(commands[0], "CommandLinkArgs") + assertEquals(args1!.children![0].text, '"with", "args"'); + + const args2 = findNodeOfType(commands[1], "CommandLinkArgs") + assertEquals(args2!.children![0].text, '"other", "args", 123'); +}); \ No newline at end of file diff --git a/common/markdown_parser/parser.ts b/common/markdown_parser/parser.ts index bf9d3fe..ebcdea5 100644 --- a/common/markdown_parser/parser.ts +++ b/common/markdown_parser/parser.ts @@ -68,13 +68,14 @@ const WikiLink: MarkdownConfig = { ], }; -export const commandLinkRegex = /^\{\[([^\]\|]+)(\|([^\]]+))?\]\}/; +export const commandLinkRegex = /^\{\[([^\]\|]+)(\|([^\]]+))?\](\(([^\)]+)\))?\}/; const CommandLink: MarkdownConfig = { defineNodes: [ { name: "CommandLink", style: { "CommandLink/...": ct.CommandLinkTag } }, { name: "CommandLinkName", style: ct.CommandLinkNameTag }, { name: "CommandLinkAlias", style: ct.CommandLinkNameTag }, + { name: "CommandLinkArgs", style: ct.CommandLinkArgsTag }, { name: "CommandLinkMark", style: t.processingInstruction }, ], parseInline: [ @@ -88,7 +89,7 @@ const CommandLink: MarkdownConfig = { ) { return -1; } - const [fullMatch, command, pipePart, label] = match; + const [fullMatch, command, pipePart, label, argsPart, args] = match; const endPos = pos + fullMatch.length; let aliasElts: any[] = []; @@ -103,11 +104,26 @@ const CommandLink: MarkdownConfig = { ), ]; } + + let argsElts: any[] = []; + if (argsPart) { + const argsStartPos = pos + 2 + command.length + (pipePart?.length ?? 0); + argsElts = [ + cx.elt("CommandLinkMark", argsStartPos, argsStartPos + 2), + cx.elt( + "CommandLinkArgs", + argsStartPos + 2, + argsStartPos + 2 + args.length, + ), + ]; + } + return cx.addElement( cx.elt("CommandLink", pos, endPos, [ cx.elt("CommandLinkMark", pos, pos + 2), cx.elt("CommandLinkName", pos + 2, pos + 2 + command.length), ...aliasElts, + ...argsElts, cx.elt("CommandLinkMark", endPos - 2, endPos), ]), ); diff --git a/plug-api/silverbullet-syscall/system.ts b/plug-api/silverbullet-syscall/system.ts index 9b65623..e78e320 100644 --- a/plug-api/silverbullet-syscall/system.ts +++ b/plug-api/silverbullet-syscall/system.ts @@ -9,8 +9,8 @@ export function invokeFunction( } // Only available on the client -export function invokeCommand(name: string): Promise { - return syscall("system.invokeCommand", name); +export function invokeCommand(name: string, args?: string[]): Promise { + return syscall("system.invokeCommand", name, args); } // Only available on the client diff --git a/plugs/editor/editor.plug.yaml b/plugs/editor/editor.plug.yaml index ddd713f..6273ec1 100644 --- a/plugs/editor/editor.plug.yaml +++ b/plugs/editor/editor.plug.yaml @@ -226,3 +226,10 @@ functions: path: ./upload.ts:uploadFile command: name: "Upload: File" + + customFlashMessage: + path: editor.ts:customFlashMessage + command: + name: "Flash: Custom Message" + contexts: + - internal \ No newline at end of file diff --git a/plugs/editor/editor.ts b/plugs/editor/editor.ts index 5a32502..2d0c401 100644 --- a/plugs/editor/editor.ts +++ b/plugs/editor/editor.ts @@ -50,3 +50,7 @@ export async function moveToPosCommand() { const pos = +posString; await editor.moveCursor(pos); } + +export async function customFlashMessage(_ctx: any, message: string) { + await editor.flashNotification(message); +} diff --git a/plugs/editor/navigate.ts b/plugs/editor/navigate.ts index b1cc1d0..b0828af 100644 --- a/plugs/editor/navigate.ts +++ b/plugs/editor/navigate.ts @@ -87,7 +87,15 @@ async function actionClickOrActionEnter( } case "CommandLink": { const commandName = mdTree.children![1]!.children![0].text!; - await system.invokeCommand(commandName); + const argsNode = findNodeOfType(mdTree, "CommandLinkArgs"); + const argsText = argsNode?.children![0]?.text; + // Assume the arguments are can be parsed as the innards of a valid JSON list + try { + const args = argsText ? JSON.parse(`[${argsText}]`) : []; + await system.invokeCommand(commandName, args); + } catch(e: any) { + await editor.flashNotification(`Error parsing command link arguments: ${e.message}`, "error"); + } break; } } diff --git a/plugs/editor/upload.ts b/plugs/editor/upload.ts index 37c215d..ad7cd63 100644 --- a/plugs/editor/upload.ts +++ b/plugs/editor/upload.ts @@ -43,7 +43,7 @@ async function saveFile(file: UploadFile) { editor.insertAtCursor(attachmentMarkdown); } -export async function uploadFile() { - const uploadFile = await editor.uploadFile(); +export async function uploadFile(_ctx: any, accept?: string, capture?: string) { + const uploadFile = await editor.uploadFile(accept, capture); await saveFile(uploadFile); -} +} \ No newline at end of file diff --git a/web/client.ts b/web/client.ts index f176d30..29a4dea 100644 --- a/web/client.ts +++ b/web/client.ts @@ -879,10 +879,14 @@ export class Client { } } - async runCommandByName(name: string) { + async runCommandByName(name: string, args?: string[]) { const cmd = this.ui.viewState.commands.get(name); if (cmd) { - await cmd.run(); + if (args) { + await cmd.run(args); + } else { + await cmd.run(); + } } else { throw new Error(`Command ${name} not found`); } diff --git a/web/cm_plugins/smart_quotes.ts b/web/cm_plugins/smart_quotes.ts index af9f446..0c1a609 100644 --- a/web/cm_plugins/smart_quotes.ts +++ b/web/cm_plugins/smart_quotes.ts @@ -8,6 +8,7 @@ const straightQuoteContexts = [ "FrontMatterCode", "DirectiveStart", "Attribute", + "CommandLink" ]; // TODO: Add support for selection (put quotes around or create blockquote block?) diff --git a/web/editor_state.ts b/web/editor_state.ts index fb41912..c9cf346 100644 --- a/web/editor_state.ts +++ b/web/editor_state.ts @@ -63,7 +63,7 @@ export function createEditorState( return false; } } - Promise.resolve() + Promise.resolve([]) .then(def.run) .catch((e: any) => { console.error(e); diff --git a/web/hooks/command.ts b/web/hooks/command.ts index 3d31770..eb2bdcd 100644 --- a/web/hooks/command.ts +++ b/web/hooks/command.ts @@ -14,7 +14,7 @@ export type CommandDef = { export type AppCommand = { command: CommandDef; - run: () => Promise; + run: (args?: string[]) => Promise; }; export type CommandHookT = { @@ -43,8 +43,8 @@ export class CommandHook extends EventEmitter const cmd = functionDef.command; this.editorCommands.set(cmd.name, { command: cmd, - run: () => { - return plug.invoke(name, [cmd]); + run: (args?: string[]) => { + return plug.invoke(name, [cmd, ...args??[]]); }, }); } diff --git a/web/syscalls/system.ts b/web/syscalls/system.ts index 5565d0a..d32fa0c 100644 --- a/web/syscalls/system.ts +++ b/web/syscalls/system.ts @@ -50,11 +50,11 @@ export function systemSyscalls( } return plug.invoke(name, args); }, - "system.invokeCommand": (_ctx, name: string) => { + "system.invokeCommand": (_ctx, name: string, args?: string[]) => { if (!client) { throw new Error("Not supported"); } - return client.runCommandByName(name); + return client.runCommandByName(name, args); }, "system.listCommands": (): { [key: string]: CommandDef } => { if (!client) { diff --git a/website/Markdown/Command links.md b/website/Markdown/Command links.md new file mode 100644 index 0000000..5db6441 --- /dev/null +++ b/website/Markdown/Command links.md @@ -0,0 +1,10 @@ +Command links allow you to create buttons in your pages that trigger commands. + +# Basic use +{[Stats: Show]} or {[Open Daily Note]} + +# Aliasing +{[Stats: Show|Show me stats]} + +# Passing arguments +{[Flash: Custom Message|Say hello]("hello there")} \ No newline at end of file diff --git a/website/Markdown/Extensions.md b/website/Markdown/Extensions.md index 0498159..6bfcded 100644 --- a/website/Markdown/Extensions.md +++ b/website/Markdown/Extensions.md @@ -8,7 +8,7 @@ In addition to supporting [[Markdown/Basics|markdown basics]] as standardized by * [[Live Templates]] * [[Anchors]] * Hashtags, e.g. `#mytag`. -* Command link syntax: `{[Stats: Show]}` rendered into a clickable button {[Stats: Show]}. +* [[Markdown/Command links]] syntax * [Tables](https://www.markdownguide.org/extended-syntax/#tables) * [Task lists](https://www.markdownguide.org/extended-syntax/#task-lists) * [Highlight](https://www.markdownguide.org/extended-syntax/#highlight)