diff --git a/cli/plug_run.ts b/cli/plug_run.ts index 84bdb71..4c40249 100644 --- a/cli/plug_run.ts +++ b/cli/plug_run.ts @@ -48,13 +48,7 @@ export async function runPlug( }, app.fetch); if (functionName) { - const [plugName, funcName] = functionName.split("."); - - const plug = serverSystem.system.loadedPlugs.get(plugName); - if (!plug) { - throw new Error(`Plug ${plugName} not found`); - } - const result = await plug.invoke(funcName, args); + const result = await serverSystem.system.invokeFunction(functionName, args); await serverSystem.close(); serverSystem.kvPrimitives.close(); serverController.abort(); diff --git a/common/settings_template.ts b/common/PAGE_TEMPLATES.ts similarity index 63% rename from common/settings_template.ts rename to common/PAGE_TEMPLATES.ts index 24dcb81..53a3b20 100644 --- a/common/settings_template.ts +++ b/common/PAGE_TEMPLATES.ts @@ -6,3 +6,11 @@ A list of built-in settings [[!silverbullet.md/SETTINGS|can be found here]]. indexPage: index \`\`\` `; + +export const INDEX_TEMPLATE = + `Hello! And welcome to your brand new SilverBullet space! + +\`\`\`template +page: "[[!silverbullet.md/Getting Started]]" +\`\`\` +`; diff --git a/common/deps.ts b/common/deps.ts index bc538fb..a04c707 100644 --- a/common/deps.ts +++ b/common/deps.ts @@ -71,7 +71,12 @@ export { Text, Transaction, } from "@codemirror/state"; -export type { ChangeSpec, Extension, StateCommand } from "@codemirror/state"; +export type { + ChangeSpec, + Compartment, + Extension, + StateCommand, +} from "@codemirror/state"; export { codeFolding, defaultHighlightStyle, @@ -132,3 +137,5 @@ export { export { mime } from "https://deno.land/x/mimetypes@v1.0.0/mod.ts"; export { compile as gitIgnoreCompiler } from "https://esm.sh/gitignore-parser@0.0.2"; + +export { z } from "https://deno.land/x/zod@v3.22.4/mod.ts"; diff --git a/common/languages.ts b/common/languages.ts index 566a05c..7888eee 100644 --- a/common/languages.ts +++ b/common/languages.ts @@ -34,6 +34,7 @@ export const builtinLanguages: Record = { "meta": StreamLanguage.define(yamlLanguage), "yaml": StreamLanguage.define(yamlLanguage), "template": StreamLanguage.define(yamlLanguage), + "block": StreamLanguage.define(yamlLanguage), "embed": StreamLanguage.define(yamlLanguage), "data": StreamLanguage.define(yamlLanguage), "toc": StreamLanguage.define(yamlLanguage), diff --git a/common/query_functions.ts b/common/query_functions.ts new file mode 100644 index 0000000..640c334 --- /dev/null +++ b/common/query_functions.ts @@ -0,0 +1,15 @@ +import { FunctionMap } from "$sb/types.ts"; +import { builtinFunctions } from "$sb/lib/builtin_query_functions.ts"; + +export function buildQueryFunctions(allKnownPages: Set): FunctionMap { + return { + ...builtinFunctions, + pageExists: (name: string) => { + if (name.startsWith("!") || name.startsWith("{{")) { + // Let's assume federated pages exist, and ignore template variable ones + return true; + } + return allKnownPages.has(name); + }, + }; +} diff --git a/common/space_index.ts b/common/space_index.ts index 0d40e44..954962e 100644 --- a/common/space_index.ts +++ b/common/space_index.ts @@ -16,7 +16,7 @@ export async function ensureSpaceIndex(ds: DataStore, system: System) { if (currentIndexVersion !== desiredIndexVersion && !indexOngoing) { console.info("Performing a full space reindex, this could take a while..."); indexOngoing = true; - await system.loadedPlugs.get("index")!.invoke("reindexSpace", []); + await system.invokeFunction("index.reindexSpace", []); console.info("Full space index complete."); await markFullSpaceIndexComplete(ds); indexOngoing = false; diff --git a/common/syscalls/handlebar_helpers.ts b/common/syscalls/handlebar_helpers.ts index ff1cbdd..ffc0af0 100644 --- a/common/syscalls/handlebar_helpers.ts +++ b/common/syscalls/handlebar_helpers.ts @@ -40,6 +40,15 @@ export function handlebarHelpers() { nextWeek.setDate(nextWeek.getDate() + 7); return niceDate(nextWeek); }, + weekStart: (startOnMonday = true) => { + const d = new Date(); + const day = d.getDay(); + let diff = d.getDate() - day; + if (startOnMonday) { + diff += day == 0 ? -6 : 1; + } + return niceDate(new Date(d.setDate(diff))); + }, ifEq: function (v1: any, v2: any, options: any) { if (v1 === v2) { return options.fn(this); diff --git a/common/syscalls/handlebars.ts b/common/syscalls/handlebars.ts index e6485d8..6c168e1 100644 --- a/common/syscalls/handlebars.ts +++ b/common/syscalls/handlebars.ts @@ -10,14 +10,22 @@ export function handlebarsSyscalls(): SysCallMapping { obj: any, globals: Record = {}, ): string => { - const templateFn = Handlebars.compile( - template, - { noEscape: true }, - ); - return templateFn(obj, { - helpers: handlebarHelpers(), - data: globals, - }); + return renderHandlebarsTemplate(template, obj, globals); }, }; } + +export function renderHandlebarsTemplate( + template: string, + obj: any, + globals: Record, +) { + const templateFn = Handlebars.compile( + template, + { noEscape: true }, + ); + return templateFn(obj, { + helpers: handlebarHelpers(), + data: globals, + }); +} diff --git a/common/util.ts b/common/util.ts index e2b23a2..17f48d7 100644 --- a/common/util.ts +++ b/common/util.ts @@ -1,8 +1,9 @@ -import { SETTINGS_TEMPLATE } from "./settings_template.ts"; +import { SETTINGS_TEMPLATE } from "./PAGE_TEMPLATES.ts"; import { YAML } from "./deps.ts"; import { SpacePrimitives } from "./spaces/space_primitives.ts"; import { expandPropertyNames } from "$sb/lib/json.ts"; import type { BuiltinSettings } from "../web/types.ts"; +import { INDEX_TEMPLATE } from "./PAGE_TEMPLATES.ts"; /** * Runs a function safely by catching any errors and logging them to the console. @@ -88,14 +89,7 @@ export async function ensureSettingsAndIndex( ); await space.writeFile( "index.md", - new TextEncoder().encode( - `Hello! And welcome to your brand new SilverBullet space! - -\`\`\`template -page: "[[!silverbullet.md/Getting Started]]" -\`\`\` -`, - ), + new TextEncoder().encode(INDEX_TEMPLATE), ); } } diff --git a/import_map.json b/import_map.json index 64513c2..0cce8e6 100644 --- a/import_map.json +++ b/import_map.json @@ -19,6 +19,7 @@ "preact": "https://esm.sh/preact@10.11.1", "$sb/": "./plug-api/", - "handlebars": "https://esm.sh/handlebars@4.7.7?target=es2022" + "handlebars": "https://esm.sh/handlebars@4.7.7?target=es2022", + "zod": "https://deno.land/x/zod@v3.22.4/mod.ts" } } diff --git a/plug-api/lib/builtin_query_functions.ts b/plug-api/lib/builtin_query_functions.ts index dc7644b..0fe8a97 100644 --- a/plug-api/lib/builtin_query_functions.ts +++ b/plug-api/lib/builtin_query_functions.ts @@ -11,6 +11,12 @@ export const builtinFunctions: FunctionMap = { min(...args: number[]) { return Math.min(...args); }, + replace(str: string, match: [string, string] | string, replace: string) { + const matcher = Array.isArray(match) + ? new RegExp(match[0], match[1] + "g") + : match; + return str.replaceAll(matcher, replace); + }, toJSON(obj: any) { return JSON.stringify(obj); }, diff --git a/plug-api/lib/tree.ts b/plug-api/lib/tree.ts index ab4af12..2af75b6 100644 --- a/plug-api/lib/tree.ts +++ b/plug-api/lib/tree.ts @@ -191,7 +191,10 @@ export function nodeAtPos(tree: ParseTree, pos: number): ParseTree | null { } // Turn ParseTree back into text -export function renderToText(tree: ParseTree): string { +export function renderToText(tree?: ParseTree): string { + if (!tree) { + return ""; + } const pieces: string[] = []; if (tree.text !== undefined) { return tree.text; diff --git a/plug-api/silverbullet-syscall/editor.ts b/plug-api/silverbullet-syscall/editor.ts index 372e1b2..5ebaa83 100644 --- a/plug-api/silverbullet-syscall/editor.ts +++ b/plug-api/silverbullet-syscall/editor.ts @@ -39,6 +39,16 @@ export function navigate( return syscall("editor.navigate", name, pos, replaceState, newWindow); } +export function openPageNavigator( + mode: "page" | "template" = "page", +): Promise { + return syscall("editor.openPageNavigator", mode); +} + +export function openCommandPalette(): Promise { + return syscall("editor.openCommandPalette"); +} + export function reloadPage(): Promise { return syscall("editor.reloadPage"); } @@ -47,6 +57,10 @@ export function reloadUI(): Promise { return syscall("editor.reloadUI"); } +export function reloadSettingsAndCommands(): Promise { + return syscall("editor.reloadSettingsAndCommands"); +} + export function openUrl(url: string, existingWindow = false): Promise { return syscall("editor.openUrl", url, existingWindow); } diff --git a/plugos/system.ts b/plugos/system.ts index a4221cf..4a667dc 100644 --- a/plugos/system.ts +++ b/plugos/system.ts @@ -75,6 +75,24 @@ export class System extends EventEmitter> { } } + /** + * Invokes a function named using the "plug.functionName" pattern, for convenience + * @param name name of the function (e.g. plug.doSomething) + * @param args an array of arguments to pass to the function + */ + invokeFunction(name: string, args: any[]): Promise { + const [plugName, functionName] = name.split("."); + if (!functionName) { + // Sanity check + throw new Error(`Missing function name: ${name}`); + } + const plug = this.loadedPlugs.get(plugName); + if (!plug) { + throw new Error(`Plug ${plugName} not found invoking ${name}`); + } + return plug.invoke(functionName, args); + } + localSyscall(name: string, args: any): Promise { return this.syscall({}, name, args); } @@ -90,7 +108,7 @@ export class System extends EventEmitter> { } if (ctx.plug) { // Only when running in a plug context do we check permissions - const plug = this.loadedPlugs.get(ctx.plug!); + const plug = this.loadedPlugs.get(ctx.plug); if (!plug) { throw new Error( `Plug ${ctx.plug} not found while attempting to invoke ${name}}`, diff --git a/plugs/editor/complete.ts b/plugs/editor/complete.ts index ca23d92..2d8dc1b 100644 --- a/plugs/editor/complete.ts +++ b/plugs/editor/complete.ts @@ -17,8 +17,14 @@ export async function pageComplete(completeEvent: CompleteEvent) { /render\s+\[\[|page:\s*["']\[\[/.test( completeEvent.linePrefix, ); - const tagToQuery = isInTemplateContext ? "template" : "page"; - let allPages: PageMeta[] = await queryObjects(tagToQuery, {}, 5); + + // When in a template context, we only want to complete template pages + // When outside of a template context, we want to complete all pages except template pages + let allPages: PageMeta[] = isInTemplateContext + ? await queryObjects("template", {}, 5) + : await queryObjects("page", { + filter: ["!=", ["attr", "tags"], ["string", "template"]], + }, 5); const prefix = match[1]; if (prefix.startsWith("!")) { // Federation prefix, let's first see if we're matching anything from federation that is locally synced diff --git a/plugs/editor/editor.plug.yaml b/plugs/editor/editor.plug.yaml index c4a2965..035d9da 100644 --- a/plugs/editor/editor.plug.yaml +++ b/plugs/editor/editor.plug.yaml @@ -15,6 +15,27 @@ functions: command: name: "Editor: Toggle Dark Mode" + openCommandPalette: + path: editor.ts:openCommandPalette + command: + name: "Open Command Palette" + key: "Ctrl-/" + mac: "Cmd-/" + + openPageNavigator: + path: editor.ts:openPageNavigator + command: + name: "Open Page Navigator" + key: "Ctrl-k" + mac: "Cmd-k" + + openTemplateNavigator: + path: editor.ts:openTemplateNavigator + command: + name: "Open Template Navigator" + key: "Ctrl-Shift-t" + mac: "Cmd-Shift-t" + # Page operations deletePage: path: "./page.ts:deletePage" @@ -35,6 +56,11 @@ functions: events: - editor:complete + reloadSettingsAndCommands: + path: editor.ts:reloadSettingsAndCommands + command: + name: "System: Reload Settings and Commands" + # Navigation linkNavigate: path: "./navigate.ts:linkNavigate" diff --git a/plugs/editor/editor.ts b/plugs/editor/editor.ts index 472268f..6d2f790 100644 --- a/plugs/editor/editor.ts +++ b/plugs/editor/editor.ts @@ -10,6 +10,18 @@ export async function setEditorMode() { } } +export function openCommandPalette() { + return editor.openCommandPalette(); +} + +export async function openPageNavigator() { + await editor.openPageNavigator("page"); +} + +export async function openTemplateNavigator() { + await editor.openPageNavigator("template"); +} + export async function toggleDarkMode() { let darkMode = await clientStore.get("darkMode"); darkMode = !darkMode; @@ -34,3 +46,8 @@ export async function moveToPosCommand() { export async function customFlashMessage(_def: any, message: string) { await editor.flashNotification(message); } + +export async function reloadSettingsAndCommands() { + await editor.reloadSettingsAndCommands(); + await editor.flashNotification("Reloaded settings and commands"); +} diff --git a/plugs/editor/page.ts b/plugs/editor/page.ts index 520a111..ac139c8 100644 --- a/plugs/editor/page.ts +++ b/plugs/editor/page.ts @@ -14,15 +14,20 @@ export async function deletePage() { await space.deletePage(pageName); } -export async function copyPage(_def: any, predefinedNewName: string) { - const oldName = await editor.getCurrentPage(); - let suggestedName = predefinedNewName || oldName; +export async function copyPage( + _def: any, + sourcePage?: string, + toName?: string, +) { + const currentPage = await editor.getCurrentPage(); + const fromName = sourcePage || currentPage; + let suggestedName = toName || fromName; - if (isFederationPath(oldName)) { - const pieces = oldName.split("/"); + if (isFederationPath(fromName)) { + const pieces = fromName.split("/"); suggestedName = pieces.slice(1).join("/"); } - const newName = await editor.prompt(`Copy to new page:`, suggestedName); + const newName = await editor.prompt(`Copy to page:`, suggestedName); if (!newName) { return; @@ -44,11 +49,17 @@ export async function copyPage(_def: any, predefinedNewName: string) { } } - const text = await editor.getText(); + const text = await space.readPage(fromName); console.log("Writing new page to space"); await space.writePage(newName, text); - console.log("Navigating to new page"); - await editor.navigate(newName); + if (currentPage === fromName) { + // If we're copying the current page, navigate there + console.log("Navigating to new page"); + await editor.navigate(newName); + } else { + // Otherwise just notify of success + await editor.flashNotification("Page copied successfully"); + } } diff --git a/plugs/federation/federation.plug.yaml b/plugs/federation/federation.plug.yaml index c7f3460..9e30fd7 100644 --- a/plugs/federation/federation.plug.yaml +++ b/plugs/federation/federation.plug.yaml @@ -28,3 +28,9 @@ functions: pageNamespace: pattern: "!.+" operation: getFileMeta + + # Library management + importLibraryCommand: + path: library.ts:importLibraryCommand + command: + name: "Library: Import" \ No newline at end of file diff --git a/plugs/federation/federation.ts b/plugs/federation/federation.ts index 3aeb766..65637ea 100644 --- a/plugs/federation/federation.ts +++ b/plugs/federation/federation.ts @@ -117,7 +117,7 @@ export async function cacheFileListing(uri: string): Promise { export async function readFile( name: string, -): Promise<{ data: Uint8Array; meta: FileMeta } | undefined> { +): Promise<{ data: Uint8Array; meta: FileMeta }> { const url = federatedPathToUrl(name); console.log("Fetching federated file", url); const r = await nativeFetch(url, { diff --git a/plugs/federation/library.ts b/plugs/federation/library.ts new file mode 100644 index 0000000..e97b6d5 --- /dev/null +++ b/plugs/federation/library.ts @@ -0,0 +1,55 @@ +import { editor, space } from "$sb/syscalls.ts"; +import { cacheFileListing, readFile } from "./federation.ts"; + +export async function importLibraryCommand(_def: any, uri?: string) { + if (!uri) { + uri = await editor.prompt("Import library (federation URL):"); + } + if (!uri) { + return; + } + uri = uri.trim(); + if (!uri.startsWith("!")) { + uri = `!${uri}`; + } + const allTemplates = (await cacheFileListing(uri)).filter((f) => + f.name.endsWith(".md") + ); + if ( + !await editor.confirm( + `You are about to import ${allTemplates.length} templates, want to do this?`, + ) + ) { + return; + } + for (const template of allTemplates) { + // Clean up file path + let pageName = template.name.replace(/\.md$/, ""); + // Remove the federation part + const pieces = pageName.split("/"); + pageName = pieces.slice(1).join("/"); + + // Fetch the file + const buf = (await readFile(template.name)).data; + + try { + // Check if it already exists + await space.getPageMeta(pageName); + + if ( + !await editor.confirm( + `Page ${pageName} already exists, are you sure you want to override it?`, + ) + ) { + continue; + } + } catch { + // Expected + } + + // Write to local space + await space.writePage(pageName, new TextDecoder().decode(buf)); + } + await editor.reloadSettingsAndCommands(); + await editor.flashNotification("Import complete!"); +} diff --git a/plugs/index/api.ts b/plugs/index/api.ts index e8a0a4a..3d941a7 100644 --- a/plugs/index/api.ts +++ b/plugs/index/api.ts @@ -60,7 +60,7 @@ export async function clearIndex(): Promise { console.log("Deleted", allKeys.length, "keys from the index"); } -// ENTITIES API +// OBJECTS API /** * Indexes entities in the data store diff --git a/plugs/index/builtins.ts b/plugs/index/builtins.ts index 0674079..03b80f4 100644 --- a/plugs/index/builtins.ts +++ b/plugs/index/builtins.ts @@ -80,11 +80,7 @@ export const builtins: Record> = { page: "!string", pageName: "string", pos: "!number", - type: "string", - trigger: "string", - where: "string", - priority: "number", - enabled: "boolean", + hooks: "hooksSpec", }, }; diff --git a/plugs/index/index.plug.yaml b/plugs/index/index.plug.yaml index 3eb52c2..c1b0281 100644 --- a/plugs/index/index.plug.yaml +++ b/plugs/index/index.plug.yaml @@ -162,17 +162,17 @@ functions: # Template Widgets renderTemplateWidgetsTop: - path: template_widget.ts:renderTemplateWidgets + path: widget.ts:renderTemplateWidgets env: client panelWidget: top renderTemplateWidgetsBottom: - path: template_widget.ts:renderTemplateWidgets + path: widget.ts:renderTemplateWidgets env: client panelWidget: bottom refreshWidgets: - path: template_widget.ts:refreshWidgets + path: widget.ts:refreshWidgets lintYAML: path: lint.ts:lintYAML diff --git a/plugs/index/toc.ts b/plugs/index/toc.ts index 8dc7d8c..1e5275f 100644 --- a/plugs/index/toc.ts +++ b/plugs/index/toc.ts @@ -41,6 +41,10 @@ export async function widget( return false; }); + if (headers.length === 0) { + return null; + } + if (config.minHeaders && headers.length < config.minHeaders) { // Not enough headers, not showing TOC return null; diff --git a/plugs/index/template_widget.ts b/plugs/index/widget.ts similarity index 71% rename from plugs/index/template_widget.ts rename to plugs/index/widget.ts index a944c54..addbb48 100644 --- a/plugs/index/template_widget.ts +++ b/plugs/index/widget.ts @@ -7,9 +7,9 @@ import { } from "$sb/silverbullet-syscall/mod.ts"; import { parseTreeToAST, renderToText } from "$sb/lib/tree.ts"; import { CodeWidgetContent } from "$sb/types.ts"; -import { loadPageObject } from "../template/template.ts"; +import { loadPageObject } from "../template/page.ts"; import { queryObjects } from "./api.ts"; -import { TemplateObject } from "../template/types.ts"; +import { TemplateObject, WidgetConfig } from "../template/types.ts"; import { expressionToKvQueryExpression } from "$sb/lib/parse-query.ts"; import { evalQueryExpression } from "$sb/lib/query.ts"; import { renderTemplate } from "../template/plug_api.ts"; @@ -24,35 +24,39 @@ export async function renderTemplateWidgets(side: "top" | "bottom"): Promise< CodeWidgetContent | null > { const text = await editor.getText(); - const pageMeta = await loadPageObject(await editor.getCurrentPage()); + let pageMeta = await loadPageObject(await editor.getCurrentPage()); const parsedMd = await markdown.parseMarkdown(text); const frontmatter = await extractFrontmatter(parsedMd); - const allFrontMatterTemplates = await queryObjects( + pageMeta = { ...pageMeta, ...frontmatter }; + + const blockTemplates = await queryObjects( "template", { - // where type = "widget:X" and enabled != false - filter: ["and", ["=", ["attr", "type"], ["string", `widget:${side}`]], [ - "!=", - ["attr", "enabled"], - ["boolean", false], - ]], - orderBy: [{ expr: ["attr", "priority"], desc: false }], + // where hooks.top/bottom exists + filter: ["attr", ["attr", "hooks"], side], + orderBy: [{ + // order by hooks.top/bottom.order asc + expr: ["attr", ["attr", ["attr", "hooks"], side], "order"], + desc: false, + }], }, ); + // console.log(`Found the following ${side} templates`, blockTemplates); const templateBits: string[] = []; // Strategy: walk through all matching templates, evaluate the 'where' expression, and pick the first one that matches - for (const template of allFrontMatterTemplates) { - if (!template.where) { + for (const template of blockTemplates) { + if (!template.hooks) { console.warn( - "Skipping template", + "No hooks specified for template", template.ref, - "because it has no 'where' expression", + "this should never happen", ); continue; } + const blockDef = WidgetConfig.parse(template.hooks[side]!); const exprAST = parseTreeToAST( - await language.parseLanguage("expression", template.where!), + await language.parseLanguage("expression", blockDef.where!), ); const parsedExpression = expressionToKvQueryExpression(exprAST[1]); if (evalQueryExpression(parsedExpression, pageMeta)) { @@ -68,11 +72,11 @@ export async function renderTemplateWidgets(side: "top" | "bottom"): Promise< rewritePageRefs(parsedMarkdown, template.ref); renderedTemplate = renderToText(parsedMarkdown); + // console.log("Rendering template", template.ref, renderedTemplate); templateBits.push(renderedTemplate.trim()); } } const summaryText = templateBits.join("\n"); - // console.log("Rendered", summaryText); return { markdown: summaryText, buttons: [ diff --git a/plugs/markdown/api.ts b/plugs/markdown/api.ts index 6612755..3e351d4 100644 --- a/plugs/markdown/api.ts +++ b/plugs/markdown/api.ts @@ -54,6 +54,7 @@ export async function expandCodeWidgets( // 'not found' is to be expected (no code widget configured for this language) // Every other error should probably be reported if (!e.message.includes("not found")) { + console.trace(); console.error("Error rendering code", e.message); } } diff --git a/plugs/markdown/markdown_render.ts b/plugs/markdown/markdown_render.ts index d8f7f0f..53b9cea 100644 --- a/plugs/markdown/markdown_render.ts +++ b/plugs/markdown/markdown_render.ts @@ -330,6 +330,13 @@ function render( const command = t.children![1].children![0].text!; let commandText = command; const aliasNode = findNodeOfType(t, "CommandLinkAlias"); + const argsNode = findNodeOfType(t, "CommandLinkArgs"); + let args: any = []; + + if (argsNode) { + args = JSON.parse(`[${argsNode.children![0].text!}]`); + } + if (aliasNode) { commandText = aliasNode.children![0].text!; } @@ -337,7 +344,7 @@ function render( return { name: "button", attrs: { - "data-onclick": JSON.stringify(["command", command]), + "data-onclick": JSON.stringify(["command", command, args]), }, body: commandText, }; diff --git a/plugs/query/lint.ts b/plugs/query/lint.ts new file mode 100644 index 0000000..450c05e --- /dev/null +++ b/plugs/query/lint.ts @@ -0,0 +1,91 @@ +import { LintEvent } from "$sb/app_event.ts"; +import { parseQuery } from "$sb/lib/parse-query.ts"; +import { cleanPageRef, resolvePath } from "$sb/lib/resolve.ts"; +import { findNodeOfType, traverseTreeAsync } from "$sb/lib/tree.ts"; +import { events, space } from "$sb/syscalls.ts"; +import { LintDiagnostic } from "$sb/types.ts"; +import { loadPageObject, replaceTemplateVars } from "../template/page.ts"; + +export async function lintQuery( + { name, tree }: LintEvent, +): Promise { + const diagnostics: LintDiagnostic[] = []; + await traverseTreeAsync(tree, async (node) => { + if (node.type === "FencedCode") { + const codeInfo = findNodeOfType(node, "CodeInfo")!; + if (!codeInfo) { + return true; + } + const codeLang = codeInfo.children![0].text!; + if ( + codeLang !== "query" + ) { + return true; + } + const codeText = findNodeOfType(node, "CodeText"); + if (!codeText) { + return true; + } + const bodyText = codeText.children![0].text!; + try { + const pageObject = await loadPageObject(name); + const parsedQuery = await parseQuery( + await replaceTemplateVars(bodyText, pageObject), + ); + + const allSources = await allQuerySources(); + if ( + parsedQuery.querySource && + !allSources.includes(parsedQuery.querySource) + ) { + diagnostics.push({ + from: codeText.from!, + to: codeText.to!, + message: `Unknown query source '${parsedQuery.querySource}'`, + severity: "error", + }); + } + if (parsedQuery.render) { + const templatePage = resolvePath( + name, + cleanPageRef(parsedQuery.render), + ); + try { + await space.getPageMeta(templatePage); + } catch { + diagnostics.push({ + from: codeText.from!, + to: codeText.to!, + message: `Could not resolve template ${templatePage}`, + severity: "error", + }); + } + } + } catch (e: any) { + diagnostics.push({ + from: codeText.from!, + to: codeText.to!, + message: e.message, + severity: "error", + }); + } + } + return false; + }); + return diagnostics; +} + +async function allQuerySources(): Promise { + const allEvents = await events.listEvents(); + + const allSources = allEvents + .filter((eventName) => + eventName.startsWith("query:") && !eventName.includes("*") + ) + .map((source) => source.substring("query:".length)); + + const allObjectTypes: string[] = (await events.dispatchEvent("query_", {})) + .flat(); + + return [...allSources, ...allObjectTypes]; +} diff --git a/plugs/query/query.plug.yaml b/plugs/query/query.plug.yaml index a3feebb..aa9030e 100644 --- a/plugs/query/query.plug.yaml +++ b/plugs/query/query.plug.yaml @@ -1,20 +1,19 @@ name: query functions: queryWidget: - path: query.ts:widget + path: widget.ts:widget codeWidget: query renderMode: markdown + # Query widget buttons + editButton: + path: widget.ts:editButton + lintQuery: - path: query.ts:lintQuery + path: lint.ts:lintQuery events: - editor:lint - templateWidget: - path: template.ts:widget - codeWidget: template - renderMode: markdown - queryComplete: path: complete.ts:queryComplete events: @@ -31,26 +30,3 @@ functions: name: "Live Queries and Templates: Refresh All" key: "Alt-q" - # Query widget buttons - editButton: - path: widget.ts:editButton - - # Slash commands - insertQuery: - redirect: template.insertTemplateText - slashCommand: - name: query - description: Insert a query - value: | - ```query - |^| - ``` - insertUseTemplate: - redirect: template.insertTemplateText - slashCommand: - name: template - description: Use a template - value: | - ```template - page: "[[|^|]]" - ``` diff --git a/plugs/query/query.ts b/plugs/query/query.ts deleted file mode 100644 index db9e056..0000000 --- a/plugs/query/query.ts +++ /dev/null @@ -1,173 +0,0 @@ -import type { LintEvent } from "$sb/app_event.ts"; -import { events, space } from "$sb/syscalls.ts"; -import { findNodeOfType, traverseTreeAsync } from "$sb/lib/tree.ts"; -import { parseQuery } from "$sb/lib/parse-query.ts"; -import { loadPageObject, replaceTemplateVars } from "../template/template.ts"; -import { cleanPageRef, resolvePath } from "$sb/lib/resolve.ts"; -import { - CodeWidgetContent, - LintDiagnostic, - PageMeta, - Query, -} from "$sb/types.ts"; -import { jsonToMDTable, renderQueryTemplate } from "../template/util.ts"; - -export async function widget( - bodyText: string, - pageName: string, -): Promise { - const pageObject = await loadPageObject(pageName); - try { - let resultMarkdown = ""; - const parsedQuery = await parseQuery( - await replaceTemplateVars(bodyText, pageObject), - ); - - const results = await performQuery( - parsedQuery, - pageObject, - ); - if (results.length === 0 && !parsedQuery.renderAll) { - resultMarkdown = "No results"; - } else { - if (parsedQuery.render) { - // Configured a custom rendering template, let's use it! - const templatePage = resolvePath(pageName, parsedQuery.render); - const rendered = await renderQueryTemplate( - pageObject, - templatePage, - results, - parsedQuery.renderAll!, - ); - resultMarkdown = rendered.trim(); - } else { - // TODO: At this point it's a bit pointless to first render a markdown table, and then convert that to HTML - // We should just render the HTML table directly - resultMarkdown = jsonToMDTable(results); - } - } - - return { - markdown: resultMarkdown, - buttons: [ - { - description: "Edit", - svg: - ``, - invokeFunction: "query.editButton", - }, - { - description: "Reload", - svg: - ``, - invokeFunction: "query.refreshAllWidgets", - }, - ], - }; - } catch (e: any) { - return { markdown: `**Error:** ${e.message}` }; - } -} - -export async function performQuery(parsedQuery: Query, pageObject: PageMeta) { - if (!parsedQuery.limit) { - parsedQuery.limit = ["number", 1000]; - } - - const eventName = `query:${parsedQuery.querySource}`; - // console.log("Parsed query", parsedQuery); - // Let's dispatch an event and see what happens - const results = await events.dispatchEvent( - eventName, - { query: parsedQuery, pageName: pageObject.name }, - 30 * 1000, - ); - if (results.length === 0) { - throw new Error(`Unsupported query source '${parsedQuery.querySource}'`); - } - return results.flat(); -} - -export async function lintQuery( - { name, tree }: LintEvent, -): Promise { - const diagnostics: LintDiagnostic[] = []; - await traverseTreeAsync(tree, async (node) => { - if (node.type === "FencedCode") { - const codeInfo = findNodeOfType(node, "CodeInfo")!; - if (!codeInfo) { - return true; - } - const codeLang = codeInfo.children![0].text!; - if ( - codeLang !== "query" - ) { - return true; - } - const codeText = findNodeOfType(node, "CodeText"); - if (!codeText) { - return true; - } - const bodyText = codeText.children![0].text!; - try { - const pageObject = await loadPageObject(name); - const parsedQuery = await parseQuery( - await replaceTemplateVars(bodyText, pageObject), - ); - - const allSources = await allQuerySources(); - if ( - parsedQuery.querySource && - !allSources.includes(parsedQuery.querySource) - ) { - diagnostics.push({ - from: codeText.from!, - to: codeText.to!, - message: `Unknown query source '${parsedQuery.querySource}'`, - severity: "error", - }); - } - if (parsedQuery.render) { - const templatePage = resolvePath( - name, - cleanPageRef(parsedQuery.render), - ); - try { - await space.getPageMeta(templatePage); - } catch { - diagnostics.push({ - from: codeText.from!, - to: codeText.to!, - message: `Could not resolve template ${templatePage}`, - severity: "error", - }); - } - } - } catch (e: any) { - diagnostics.push({ - from: codeText.from!, - to: codeText.to!, - message: e.message, - severity: "error", - }); - } - } - return false; - }); - return diagnostics; -} - -async function allQuerySources(): Promise { - const allEvents = await events.listEvents(); - - const allSources = allEvents - .filter((eventName) => - eventName.startsWith("query:") && !eventName.includes("*") - ) - .map((source) => source.substring("query:".length)); - - const allObjectTypes: string[] = (await events.dispatchEvent("query_", {})) - .flat(); - - return [...allSources, ...allObjectTypes]; -} diff --git a/plugs/query/widget.ts b/plugs/query/widget.ts index 2b3fe48..a3b496d 100644 --- a/plugs/query/widget.ts +++ b/plugs/query/widget.ts @@ -1,4 +1,85 @@ -import { codeWidget, editor } from "$sb/syscalls.ts"; +import { codeWidget, editor, events } from "$sb/syscalls.ts"; +import { parseQuery } from "$sb/lib/parse-query.ts"; +import { loadPageObject, replaceTemplateVars } from "../template/page.ts"; +import { resolvePath } from "$sb/lib/resolve.ts"; +import { CodeWidgetContent, PageMeta, Query } from "$sb/types.ts"; +import { jsonToMDTable, renderQueryTemplate } from "../template/util.ts"; + +export async function widget( + bodyText: string, + pageName: string, +): Promise { + const pageObject = await loadPageObject(pageName); + try { + let resultMarkdown = ""; + const parsedQuery = await parseQuery( + await replaceTemplateVars(bodyText, pageObject), + ); + + const results = await performQuery( + parsedQuery, + pageObject, + ); + if (results.length === 0 && !parsedQuery.renderAll) { + resultMarkdown = "No results"; + } else { + if (parsedQuery.render) { + // Configured a custom rendering template, let's use it! + const templatePage = resolvePath(pageName, parsedQuery.render); + const rendered = await renderQueryTemplate( + pageObject, + templatePage, + results, + parsedQuery.renderAll!, + ); + resultMarkdown = rendered.trim(); + } else { + // TODO: At this point it's a bit pointless to first render a markdown table, and then convert that to HTML + // We should just render the HTML table directly + resultMarkdown = jsonToMDTable(results); + } + } + + return { + markdown: resultMarkdown, + buttons: [ + { + description: "Edit", + svg: + ``, + invokeFunction: "query.editButton", + }, + { + description: "Reload", + svg: + ``, + invokeFunction: "query.refreshAllWidgets", + }, + ], + }; + } catch (e: any) { + return { markdown: `**Error:** ${e.message}` }; + } +} + +export async function performQuery(parsedQuery: Query, pageObject: PageMeta) { + if (!parsedQuery.limit) { + parsedQuery.limit = ["number", 1000]; + } + + const eventName = `query:${parsedQuery.querySource}`; + // console.log("Parsed query", parsedQuery); + // Let's dispatch an event and see what happens + const results = await events.dispatchEvent( + eventName, + { query: parsedQuery, pageName: pageObject.name }, + 30 * 1000, + ); + if (results.length === 0) { + throw new Error(`Unsupported query source '${parsedQuery.querySource}'`); + } + return results.flat(); +} export function refreshAllWidgets() { codeWidget.refreshAll(); diff --git a/plugs/tasks/tasks.plug.yaml b/plugs/tasks/tasks.plug.yaml index cd857fc..7d8fc6e 100644 --- a/plugs/tasks/tasks.plug.yaml +++ b/plugs/tasks/tasks.plug.yaml @@ -23,15 +23,6 @@ functions: updateTaskState: path: task.ts:updateTaskState - - turnIntoTask: - redirect: template.applyLineReplace - slashCommand: - name: task - description: Turn into task - match: "^(\\s*)[\\-\\*]?\\s*(\\[[ xX]\\])?\\s*" - replace: "$1* [ ] " - indexTasks: path: "./task.ts:indexTasks" events: diff --git a/plugs/template/complete.ts b/plugs/template/complete.ts deleted file mode 100644 index fca95e6..0000000 --- a/plugs/template/complete.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { CompleteEvent, SlashCompletion } from "$sb/app_event.ts"; -import { PageMeta } from "$sb/types.ts"; -import { editor, events, markdown, space } from "$sb/syscalls.ts"; -import type { - AttributeCompleteEvent, - AttributeCompletion, -} from "../index/attributes.ts"; -import { queryObjects } from "../index/plug_api.ts"; -import { TemplateObject } from "./types.ts"; -import { loadPageObject } from "./template.ts"; -import { renderTemplate } from "./api.ts"; -import { prepareFrontmatterDispatch } from "$sb/lib/frontmatter.ts"; -import { buildHandebarOptions } from "./util.ts"; - -export async function templateVariableComplete(completeEvent: CompleteEvent) { - const match = /\{\{([\w@]*)$/.exec(completeEvent.linePrefix); - if (!match) { - return null; - } - - const handlebarOptions = buildHandebarOptions({ name: "" } as PageMeta); - let allCompletions: any[] = Object.keys(handlebarOptions.helpers).map( - (name) => ({ label: name, detail: "helper" }), - ); - allCompletions = allCompletions.concat( - Object.keys(handlebarOptions.data).map((key) => ({ - label: `@${key}`, - detail: "global variable", - })), - ); - - const completions = (await events.dispatchEvent( - `attribute:complete:_`, - { - source: "", - prefix: match[1], - } as AttributeCompleteEvent, - )).flat() as AttributeCompletion[]; - - allCompletions = allCompletions.concat( - attributeCompletionsToCMCompletion(completions), - ); - - return { - from: completeEvent.pos - match[1].length, - options: allCompletions, - }; -} - -export async function templateSlashComplete( - completeEvent: CompleteEvent, -): Promise { - const allTemplates = await queryObjects("template", { - // Only return templates that have a trigger and are not expliclty disabled - filter: ["and", ["attr", "trigger"], ["!=", ["attr", "enabled"], [ - "boolean", - false, - ]]], - }, 5); - return allTemplates.map((template) => ({ - label: template.trigger!, - detail: "template", - templatePage: template.ref, - pageName: completeEvent.pageName, - invoke: "template.insertSlashTemplate", - })); -} - -export async function insertSlashTemplate(slashCompletion: SlashCompletion) { - const pageObject = await loadPageObject(slashCompletion.pageName); - - const templateText = await space.readPage(slashCompletion.templatePage); - let { renderedFrontmatter, text } = await renderTemplate( - templateText, - pageObject, - ); - - let cursorPos = await editor.getCursor(); - - if (renderedFrontmatter) { - renderedFrontmatter = renderedFrontmatter.trim(); - const pageText = await editor.getText(); - const tree = await markdown.parseMarkdown(pageText); - - const dispatch = await prepareFrontmatterDispatch( - tree, - renderedFrontmatter, - ); - if (cursorPos === 0) { - dispatch.selection = { anchor: renderedFrontmatter.length + 9 }; - } - await editor.dispatch(dispatch); - } - - cursorPos = await editor.getCursor(); - const carretPos = text.indexOf("|^|"); - text = text.replace("|^|", ""); - await editor.insertAtCursor(text); - if (carretPos !== -1) { - await editor.moveCursor(cursorPos + carretPos); - } -} - -export function attributeCompletionsToCMCompletion( - completions: AttributeCompletion[], -) { - return completions.map( - (completion) => ({ - label: completion.name, - detail: `${completion.attributeType} (${completion.source})`, - type: "attribute", - }), - ); -} diff --git a/plugs/template/lint.ts b/plugs/template/lint.ts new file mode 100644 index 0000000..e39fb99 --- /dev/null +++ b/plugs/template/lint.ts @@ -0,0 +1,43 @@ +import { LintEvent } from "$sb/app_event.ts"; +import { LintDiagnostic } from "$sb/types.ts"; +import { findNodeOfType } from "$sb/lib/tree.ts"; +import { FrontmatterConfig } from "./types.ts"; +import { extractFrontmatter } from "$sb/lib/frontmatter.ts"; + +export async function lintTemplateFrontmatter( + { tree }: LintEvent, +): Promise { + const diagnostics: LintDiagnostic[] = []; + const frontmatter = await extractFrontmatter(tree); + + // Just looking this up again for the purposes of error reporting + const frontmatterNode = findNodeOfType(tree, "FrontMatterCode")!; + if (!frontmatter.tags?.includes("template")) { + return []; + } + try { + // Just parse to make sure it's valid + FrontmatterConfig.parse(frontmatter); + } catch (e: any) { + if (e.message.startsWith("[")) { // We got a zod error + const zodErrors = JSON.parse(e.message); + for (const zodError of zodErrors) { + console.log("Zod validation error", zodError); + diagnostics.push({ + from: frontmatterNode.from!, + to: frontmatterNode.to!, + message: `Attribute ${zodError.path.join(".")}: ${zodError.message}`, + severity: "error", + }); + } + } else { + diagnostics.push({ + from: frontmatterNode.from!, + to: frontmatterNode.to!, + message: e.message, + severity: "error", + }); + } + } + return diagnostics; +} diff --git a/plugs/template/page.ts b/plugs/template/page.ts new file mode 100644 index 0000000..82eaae1 --- /dev/null +++ b/plugs/template/page.ts @@ -0,0 +1,226 @@ +import { editor, handlebars, space } from "$sb/syscalls.ts"; +import { PageMeta } from "$sb/types.ts"; +import { getObjectByRef, queryObjects } from "../index/plug_api.ts"; +import { FrontmatterConfig, TemplateObject } from "./types.ts"; +import { renderTemplate } from "./api.ts"; + +export async function newPageCommand( + _cmdDef: any, + templateName?: string, + askName = true, +) { + if (!templateName) { + const allPageTemplates = await listPageTemplates(); + // console.log("All page templates", allPageTemplates); + const selectedTemplate = await selectPageTemplate(allPageTemplates); + + if (!selectedTemplate) { + return; + } + templateName = selectedTemplate.ref; + } + console.log("Selected template", templateName); + + await instantiatePageTemplate(templateName!, undefined, askName); +} + +function listPageTemplates() { + return queryObjects("template", { + // where hooks.newPage exists + filter: ["attr", ["attr", "hooks"], "newPage"], + }); +} + +// Invoked when a new page is created +export async function newPage(pageName: string) { + console.log("Asked to setup a new page for", pageName); + const allPageTemplatesMatchingPrefix = (await listPageTemplates()).filter( + (templateObject) => { + const forPrefix = templateObject.hooks?.newPage?.forPrefix; + return forPrefix && pageName.startsWith(forPrefix); + }, + ); + // console.log("Matching templates", allPageTemplatesMatchingPrefix); + if (allPageTemplatesMatchingPrefix.length === 0) { + // No matching templates, that's ok, we'll just start with an empty page, so let's just return + return; + } + if (allPageTemplatesMatchingPrefix.length === 1) { + // Only one matching template, let's use it + await instantiatePageTemplate( + allPageTemplatesMatchingPrefix[0].ref, + pageName, + false, + ); + } else { + // Let's offer a choice + const selectedTemplate = await selectPageTemplate( + allPageTemplatesMatchingPrefix, + ); + + if (!selectedTemplate) { + // No choice made? We'll start out empty + return; + } + + await instantiatePageTemplate( + selectedTemplate.ref, + pageName, + false, + ); + } +} + +function selectPageTemplate(options: TemplateObject[]) { + return editor.filterBox( + "Page template", + options.map((templateObj) => { + const niceName = templateObj.ref.split("/").pop()!; + return { + ...templateObj, + description: templateObj.description || templateObj.ref, + name: templateObj.displayName || niceName, + }; + }), + `Select the template to create a new page from`, + ); +} + +async function instantiatePageTemplate( + templateName: string, + intoCurrentPage: string | undefined, + askName: boolean, +) { + const templateText = await space.readPage(templateName!); + + console.log( + "Instantiating page template", + templateName, + intoCurrentPage, + askName, + ); + + const tempPageMeta: PageMeta = { + tag: "page", + ref: "", + name: "", + created: "", + lastModified: "", + perm: "rw", + }; + // Just used to extract the frontmatter + const { frontmatter } = await renderTemplate( + templateText, + tempPageMeta, + ); + + let frontmatterConfig: FrontmatterConfig; + try { + frontmatterConfig = FrontmatterConfig.parse(frontmatter!); + } catch (e: any) { + await editor.flashNotification( + `Error parsing template frontmatter for ${templateName}: ${e.message}`, + ); + return; + } + const newPageConfig = frontmatterConfig.hooks!.newPage!; + + let pageName: string | undefined = intoCurrentPage || + await replaceTemplateVars( + newPageConfig.suggestedName || "", + tempPageMeta, + ); + + if (!intoCurrentPage && askName && newPageConfig.confirmName !== false) { + pageName = await editor.prompt( + "Name of new page", + await replaceTemplateVars( + newPageConfig.suggestedName || "", + tempPageMeta, + ), + ); + if (!pageName) { + return; + } + } + tempPageMeta.name = pageName; + + if (!intoCurrentPage) { + // Check if page exists, but only if we're not forcing the name (which only happens when we know that we're creating a new page already) + try { + // Fails if doesn't exist + await space.getPageMeta(pageName); + + // So, page exists + if (newPageConfig.openIfExists) { + console.log("Page already exists, navigating there"); + await editor.navigate(pageName); + return; + } + + // let's warn + if ( + !await editor.confirm( + `Page ${pageName} already exists, are you sure you want to override it?`, + ) + ) { + // Just navigate there without instantiating + return editor.navigate(pageName); + } + } catch { + // The preferred scenario, let's keep going + } + } + + const { text: pageText, renderedFrontmatter } = await renderTemplate( + templateText, + tempPageMeta, + ); + let fullPageText = renderedFrontmatter + ? "---\n" + renderedFrontmatter + "---\n" + pageText + : pageText; + const carretPos = fullPageText.indexOf("|^|"); + fullPageText = fullPageText.replace("|^|", ""); + if (intoCurrentPage) { + await editor.insertAtCursor(fullPageText); + if (carretPos !== -1) { + await editor.moveCursor(carretPos); + } + } else { + await space.writePage( + pageName, + fullPageText, + ); + await editor.navigate(pageName, carretPos !== -1 ? carretPos : undefined); + } +} + +export async function loadPageObject(pageName?: string): Promise { + if (!pageName) { + return { + ref: "", + name: "", + tags: ["page"], + lastModified: "", + created: "", + } as PageMeta; + } + return (await getObjectByRef( + pageName, + "page", + pageName, + )) || { + ref: pageName, + name: pageName, + tags: ["page"], + lastModified: "", + created: "", + } as PageMeta; +} + +export function replaceTemplateVars( + s: string, + pageMeta: PageMeta, +): Promise { + return handlebars.renderTemplate(s, {}, { page: pageMeta }); +} diff --git a/plugs/template/snippet.ts b/plugs/template/snippet.ts new file mode 100644 index 0000000..d372437 --- /dev/null +++ b/plugs/template/snippet.ts @@ -0,0 +1,155 @@ +import { CompleteEvent, SlashCompletion } from "$sb/app_event.ts"; +import { editor, markdown, space } from "$sb/syscalls.ts"; +import type { AttributeCompletion } from "../index/attributes.ts"; +import { queryObjects } from "../index/plug_api.ts"; +import { TemplateObject } from "./types.ts"; +import { loadPageObject } from "./page.ts"; +import { renderTemplate } from "./api.ts"; +import { prepareFrontmatterDispatch } from "$sb/lib/frontmatter.ts"; +import { SnippetConfig } from "./types.ts"; +import { snippet } from "@codemirror/autocomplete"; + +export async function snippetSlashComplete( + completeEvent: CompleteEvent, +): Promise { + const allTemplates = await queryObjects("template", { + // where hooks.snippet.slashCommand exists + filter: ["attr", ["attr", ["attr", "hooks"], "snippet"], "slashCommand"], + }, 5); + return allTemplates.map((template) => { + const snippetTemplate = template.hooks!.snippet!; + + return { + label: snippetTemplate.slashCommand, + detail: template.description, + templatePage: template.ref, + pageName: completeEvent.pageName, + invoke: "template.insertSnippetTemplate", + }; + }); +} + +export async function insertSnippetTemplate(slashCompletion: SlashCompletion) { + const pageObject = await loadPageObject( + slashCompletion.pageName, + ); + + const templateText = await space.readPage(slashCompletion.templatePage); + let { renderedFrontmatter, text: replacementText, frontmatter } = + await renderTemplate( + templateText, + pageObject, + ); + let snippetTemplate: SnippetConfig; + try { + snippetTemplate = SnippetConfig.parse(frontmatter.hooks!.snippet!); + } catch (e: any) { + console.error( + `Invalid template configuration for ${slashCompletion.templatePage}:`, + e.message, + ); + await editor.flashNotification( + `Invalid template configuration for ${slashCompletion.templatePage}, won't insert snippet`, + "error", + ); + return; + } + + let cursorPos = await editor.getCursor(); + + if (renderedFrontmatter) { + renderedFrontmatter = renderedFrontmatter.trim(); + const pageText = await editor.getText(); + const tree = await markdown.parseMarkdown(pageText); + + const dispatch = await prepareFrontmatterDispatch( + tree, + renderedFrontmatter, + ); + if (cursorPos === 0) { + dispatch.selection = { anchor: renderedFrontmatter.length + 9 }; + } + await editor.dispatch(dispatch); + // update cursor position + cursorPos = await editor.getCursor(); + } + + if (snippetTemplate.insertAt) { + switch (snippetTemplate.insertAt) { + case "page-start": + await editor.moveCursor(0); + break; + case "page-end": + await editor.moveCursor((await editor.getText()).length); + break; + case "line-start": { + const pageText = await editor.getText(); + let startOfLine = cursorPos; + while (startOfLine > 0 && pageText[startOfLine - 1] !== "\n") { + startOfLine--; + } + await editor.moveCursor(startOfLine); + break; + } + case "line-end": { + const pageText = await editor.getText(); + let endOfLine = cursorPos; + while (endOfLine < pageText.length && pageText[endOfLine] !== "\n") { + endOfLine++; + } + await editor.moveCursor(endOfLine); + break; + } + default: + // Deliberate no-op + } + } + + cursorPos = await editor.getCursor(); + + if (snippetTemplate.matchRegex) { + const pageText = await editor.getText(); + // Regex matching mode + const matchRegex = new RegExp(snippetTemplate.matchRegex); + + let startOfLine = cursorPos; + while (startOfLine > 0 && pageText[startOfLine - 1] !== "\n") { + startOfLine--; + } + let currentLine = pageText.slice(startOfLine, cursorPos); + const emptyLine = !currentLine; + currentLine = currentLine.replace(matchRegex, replacementText); + + await editor.dispatch({ + changes: { + from: startOfLine, + to: cursorPos, + insert: currentLine, + }, + selection: emptyLine + ? { + anchor: startOfLine + currentLine.length, + } + : undefined, + }); + } else { + const carretPos = replacementText.indexOf("|^|"); + replacementText = replacementText.replace("|^|", ""); + await editor.insertAtCursor(replacementText); + if (carretPos !== -1) { + await editor.moveCursor(cursorPos + carretPos); + } + } +} + +export function attributeCompletionsToCMCompletion( + completions: AttributeCompletion[], +) { + return completions.map( + (completion) => ({ + label: completion.name, + detail: `${completion.attributeType} (${completion.source})`, + type: "attribute", + }), + ); +} diff --git a/plugs/template/template.plug.yaml b/plugs/template/template.plug.yaml index 29ba000..cf66eae 100644 --- a/plugs/template/template.plug.yaml +++ b/plugs/template/template.plug.yaml @@ -3,135 +3,51 @@ functions: # API renderTemplate: path: api.ts:renderTemplate + cleanTemplate: path: api.ts:cleanTemplate - # Used by various slash commands - insertTemplateText: - path: template.ts:insertTemplateText - + # Indexing indexTemplate: - path: ./index.ts:indexTemplate + path: index.ts:indexTemplate events: # Special event only triggered for template pages - page:indexTemplate # Completion templateSlashCommand: - path: ./complete.ts:templateSlashComplete + path: snippet.ts:snippetSlashComplete events: - slash:complete - insertSlashTemplate: - path: ./complete.ts:insertSlashTemplate + insertSnippetTemplate: + path: snippet.ts:insertSnippetTemplate handlebarHelperComplete: - path: ./complete.ts:templateVariableComplete + path: var.ts:templateVariableComplete events: - editor:complete - applyLineReplace: - path: ./template.ts:applyLineReplace - insertFrontMatter: - redirect: insertTemplateText - slashCommand: - name: frontmatter - description: Insert page frontmatter - value: | - --- - |^| - --- - makeH1: - redirect: applyLineReplace - slashCommand: - name: h1 - description: Turn line into h1 header - match: "^#*\\s*" - replace: "# " - makeH2: - redirect: applyLineReplace - slashCommand: - name: h2 - description: Turn line into h2 header - match: "^#*\\s*" - replace: "## " - makeH3: - redirect: applyLineReplace - slashCommand: - name: h3 - description: Turn line into h3 header - match: "^#*\\s*" - replace: "### " - makeH4: - redirect: applyLineReplace - slashCommand: - name: h4 - description: Turn line into h4 header - match: "^#*\\s*" - replace: "#### " - insertCodeBlock: - redirect: insertTemplateText - slashCommand: - name: code - description: Insert fenced code block - value: | - ```|^| - - ``` + # Widget + templateWidget: # Legacy + path: template_block.ts:widget + codeWidget: template + renderMode: markdown - insertHRTemplate: - redirect: insertTemplateText - slashCommand: - name: hr - description: Insert a horizontal rule - value: "---" - - insertTable: - redirect: insertTemplateText - slashCommand: - name: table - description: Insert a table - boost: -1 # Low boost because it's likely not very commonly used - value: | - | Header A | Header B | - |----------|----------| - | Cell A|^| | Cell B | - - quickNoteCommand: - path: ./template.ts:quickNoteCommand - command: - name: "Quick Note" - key: "Alt-Shift-n" - priority: 3 - - dailyNoteCommand: - path: ./template.ts:dailyNoteCommand - command: - name: "Open Daily Note" - key: "Alt-Shift-d" - - weeklyNoteCommand: - path: ./template.ts:weeklyNoteCommand - command: - name: "Open Weekly Note" - key: "Alt-Shift-w" + # API invoked when a new page is created + newPage: + path: page.ts:newPage + # Commands newPageCommand: - path: ./template.ts:newPageCommand + path: page.ts:newPageCommand command: name: "Page: From Template" key: "Alt-Shift-t" - insertTodayCommand: - path: "./template.ts:insertTemplateText" - slashCommand: - name: today - description: Insert today's date - value: "{{today}}" - insertTomorrowCommand: - path: "./template.ts:insertTemplateText" - slashCommand: - name: tomorrow - description: Insert tomorrow's date - value: "{{tomorrow}}" + # Lint + lintTemplateFrontmatter: + path: lint.ts:lintTemplateFrontmatter + events: + - editor:lint diff --git a/plugs/template/template.ts b/plugs/template/template.ts deleted file mode 100644 index c6d3b0b..0000000 --- a/plugs/template/template.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { editor, handlebars, space } from "$sb/syscalls.ts"; -import { niceDate, niceTime } from "$sb/lib/dates.ts"; -import { readSettings } from "$sb/lib/settings_page.ts"; -import { cleanPageRef } from "$sb/lib/resolve.ts"; -import { PageMeta } from "$sb/types.ts"; -import { getObjectByRef, queryObjects } from "../index/plug_api.ts"; -import { TemplateObject } from "./types.ts"; -import { renderTemplate } from "./api.ts"; - -export async function newPageCommand( - _cmdDef: any, - templateName?: string, - askName = true, -) { - if (!templateName) { - const allPageTemplates = await queryObjects("template", { - // Only return templates that have a trigger - filter: ["=", ["attr", "type"], ["string", "page"]], - }); - const selectedTemplate = await editor.filterBox( - "Page template", - allPageTemplates - .map((pageMeta) => ({ - ...pageMeta, - name: pageMeta.displayName || pageMeta.ref, - })), - `Select the template to create a new page from (listing any page tagged with #template and 'page' set as 'type')`, - ); - - if (!selectedTemplate) { - return; - } - templateName = selectedTemplate.ref; - } - console.log("Selected template", templateName); - - const templateText = await space.readPage(templateName!); - - const tempPageMeta: PageMeta = { - tag: "page", - ref: "", - name: "", - created: "", - lastModified: "", - perm: "rw", - }; - // Just used to extract the frontmatter - const { frontmatter } = await renderTemplate( - templateText, - tempPageMeta, - ); - - let pageName: string | undefined = await replaceTemplateVars( - frontmatter?.pageName || "", - tempPageMeta, - ); - - if (askName) { - pageName = await editor.prompt( - "Name of new page", - await replaceTemplateVars(frontmatter?.pageName || "", tempPageMeta), - ); - if (!pageName) { - return; - } - } - tempPageMeta.name = pageName; - - try { - // Fails if doesn't exist - await space.getPageMeta(pageName); - - // So, page exists, let's warn - if ( - !await editor.confirm( - `Page ${pageName} already exists, are you sure you want to override it?`, - ) - ) { - // Just navigate there without instantiating - return editor.navigate(pageName); - } - } catch { - // The preferred scenario, let's keep going - } - - const { text: pageText, renderedFrontmatter } = await renderTemplate( - templateText, - tempPageMeta, - ); - let fullPageText = renderedFrontmatter - ? "---\n" + renderedFrontmatter + "---\n" + pageText - : pageText; - const carretPos = fullPageText.indexOf("|^|"); - fullPageText = fullPageText.replace("|^|", ""); - await space.writePage( - pageName, - fullPageText, - ); - await editor.navigate(pageName, carretPos !== -1 ? carretPos : undefined); -} - -export async function loadPageObject(pageName?: string): Promise { - if (!pageName) { - return { - ref: "", - name: "", - tags: ["page"], - lastModified: "", - created: "", - } as PageMeta; - } - return (await getObjectByRef( - pageName, - "page", - pageName, - )) || { - ref: pageName, - name: pageName, - tags: ["page"], - lastModified: "", - created: "", - } as PageMeta; -} - -export function replaceTemplateVars( - s: string, - pageMeta: PageMeta, -): Promise { - return handlebars.renderTemplate(s, {}, { page: pageMeta }); -} - -export async function quickNoteCommand() { - const { quickNotePrefix } = await readSettings({ - quickNotePrefix: "📥 ", - }); - const date = niceDate(new Date()); - const time = niceTime(new Date()); - const pageName = `${quickNotePrefix}${date} ${time}`; - await editor.navigate(pageName); -} - -export async function dailyNoteCommand() { - const { dailyNoteTemplate, dailyNotePrefix } = await readSettings({ - dailyNoteTemplate: "[[template/page/Daily Note]]", - dailyNotePrefix: "📅 ", - }); - const date = niceDate(new Date()); - const pageName = `${dailyNotePrefix}${date}`; - let carretPos = 0; - - try { - await space.getPageMeta(pageName); - } catch { - // Doesn't exist, let's create - let dailyNoteTemplateText = ""; - try { - dailyNoteTemplateText = await space.readPage( - cleanPageRef(dailyNoteTemplate), - ); - carretPos = dailyNoteTemplateText.indexOf("|^|"); - if (carretPos === -1) { - carretPos = 0; - } - dailyNoteTemplateText = dailyNoteTemplateText.replace("|^|", ""); - } catch { - console.warn(`No daily note template found at ${dailyNoteTemplate}`); - } - - await space.writePage( - pageName, - await replaceTemplateVars(dailyNoteTemplateText, { - tag: "page", - ref: pageName, - name: pageName, - created: "", - lastModified: "", - perm: "rw", - }), - ); - } - await editor.navigate(pageName, carretPos); -} - -function getWeekStartDate(monday = false) { - const d = new Date(); - const day = d.getDay(); - let diff = d.getDate() - day; - if (monday) { - diff += day == 0 ? -6 : 1; - } - return new Date(d.setDate(diff)); -} - -export async function weeklyNoteCommand() { - const { weeklyNoteTemplate, weeklyNotePrefix, weeklyNoteMonday } = - await readSettings({ - weeklyNoteTemplate: "[[template/page/Weekly Note]]", - weeklyNotePrefix: "🗓️ ", - weeklyNoteMonday: false, - }); - let weeklyNoteTemplateText = ""; - try { - weeklyNoteTemplateText = await space.readPage( - cleanPageRef(weeklyNoteTemplate), - ); - } catch { - console.warn(`No weekly note template found at ${weeklyNoteTemplate}`); - } - const date = niceDate(getWeekStartDate(weeklyNoteMonday)); - const pageName = `${weeklyNotePrefix}${date}`; - if (weeklyNoteTemplateText) { - try { - await space.getPageMeta(pageName); - } catch { - // Doesn't exist, let's create - await space.writePage( - pageName, - await replaceTemplateVars(weeklyNoteTemplateText, { - name: pageName, - ref: pageName, - tag: "page", - created: "", - lastModified: "", - perm: "rw", - }), - ); - } - await editor.navigate(pageName); - } else { - await editor.navigate(pageName); - } -} - -export async function insertTemplateText(cmdDef: any) { - const cursorPos = await editor.getCursor(); - const page = await editor.getCurrentPage(); - const pageMeta = await loadPageObject(page); - let templateText: string = cmdDef.value; - const carretPos = templateText.indexOf("|^|"); - templateText = templateText.replace("|^|", ""); - templateText = await replaceTemplateVars(templateText, pageMeta); - await editor.insertAtCursor(templateText); - if (carretPos !== -1) { - await editor.moveCursor(cursorPos + carretPos); - } -} - -export async function applyLineReplace(cmdDef: any) { - const cursorPos = await editor.getCursor(); - const text = await editor.getText(); - const matchRegex = new RegExp(cmdDef.match); - let startOfLine = cursorPos; - while (startOfLine > 0 && text[startOfLine - 1] !== "\n") { - startOfLine--; - } - let currentLine = text.slice(startOfLine, cursorPos); - - const emptyLine = !currentLine; - - currentLine = currentLine.replace(matchRegex, cmdDef.replace); - - await editor.dispatch({ - changes: { - from: startOfLine, - to: cursorPos, - insert: currentLine, - }, - selection: emptyLine - ? { - anchor: startOfLine + currentLine.length, - } - : undefined, - }); -} diff --git a/plugs/query/template.ts b/plugs/template/template_block.ts similarity index 86% rename from plugs/query/template.ts rename to plugs/template/template_block.ts index 8cad01b..e0cee27 100644 --- a/plugs/query/template.ts +++ b/plugs/template/template_block.ts @@ -1,13 +1,13 @@ import { markdown, space, YAML } from "$sb/syscalls.ts"; -import { loadPageObject, replaceTemplateVars } from "../template/template.ts"; +import { loadPageObject, replaceTemplateVars } from "./page.ts"; import { CodeWidgetContent, PageMeta } from "$sb/types.ts"; -import { renderTemplate } from "../template/plug_api.ts"; +import { renderTemplate } from "./plug_api.ts"; import { renderToText } from "$sb/lib/tree.ts"; import { rewritePageRefs, rewritePageRefsInString } from "$sb/lib/resolve.ts"; -import { performQuery } from "./query.ts"; +import { performQuery } from "../query/widget.ts"; import { parseQuery } from "$sb/lib/parse-query.ts"; -type TemplateConfig = { +type TemplateWidgetConfig = { // Pull the template from a page page?: string; // Or use a string directly @@ -29,7 +29,7 @@ export async function widget( const pageMeta: PageMeta = await loadPageObject(pageName); try { - const config: TemplateConfig = await YAML.parse(bodyText); + const config: TemplateWidgetConfig = await YAML.parse(bodyText); let templateText = config.template || ""; let templatePage = config.page; if (templatePage) { @@ -41,7 +41,13 @@ export async function widget( if (!templatePage) { throw new Error("No template page specified"); } - templateText = await space.readPage(templatePage); + try { + templateText = await space.readPage(templatePage); + } catch (e: any) { + if (e.message === "Not found") { + throw new Error(`Template page ${templatePage} not found`); + } + } } let value: any; diff --git a/plugs/template/types.ts b/plugs/template/types.ts index da74c8b..80022da 100644 --- a/plugs/template/types.ts +++ b/plugs/template/types.ts @@ -1,17 +1,100 @@ import { ObjectValue } from "$sb/types.ts"; +import { z, ZodEffects } from "zod"; -export type TemplateFrontmatter = { - displayName?: string; - type?: "page"; +export const CommandConfig = z.object({ + command: z.string().optional(), + key: z.string().optional(), + mac: z.string().optional(), +}); + +export type CommandConfig = z.infer; + +/** + * Used for creating new pages using {[Page: From Template]} command + */ +export const NewPageConfig = refineCommand( + z.object({ + // Suggested name for the new page, can use template placeholders + suggestedName: z.string().optional(), + // Suggest (or auto use) this template for a specific prefix + forPrefix: z.string().optional(), + // Confirm the name before creating + confirmName: z.boolean().optional(), + // If the page already exists, open it instead of creating a new one + openIfExists: z.boolean().optional(), + }).strict().merge(CommandConfig), +); + +export type NewPageConfig = z.infer; + +/** + * Represents a snippet + */ + +export const SnippetConfig = refineCommand( + z.object({ + slashCommand: z.string(), // trigger + // Regex match to apply (implicitly makes the body the regex replacement) + matchRegex: z.string().optional(), + insertAt: z.enum([ + "cursor", + "line-start", + "line-end", + "page-start", + "page-end", + ]).optional(), // defaults to cursor + }).strict().merge(CommandConfig), +); + +/** + * Ensures that 'command' is present if either 'key' or 'mac' is present for a particular object + * @param o object to 'refine' with this constraint + * @returns + */ +function refineCommand(o: T): ZodEffects { + return o.refine((data) => { + // Check if either 'key' or 'mac' is present + const hasKeyOrMac = data.key !== undefined || data.mac !== undefined; + // Ensure 'command' is present if either 'key' or 'mac' is present + return !hasKeyOrMac || data.command !== undefined; + }, { + message: + "Attribute 'command' is required when specifying a key binding via 'key' and/or 'mac'.", + }); +} + +export type SnippetConfig = z.infer; + +export const WidgetConfig = z.object({ + where: z.string(), + priority: z.number().optional(), +}); + +export type WidgetConfig = z.infer; + +export const HooksConfig = z.object({ + top: WidgetConfig.optional(), + bottom: WidgetConfig.optional(), + newPage: NewPageConfig.optional(), + snippet: SnippetConfig.optional(), +}).strict(); + +export type HooksConfig = z.infer; + +export const FrontmatterConfig = z.object({ + // Used for matching in page navigator + displayName: z.string().optional(), + tags: z.union([z.string(), z.array(z.string())]).optional(), + + // For use in the template selector slash commands and other avenues + + description: z.string().optional(), // Frontmatter can be encoded as an object (in which case we'll serialize it) or as a string - frontmatter?: Record | string; + frontmatter: z.union([z.record(z.unknown()), z.string()]).optional(), - // Specific for slash templates - trigger?: string; + hooks: HooksConfig.optional(), +}); - // Specific for frontmatter templates - where?: string; // expression (SB query style) - priority?: number; // When multiple templates match, the one with the highest priority is used -}; +export type FrontmatterConfig = z.infer; -export type TemplateObject = ObjectValue; +export type TemplateObject = ObjectValue; diff --git a/plugs/template/var.ts b/plugs/template/var.ts new file mode 100644 index 0000000..35939db --- /dev/null +++ b/plugs/template/var.ts @@ -0,0 +1,44 @@ +import { CompleteEvent } from "$sb/app_event.ts"; +import { PageMeta } from "$sb/types.ts"; +import { events } from "$sb/syscalls.ts"; +import { buildHandebarOptions } from "./util.ts"; +import { + AttributeCompleteEvent, + AttributeCompletion, +} from "../index/attributes.ts"; +import { attributeCompletionsToCMCompletion } from "./snippet.ts"; + +export async function templateVariableComplete(completeEvent: CompleteEvent) { + const match = /\{\{([\w@]*)$/.exec(completeEvent.linePrefix); + if (!match) { + return null; + } + + const handlebarOptions = buildHandebarOptions({ name: "" } as PageMeta); + let allCompletions: any[] = Object.keys(handlebarOptions.helpers).map( + (name) => ({ label: name, detail: "helper" }), + ); + allCompletions = allCompletions.concat( + Object.keys(handlebarOptions.data).map((key) => ({ + label: `@${key}`, + detail: "global variable", + })), + ); + + const completions = (await events.dispatchEvent( + `attribute:complete:_`, + { + source: "", + prefix: match[1], + } as AttributeCompleteEvent, + )).flat() as AttributeCompletion[]; + + allCompletions = allCompletions.concat( + attributeCompletionsToCMCompletion(completions), + ); + + return { + from: completeEvent.pos - match[1].length, + options: allCompletions, + }; +} diff --git a/server/http_server.ts b/server/http_server.ts index 81a0195..58f29ea 100644 --- a/server/http_server.ts +++ b/server/http_server.ts @@ -405,7 +405,7 @@ export class HttpServer { const args: string[] = body; try { const result = await spaceServer.system!.syscall( - { plug: plugName }, + { plug: plugName === "_" ? undefined : plugName }, syscall, args, ); diff --git a/server/server_system.ts b/server/server_system.ts index 28d97d1..fd15b84 100644 --- a/server/server_system.ts +++ b/server/server_system.ts @@ -34,6 +34,8 @@ import { KVPrimitivesManifestCache } from "../plugos/manifest_cache.ts"; import { KvPrimitives } from "../plugos/lib/kv_primitives.ts"; import { ShellBackend } from "./shell_backend.ts"; import { ensureSpaceIndex } from "../common/space_index.ts"; +import { FileMeta } from "$sb/types.ts"; +import { buildQueryFunctions } from "../common/query_functions.ts"; // // Important: load this before the actual plugs // import { @@ -59,6 +61,7 @@ export class ServerSystem { // denoKv!: Deno.Kv; listInterval?: number; ds!: DataStore; + allKnownPages = new Set(); constructor( private baseSpacePrimitives: SpacePrimitives, @@ -69,7 +72,10 @@ export class ServerSystem { // Always needs to be invoked right after construction async init(awaitIndex = false) { - this.ds = new DataStore(this.kvPrimitives); + this.ds = new DataStore( + this.kvPrimitives, + buildQueryFunctions(this.allKnownPages), + ); this.system = new System( "server", @@ -177,6 +183,19 @@ export class ServerSystem { } }); + eventHook.addLocalListener( + "file:listed", + (allFiles: FileMeta[]) => { + // Update list of known pages + this.allKnownPages.clear(); + allFiles.forEach((f) => { + if (f.name.endsWith(".md")) { + this.allKnownPages.add(f.name.slice(0, -3)); + } + }); + }, + ); + // Ensure a valid index const indexPromise = ensureSpaceIndex(this.ds, this.system); if (awaitIndex) { diff --git a/web/client.ts b/web/client.ts index a2b062e..bfdd32f 100644 --- a/web/client.ts +++ b/web/client.ts @@ -1,5 +1,6 @@ // Third party web dependencies import { + Compartment, CompletionContext, CompletionResult, EditorView, @@ -52,6 +53,8 @@ import { markFullSpaceIndexComplete, } from "../common/space_index.ts"; import { LimitedMap } from "$sb/lib/limited_map.ts"; +import { renderHandlebarsTemplate } from "../common/syscalls/handlebars.ts"; +import { buildQueryFunctions } from "../common/query_functions.ts"; const frontMatterRegex = /^---\n(([^\n]|\n)*?)---\n/; const autoSaveInterval = 1000; @@ -71,6 +74,8 @@ declare global { export class Client { system!: ClientSystem; editorView!: EditorView; + keyHandlerCompartment?: Compartment; + private pageNavigator!: PathPageNavigator; private dbPrefix: string; @@ -136,7 +141,10 @@ export class Client { `${this.dbPrefix}_state`, ); await stateKvPrimitives.init(); - this.stateDataStore = new DataStore(stateKvPrimitives); + this.stateDataStore = new DataStore( + stateKvPrimitives, + buildQueryFunctions(this.allKnownPages), + ); // Setup message queue this.mq = new DataStoreMQ(this.stateDataStore); @@ -190,8 +198,7 @@ export class Client { await this.system.init(); - // Load settings - this.settings = await ensureSettingsAndIndex(localSpacePrimitives); + await this.loadSettings(); await this.loadCaches(); // Pinging a remote space to ensure we're authenticated properly, if not will result in a redirect to auth page @@ -240,6 +247,10 @@ export class Client { this.updatePageListCache().catch(console.error); } + async loadSettings() { + this.settings = await ensureSettingsAndIndex(this.space.spacePrimitives); + } + private async initSync() { this.syncService.start(); @@ -303,7 +314,7 @@ export class Client { private initNavigator() { this.pageNavigator = new PathPageNavigator( - cleanPageRef(this.settings.indexPage), + cleanPageRef(renderHandlebarsTemplate(this.settings.indexPage, {}, {})), ); this.pageNavigator.subscribe( @@ -478,7 +489,12 @@ export class Client { new EventedSpacePrimitives( // Using fallback space primitives here to allow (by default) local reads to "fall through" to HTTP when files aren't synced yet new FallbackSpacePrimitives( - new DataStoreSpacePrimitives(new DataStore(spaceKvPrimitives)), + new DataStoreSpacePrimitives( + new DataStore( + spaceKvPrimitives, + buildQueryFunctions(this.allKnownPages), + ), + ), this.plugSpaceRemotePrimitives, ), this.eventHook, @@ -487,7 +503,7 @@ export class Client { // Run when a list of files has been retrieved async () => { if (!this.settings) { - this.settings = await ensureSettingsAndIndex(localSpacePrimitives!); + await this.loadSettings(); } if (typeof this.settings?.spaceIgnore === "string") { @@ -547,11 +563,12 @@ export class Client { "file:listed", (allFiles: FileMeta[]) => { // Update list of known pages - this.allKnownPages = new Set( - allFiles.filter((f) => f.name.endsWith(".md")).map((f) => - f.name.slice(0, -3) - ), - ); + this.allKnownPages.clear(); + allFiles.forEach((f) => { + if (f.name.endsWith(".md")) { + this.allKnownPages.add(f.name.slice(0, -3)); + } + }); }, ); @@ -638,9 +655,9 @@ export class Client { ); } - startPageNavigate() { + startPageNavigate(mode: "page" | "template") { // Then show the page navigator - this.ui.viewDispatch({ type: "start-navigate" }); + this.ui.viewDispatch({ type: "start-navigate", mode }); this.updatePageListCache().catch(console.error); } @@ -854,7 +871,9 @@ export class Client { newWindow = false, ) { if (!name) { - name = cleanPageRef(this.settings.indexPage); + name = cleanPageRef( + renderHandlebarsTemplate(this.settings.indexPage, {}, {}), + ); } try { @@ -903,6 +922,7 @@ export class Client { if (e.message.includes("Not found")) { // Not found, new page console.log("Page doesn't exist, creating new page:", pageName); + // Initialize page doc = { text: "", meta: { @@ -914,6 +934,13 @@ export class Client { perm: "rw", } as PageMeta, }; + this.system.system.invokeFunction("template.newPage", [pageName]).then( + () => { + this.focus(); + }, + ).catch( + console.error, + ); } else { this.flashNotification( `Could not load page ${pageName}: ${e.message}`, diff --git a/web/client_system.ts b/web/client_system.ts index 9b22f6d..7ea1554 100644 --- a/web/client_system.ts +++ b/web/client_system.ts @@ -42,6 +42,7 @@ import { KVPrimitivesManifestCache } from "../plugos/manifest_cache.ts"; import { deepObjectMerge } from "$sb/lib/json.ts"; import { Query } from "$sb/types.ts"; import { PanelWidgetHook } from "./hooks/panel_widget.ts"; +import { createKeyBindings } from "./editor_state.ts"; const plugNameExtractRegex = /\/(.+)\.plug\.js$/; @@ -103,6 +104,12 @@ export class ClientSystem { type: "update-commands", commands: commandMap, }); + // Replace the key mapping compartment (keybindings) + this.client.editorView.dispatch({ + effects: this.client.keyHandlerCompartment?.reconfigure( + createKeyBindings(this.client), + ), + }); }, }); this.system.addHook(this.commandHook); diff --git a/web/cm_plugins/editor_paste.ts b/web/cm_plugins/editor_paste.ts index a42a15e..2cc0e0b 100644 --- a/web/cm_plugins/editor_paste.ts +++ b/web/cm_plugins/editor_paste.ts @@ -131,9 +131,12 @@ export function attachmentExtension(editor: Client) { if (currentNode) { const fencedParentNode = findParentMatching( currentNode, - (t) => t.type === "FencedCode", + (t) => ["FrontMatter", "FencedCode"].includes(t.type!), ); - if (fencedParentNode || currentNode.type === "FencedCode") { + if ( + fencedParentNode || + ["FrontMatter", "FencedCode"].includes(currentNode.type!) + ) { console.log("Inside of fenced code block, not pasting rich text"); return false; } diff --git a/web/cm_plugins/markdown_widget.ts b/web/cm_plugins/markdown_widget.ts index 0831a06..1e225a9 100644 --- a/web/cm_plugins/markdown_widget.ts +++ b/web/cm_plugins/markdown_widget.ts @@ -165,8 +165,13 @@ export class MarkdownWidget extends WidgetType { el.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); - console.info("Command link clicked in widget, running", command); - this.client.runCommandByName(command).catch(console.error); + console.info( + "Command link clicked in widget, running", + parsedOnclick, + ); + this.client.runCommandByName(command, parsedOnclick[2]).catch( + console.error, + ); }); } }); diff --git a/web/cm_plugins/util.ts b/web/cm_plugins/util.ts index 4404425..ca2bc3c 100644 --- a/web/cm_plugins/util.ts +++ b/web/cm_plugins/util.ts @@ -30,13 +30,21 @@ export class LinkWidget extends WidgetType { anchor.textContent = this.options.text; // Mouse handling - anchor.addEventListener("mousedown", (e) => { + anchor.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + }); + anchor.addEventListener("mouseup", (e) => { if (e.button !== 0) { return; } e.preventDefault(); e.stopPropagation(); - this.options.callback(e); + try { + this.options.callback(e); + } catch (e) { + console.error("Error handling wiki link click", e); + } }); // Touch handling @@ -111,7 +119,7 @@ export class ButtonWidget extends WidgetType { const anchor = document.createElement("button"); anchor.className = this.cssClass; anchor.textContent = this.text; - anchor.addEventListener("click", (e) => { + anchor.addEventListener("mouseup", (e) => { e.preventDefault(); e.stopPropagation(); this.callback(e); diff --git a/web/components/command_palette.tsx b/web/components/command_palette.tsx index e7d45e7..6263337 100644 --- a/web/components/command_palette.tsx +++ b/web/components/command_palette.tsx @@ -40,7 +40,7 @@ export function CommandPalette({ }); if (commandOverride) { shortcut = commandOverride; - console.log(`Shortcut override for ${name}:`, shortcut); + // console.log(`Shortcut override for ${name}:`, shortcut); } } options.push({ diff --git a/web/components/filter.tsx b/web/components/filter.tsx index 4394068..1e51120 100644 --- a/web/components/filter.tsx +++ b/web/components/filter.tsx @@ -47,7 +47,10 @@ export function FilterList({ }) { const [text, setText] = useState(""); const [matchingOptions, setMatchingOptions] = useState( - fuzzySearchAndSort(options, ""), + fuzzySearchAndSort( + preFilter ? preFilter(options, "") : options, + "", + ), ); const [selectedOption, setSelectionOption] = useState(0); diff --git a/web/components/mini_editor.tsx b/web/components/mini_editor.tsx index d9355cd..9f629ee 100644 --- a/web/components/mini_editor.tsx +++ b/web/components/mini_editor.tsx @@ -84,7 +84,7 @@ export function MiniEditor( } }; } - }, [editorDiv]); + }, [editorDiv, placeholderText]); useEffect(() => { callbacksRef.current = { diff --git a/web/components/page_navigator.tsx b/web/components/page_navigator.tsx index f71069b..4012b3a 100644 --- a/web/components/page_navigator.tsx +++ b/web/components/page_navigator.tsx @@ -11,12 +11,14 @@ export function PageNavigator({ onNavigate, completer, vimMode, + mode, darkMode, currentPage, }: { allPages: PageMeta[]; vimMode: boolean; darkMode: boolean; + mode: "page" | "template"; onNavigate: (page: string | undefined) => void; completer: (context: CompletionContext) => Promise; currentPage?: string; @@ -72,7 +74,7 @@ export function PageNavigator({ } return ( { - const allTags = phrase.match(tagRegex); - if (allTags) { - // Search phrase contains hash tags, let's pre-filter the results based on this - const filterTags = allTags.map((t) => t.slice(1)); + if (mode === "page") { + const allTags = phrase.match(tagRegex); + if (allTags) { + // Search phrase contains hash tags, let's pre-filter the results based on this + const filterTags = allTags.map((t) => t.slice(1)); + options = options.filter((pageMeta) => { + if (!pageMeta.tags) { + return false; + } + return filterTags.every((tag) => + pageMeta.tags.find((itemTag: string) => itemTag.startsWith(tag)) + ); + }); + } options = options.filter((pageMeta) => { - if (!pageMeta.tags) { - return false; - } - return filterTags.every((tag) => - pageMeta.tags.find((itemTag: string) => itemTag.startsWith(tag)) - ); + return !pageMeta.tags?.includes("template"); }); + return options; + } else { + // Filter on pages tagged with "template" + options = options.filter((pageMeta) => { + return pageMeta.tags?.includes("template"); + }); + return options; } - return options; }} allowNew={true} - helpText="Press Enter to open the selected page, or Shift-Enter to create a new page with this exact name." - newHint="Create page" + helpText={`Press Enter to open the selected ${mode}, or Shift-Enter to create a new ${mode} with this exact name.`} + newHint={`Create ${mode}`} completePrefix={completePrefix} onSelect={(opt) => { onNavigate(opt?.name); diff --git a/web/components/top_bar.tsx b/web/components/top_bar.tsx index ea33641..bfe724f 100644 --- a/web/components/top_bar.tsx +++ b/web/components/top_bar.tsx @@ -66,7 +66,7 @@ export function TopBar({ // Then calculate a new width currentPageElement.style.width = `${ - Math.min(editorWidth - 170, innerDiv.clientWidth - 170) + Math.min(editorWidth - 200, innerDiv.clientWidth - 200) }px`; } } diff --git a/web/deps.ts b/web/deps.ts index 7090f5d..d6aeb52 100644 --- a/web/deps.ts +++ b/web/deps.ts @@ -14,6 +14,7 @@ export { Home as HomeIcon, RefreshCw as RefreshCwIcon, Terminal as TerminalIcon, + Type as TemplateIcon, } from "https://esm.sh/preact-feather@4.2.1?external=preact"; // Vim mode diff --git a/web/editor_state.ts b/web/editor_state.ts index cd8ec84..28b8613 100644 --- a/web/editor_state.ts +++ b/web/editor_state.ts @@ -45,6 +45,7 @@ import { TextChange } from "$sb/lib/change.ts"; import { postScriptPrefacePlugin } from "./cm_plugins/top_bottom_panels.ts"; import { languageFor } from "../common/languages.ts"; import { plugLinter } from "./cm_plugins/lint.ts"; +import { Compartment, Extension } from "@codemirror/state"; export function createEditorState( client: Client, @@ -52,85 +53,16 @@ export function createEditorState( text: string, readOnly: boolean, ): EditorState { - const commandKeyBindings: KeyBinding[] = []; - - // Track which keyboard shortcuts for which commands we've overridden, so we can skip them later - const overriddenCommands = new Set(); - // Keyboard shortcuts from SETTINGS take precedense - if (client.settings?.shortcuts) { - for (const shortcut of client.settings.shortcuts) { - // Figure out if we're using the command link syntax here, if so: parse it out - const commandMatch = commandLinkRegex.exec(shortcut.command); - let cleanCommandName = shortcut.command; - let args: any[] = []; - if (commandMatch) { - cleanCommandName = commandMatch[1]; - args = commandMatch[5] ? JSON.parse(`[${commandMatch[5]}]`) : []; - } - if (args.length === 0) { - // If there was no "specialization" of this command (that is, we effectively created a keybinding for an existing command but with arguments), let's add it to the overridden command set: - overriddenCommands.add(cleanCommandName); - } - commandKeyBindings.push({ - key: shortcut.key, - mac: shortcut.mac, - run: (): boolean => { - client.runCommandByName(cleanCommandName, args).catch((e: any) => { - console.error(e); - client.flashNotification( - `Error running command: ${e.message}`, - "error", - ); - }).then(() => { - // Always be focusing the editor after running a command - client.focus(); - }); - return true; - }, - }); - } - } - - // Then add bindings for plug commands - for (const def of client.system.commandHook.editorCommands.values()) { - if (def.command.key) { - // If we've already overridden this command, skip it - if (overriddenCommands.has(def.command.key)) { - continue; - } - commandKeyBindings.push({ - key: def.command.key, - mac: def.command.mac, - run: (): boolean => { - if (def.command.contexts) { - const context = client.getContext(); - if (!context || !def.command.contexts.includes(context)) { - return false; - } - } - Promise.resolve([]) - .then(def.run) - .catch((e: any) => { - console.error(e); - client.flashNotification( - `Error running command: ${e.message}`, - "error", - ); - }) - .then(() => { - // Always be focusing the editor after running a command - client.focus(); - }); - return true; - }, - }); - } - } - let touchCount = 0; const markdownLanguage = buildMarkdown(client.system.mdExtensions); + // Ugly: keep the keyhandler compartment in the client, to be replaced later once more commands are loaded + client.keyHandlerCompartment = new Compartment(); + const keyBindings = client.keyHandlerCompartment.of( + createKeyBindings(client), + ); + return EditorState.create({ doc: text, extensions: [ @@ -209,48 +141,13 @@ export function createEditorState( { selector: "BulletList", class: "sb-line-ul" }, { selector: "OrderedList", class: "sb-line-ol" }, { selector: "TableHeader", class: "sb-line-tbl-header" }, - { selector: "FrontMatter", class: "sb-frontmatter" }, - ]), - keymap.of([ - ...commandKeyBindings, - ...smartQuoteKeymap, - ...closeBracketsKeymap, - ...standardKeymap, - ...searchKeymap, - ...historyKeymap, - ...completionKeymap, - indentWithTab, { - key: "Ctrl-k", - mac: "Cmd-k", - run: (): boolean => { - client.startPageNavigate(); - return true; - }, - }, - { - key: "Ctrl-/", - mac: "Cmd-/", - run: (): boolean => { - client.ui.viewDispatch({ - type: "show-palette", - context: client.getContext(), - }); - return true; - }, - }, - { - key: "Ctrl-.", - mac: "Cmd-.", - run: (): boolean => { - client.ui.viewDispatch({ - type: "show-palette", - context: client.getContext(), - }); - return true; - }, + selector: "FrontMatter", + class: "sb-frontmatter", + disableSpellCheck: true, }, ]), + keyBindings, EditorView.domEventHandlers({ // This may result in duplicated touch events on mobile devices touchmove: () => { @@ -366,3 +263,101 @@ export function createEditorState( ], }); } + +export function createKeyBindings(client: Client): Extension { + const commandKeyBindings: KeyBinding[] = []; + + // Track which keyboard shortcuts for which commands we've overridden, so we can skip them later + const overriddenCommands = new Set(); + // Keyboard shortcuts from SETTINGS take precedense + if (client.settings?.shortcuts) { + for (const shortcut of client.settings.shortcuts) { + // Figure out if we're using the command link syntax here, if so: parse it out + const commandMatch = commandLinkRegex.exec(shortcut.command); + let cleanCommandName = shortcut.command; + let args: any[] = []; + if (commandMatch) { + cleanCommandName = commandMatch[1]; + args = commandMatch[5] ? JSON.parse(`[${commandMatch[5]}]`) : []; + } + if (args.length === 0) { + // If there was no "specialization" of this command (that is, we effectively created a keybinding for an existing command but with arguments), let's add it to the overridden command set: + overriddenCommands.add(cleanCommandName); + } + commandKeyBindings.push({ + key: shortcut.key, + mac: shortcut.mac, + run: (): boolean => { + client.runCommandByName(cleanCommandName, args).catch((e: any) => { + console.error(e); + client.flashNotification( + `Error running command: ${e.message}`, + "error", + ); + }).then(() => { + // Always be focusing the editor after running a command + client.focus(); + }); + return true; + }, + }); + } + } + + // Then add bindings for plug commands + for (const def of client.system.commandHook.editorCommands.values()) { + if (def.command.key) { + // If we've already overridden this command, skip it + if (overriddenCommands.has(def.command.key)) { + continue; + } + commandKeyBindings.push({ + key: def.command.key, + mac: def.command.mac, + run: (): boolean => { + if (def.command.contexts) { + const context = client.getContext(); + if (!context || !def.command.contexts.includes(context)) { + return false; + } + } + Promise.resolve([]) + .then(def.run) + .catch((e: any) => { + console.error(e); + client.flashNotification( + `Error running command: ${e.message}`, + "error", + ); + }) + .then(() => { + // Always be focusing the editor after running a command + client.focus(); + }); + return true; + }, + }); + } + } + return keymap.of([ + ...commandKeyBindings, + ...smartQuoteKeymap, + ...closeBracketsKeymap, + ...standardKeymap, + ...searchKeymap, + ...historyKeymap, + ...completionKeymap, + indentWithTab, + { + key: "Ctrl-.", + mac: "Cmd-.", + run: (): boolean => { + client.ui.viewDispatch({ + type: "show-palette", + context: client.getContext(), + }); + return true; + }, + }, + ]); +} diff --git a/web/editor_ui.tsx b/web/editor_ui.tsx index c68b443..3c3809b 100644 --- a/web/editor_ui.tsx +++ b/web/editor_ui.tsx @@ -12,6 +12,7 @@ import { preactRender, RefreshCwIcon, runScopeHandlers, + TemplateIcon, TerminalIcon, useEffect, useReducer, @@ -20,6 +21,7 @@ import type { Client } from "./client.ts"; import { Panel } from "./components/panel.tsx"; import { h } from "./deps.ts"; import { sleep } from "$sb/lib/async.ts"; +import { template } from "https://esm.sh/v132/handlebars@4.7.7/runtime.d.ts"; export class MainUI { viewState: AppViewState = initialViewState; @@ -44,7 +46,7 @@ export class MainUI { if (ev.touches.length === 2) { ev.stopPropagation(); ev.preventDefault(); - client.startPageNavigate(); + client.startPageNavigate("page"); } // Launch the command palette using a three-finger tap if (ev.touches.length === 3) { @@ -99,6 +101,7 @@ export class MainUI { { client.navigate("", 0); + // And let's make sure all panels are closed + dispatch({ type: "hide-filterbox" }); }, href: "", }, @@ -251,7 +256,16 @@ export class MainUI { icon: BookIcon, description: `Open page (${isMacLike() ? "Cmd-k" : "Ctrl-k"})`, callback: () => { - client.startPageNavigate(); + client.startPageNavigate("page"); + }, + }, + { + icon: TemplateIcon, + description: `Open template (${ + isMacLike() ? "Cmd-Shift-t" : "Ctrl-Shift-t" + })`, + callback: () => { + client.startPageNavigate("template"); }, }, { diff --git a/web/hooks/command.ts b/web/hooks/command.ts index 69bd33a..1688a72 100644 --- a/web/hooks/command.ts +++ b/web/hooks/command.ts @@ -1,6 +1,13 @@ import { Hook, Manifest } from "../../plugos/types.ts"; import { System } from "../../plugos/system.ts"; import { EventEmitter } from "../../plugos/event.ts"; +import { ObjectValue } from "$sb/types.ts"; +import { + FrontmatterConfig, + SnippetConfig, +} from "../../plugs/template/types.ts"; +import { throttle } from "$sb/lib/async.ts"; +import { NewPageConfig } from "../../plugs/template/types.ts"; export type CommandDef = { name: string; @@ -31,10 +38,15 @@ export type CommandHookEvents = { export class CommandHook extends EventEmitter implements Hook { editorCommands = new Map(); + system!: System; - buildAllCommands(system: System) { + throttledBuildAllCommands = throttle(() => { + this.buildAllCommands().catch(console.error); + }, 1000); + + async buildAllCommands() { this.editorCommands.clear(); - for (const plug of system.loadedPlugs.values()) { + for (const plug of this.system.loadedPlugs.values()) { for ( const [name, functionDef] of Object.entries( plug.manifest!.functions, @@ -52,18 +64,89 @@ export class CommandHook extends EventEmitter }); } } + await this.loadPageTemplateCommands(); this.emit("commandsUpdated", this.editorCommands); } + async loadPageTemplateCommands() { + // This relies on two plugs being loaded: index and template + const indexPlug = this.system.loadedPlugs.get("index"); + const templatePlug = this.system.loadedPlugs.get("template"); + if (!indexPlug || !templatePlug) { + // Index and template plugs not yet loaded, let's wait + return; + } + + // Query all page templates that have a command configured + const templateCommands: ObjectValue[] = await indexPlug + .invoke( + "queryObjects", + ["template", { + // where hooks.newPage.command or hooks.snippet.command + filter: ["or", [ + "attr", + ["attr", ["attr", "hooks"], "newPage"], + "command", + ], [ + "attr", + ["attr", ["attr", "hooks"], "snippet"], + "command", + ]], + }], + ); + + // console.log("Template commands", templateCommands); + + for (const page of templateCommands) { + try { + if (page.hooks!.newPage) { + const newPageConfig = NewPageConfig.parse(page.hooks!.newPage); + const cmdDef = { + name: newPageConfig.command!, + key: newPageConfig.key, + mac: newPageConfig.mac, + }; + this.editorCommands.set(newPageConfig.command!, { + command: cmdDef, + run: () => { + return templatePlug.invoke("newPageCommand", [cmdDef, page.ref]); + }, + }); + } + if (page.hooks!.snippet) { + const snippetConfig = SnippetConfig.parse(page.hooks!.snippet); + const cmdDef = { + name: snippetConfig.command!, + key: snippetConfig.key, + mac: snippetConfig.mac, + }; + this.editorCommands.set(snippetConfig.command!, { + command: cmdDef, + run: () => { + return templatePlug.invoke("insertSnippetTemplate", [ + { templatePage: page.ref }, + ]); + }, + }); + } + } catch (e: any) { + console.error("Error loading command from", page.ref, e); + } + } + + // console.log("Page template commands", pageTemplateCommands); + } + apply(system: System): void { + this.system = system; system.on({ plugLoaded: () => { - this.buildAllCommands(system); + this.throttledBuildAllCommands(); }, }); // On next tick setTimeout(() => { - this.buildAllCommands(system); + this.throttledBuildAllCommands(); }); } diff --git a/web/hooks/slash_command.ts b/web/hooks/slash_command.ts index ca54dda..c85798c 100644 --- a/web/hooks/slash_command.ts +++ b/web/hooks/slash_command.ts @@ -115,18 +115,10 @@ export class SlashCommandHook implements Hook { }); // Replace with whatever the completion is safeRun(async () => { - const [plugName, functionName] = slashCompletion.invoke.split( - ".", + await this.editor.system.system.invokeFunction( + slashCompletion.invoke, + [slashCompletion], ); - const plug = this.editor.system.system.loadedPlugs.get(plugName); - if (!plug) { - this.editor.flashNotification( - `Plug ${plugName} not found`, - "error", - ); - return; - } - await plug.invoke(functionName, [slashCompletion]); this.editor.focus(); }); }, diff --git a/web/reducer.ts b/web/reducer.ts index 7ab6fd2..3a0d550 100644 --- a/web/reducer.ts +++ b/web/reducer.ts @@ -66,6 +66,7 @@ export default function reducer( return { ...state, showPageNavigator: true, + pageNavigatorMode: action.mode, showCommandPalette: false, showFilterBox: false, }; @@ -141,6 +142,8 @@ export default function reducer( case "hide-filterbox": return { ...state, + showCommandPalette: false, + showPageNavigator: false, showFilterBox: false, filterBoxOnSelect: () => {}, filterBoxPlaceHolder: "", diff --git a/web/syscalls/editor.ts b/web/syscalls/editor.ts index 829a36f..6ac4357 100644 --- a/web/syscalls/editor.ts +++ b/web/syscalls/editor.ts @@ -14,22 +14,22 @@ import { SysCallMapping } from "../../plugos/system.ts"; import type { FilterOption } from "../types.ts"; import { UploadFile } from "../../plug-api/types.ts"; -export function editorSyscalls(editor: Client): SysCallMapping { +export function editorSyscalls(client: Client): SysCallMapping { const syscalls: SysCallMapping = { "editor.getCurrentPage": (): string => { - return editor.currentPage!; + return client.currentPage!; }, "editor.getText": () => { - return editor.editorView.state.sliceDoc(); + return client.editorView.state.sliceDoc(); }, "editor.getCursor": (): number => { - return editor.editorView.state.selection.main.from; + return client.editorView.state.selection.main.from; }, "editor.getSelection": (): { from: number; to: number } => { - return editor.editorView.state.selection.main; + return client.editorView.state.selection.main; }, "editor.save": () => { - return editor.save(true); + return client.save(true); }, "editor.navigate": async ( _ctx, @@ -38,14 +38,18 @@ export function editorSyscalls(editor: Client): SysCallMapping { replaceState = false, newWindow = false, ) => { - await editor.navigate(name, pos, replaceState, newWindow); + await client.navigate(name, pos, replaceState, newWindow); }, "editor.reloadPage": async () => { - await editor.reloadPage(); + await client.reloadPage(); }, "editor.reloadUI": () => { location.reload(); }, + "editor.reloadSettingsAndCommands": async () => { + await client.loadSettings(); + await client.system.commandHook.buildAllCommands(); + }, "editor.openUrl": (_ctx, url: string, existingWindow = false) => { if (!existingWindow) { const win = window.open(url, "_blank"); @@ -113,7 +117,7 @@ export function editorSyscalls(editor: Client): SysCallMapping { message: string, type: "error" | "info" = "info", ) => { - editor.flashNotification(message, type); + client.flashNotification(message, type); }, "editor.filterBox": ( _ctx, @@ -122,7 +126,7 @@ export function editorSyscalls(editor: Client): SysCallMapping { helpText = "", placeHolder = "", ): Promise => { - return editor.filterBox(label, options, helpText, placeHolder); + return client.filterBox(label, options, helpText, placeHolder); }, "editor.showPanel": ( _ctx, @@ -131,28 +135,28 @@ export function editorSyscalls(editor: Client): SysCallMapping { html: string, script: string, ) => { - editor.ui.viewDispatch({ + client.ui.viewDispatch({ type: "show-panel", id: id as any, config: { html, script, mode }, }); setTimeout(() => { // Dummy dispatch to rerender the editor and toggle the panel - editor.editorView.dispatch({}); + client.editorView.dispatch({}); }); }, "editor.hidePanel": (_ctx, id: string) => { - editor.ui.viewDispatch({ + client.ui.viewDispatch({ type: "hide-panel", id: id as any, }); setTimeout(() => { // Dummy dispatch to rerender the editor and toggle the panel - editor.editorView.dispatch({}); + client.editorView.dispatch({}); }); }, "editor.insertAtPos": (_ctx, text: string, pos: number) => { - editor.editorView.dispatch({ + client.editorView.dispatch({ changes: { insert: text, from: pos, @@ -160,7 +164,7 @@ export function editorSyscalls(editor: Client): SysCallMapping { }); }, "editor.replaceRange": (_ctx, from: number, to: number, text: string) => { - editor.editorView.dispatch({ + client.editorView.dispatch({ changes: { insert: text, from: from, @@ -169,13 +173,13 @@ export function editorSyscalls(editor: Client): SysCallMapping { }); }, "editor.moveCursor": (_ctx, pos: number, center = false) => { - editor.editorView.dispatch({ + client.editorView.dispatch({ selection: { anchor: pos, }, }); if (center) { - editor.editorView.dispatch({ + client.editorView.dispatch({ effects: [ EditorView.scrollIntoView( pos, @@ -186,10 +190,10 @@ export function editorSyscalls(editor: Client): SysCallMapping { ], }); } - editor.editorView.focus(); + client.editorView.focus(); }, "editor.setSelection": (_ctx, from: number, to: number) => { - editor.editorView.dispatch({ + client.editorView.dispatch({ selection: { anchor: from, head: to, @@ -198,7 +202,7 @@ export function editorSyscalls(editor: Client): SysCallMapping { }, "editor.insertAtCursor": (_ctx, text: string) => { - const editorView = editor.editorView; + const editorView = client.editorView; const from = editorView.state.selection.main.from; editorView.dispatch({ changes: { @@ -211,47 +215,55 @@ export function editorSyscalls(editor: Client): SysCallMapping { }); }, "editor.dispatch": (_ctx, change: Transaction) => { - editor.editorView.dispatch(change); + client.editorView.dispatch(change); }, "editor.prompt": ( _ctx, message: string, defaultValue = "", ): Promise => { - return editor.prompt(message, defaultValue); + return client.prompt(message, defaultValue); }, "editor.confirm": (_ctx, message: string): Promise => { - return editor.confirm(message); + return client.confirm(message); }, "editor.getUiOption": (_ctx, key: string): any => { - return (editor.ui.viewState.uiOptions as any)[key]; + return (client.ui.viewState.uiOptions as any)[key]; }, "editor.setUiOption": (_ctx, key: string, value: any) => { - editor.ui.viewDispatch({ + client.ui.viewDispatch({ type: "set-ui-option", key, value, }); }, "editor.vimEx": (_ctx, exCommand: string) => { - const cm = vimGetCm(editor.editorView)!; + const cm = vimGetCm(client.editorView)!; return Vim.handleEx(cm, exCommand); }, + "editor.openPageNavigator": (_ctx, mode: "page" | "template" = "page") => { + client.startPageNavigate(mode); + }, + "editor.openCommandPalette": () => { + client.ui.viewDispatch({ + type: "show-palette", + }); + }, // Folding "editor.fold": () => { - foldCode(editor.editorView); + foldCode(client.editorView); }, "editor.unfold": () => { - unfoldCode(editor.editorView); + unfoldCode(client.editorView); }, "editor.toggleFold": () => { - toggleFold(editor.editorView); + toggleFold(client.editorView); }, "editor.foldAll": () => { - foldAll(editor.editorView); + foldAll(client.editorView); }, "editor.unfoldAll": () => { - unfoldAll(editor.editorView); + unfoldAll(client.editorView); }, }; diff --git a/web/syscalls/util.ts b/web/syscalls/util.ts index 88fe078..e37cb3e 100644 --- a/web/syscalls/util.ts +++ b/web/syscalls/util.ts @@ -18,11 +18,8 @@ export async function proxySyscall( name: string, args: any[], ): Promise { - if (!ctx.plug) { - throw new Error(`Cannot proxy ${name} syscall without plug context`); - } const resp = await httpSpacePrimitives.authenticatedFetch( - `${httpSpacePrimitives.url}/.rpc/${ctx.plug}/${name}`, + `${httpSpacePrimitives.url}/.rpc/${ctx.plug || "_"}/${name}`, { method: "POST", body: JSON.stringify(args), diff --git a/web/types.ts b/web/types.ts index 7c5b1b0..1a42d75 100644 --- a/web/types.ts +++ b/web/types.ts @@ -63,6 +63,9 @@ export type AppViewState = { forcedROMode: boolean; }; + // Page navigator mode + pageNavigatorMode: "page" | "template"; + // Filter box showFilterBox: boolean; filterBoxLabel: string; @@ -87,6 +90,7 @@ export const initialViewState: AppViewState = { isLoading: false, showPageNavigator: false, showCommandPalette: false, + pageNavigatorMode: "page", unsavedChanges: false, syncFailures: 0, uiOptions: { @@ -122,7 +126,7 @@ export type Action = | { type: "page-saved" } | { type: "sync-change"; syncSuccess: boolean } | { type: "update-page-list"; allPages: PageMeta[] } - | { type: "start-navigate" } + | { type: "start-navigate"; mode: "page" | "template" } | { type: "stop-navigate" } | { type: "update-commands"; diff --git a/website/Blocks.md b/website/Blocks.md new file mode 100644 index 0000000..f2a5d5c --- /dev/null +++ b/website/Blocks.md @@ -0,0 +1,16 @@ +Blocks use the fenced code block notation of [[Markdown]], and assign special behavior to it. + +The general syntax is: + + ```block-type + block configuration + ``` + +These are the block types that ship with SilverBullet, but [[Plugs]] can define their own: + +* `template`: [[Live Templates]] +* `query`: [[Live Queries]] +* `toc`: [[Table of Contents]] +* `embed`: [[Live Embeds]] + +The fenced code block syntax is also used to get [[Markdown/Syntax Highlighting]] for numerous programming languages. \ No newline at end of file diff --git a/website/CHANGELOG.md b/website/CHANGELOG.md index 741df59..2a7c5c4 100644 --- a/website/CHANGELOG.md +++ b/website/CHANGELOG.md @@ -2,9 +2,16 @@ An attempt at documenting the changes/new features introduced in each release. --- -## Next -_Not yet released, this will likely become 0.6.0._ +## Edge +_Not yet released, this will likely become 0.6.0. To try this out now, check out [the docs on edge](https://community.silverbullet.md/t/living-on-the-edge-builds/27)._ +* **Templates 2.0**: templates are now turbo charged (that’s a technical term) and have replaced a lot of previously built in (slash) commands. There’s more to this than will fit this CHANGELOG, have a look at [[Templates]]: and more specifically [[Page Templates]], [[Snippets]], [[Live Template Widgets]] and [[Libraries]]. + A quick FAQ: + * **Where did my templates go!?** They have now moved to the [[Template Picker]], see that “T” button up there? Yeah, that’s new. + * **Where did all my slash commands go?!** They are now distributed via [[Libraries]]. Yep, Libraries are here, enabling an easier way to distribute templates and pages. Read [[Libraries]] for more info. + * **But, what about slash templates etc.?!** Yeah, we did some rebranding and changed how these are defined. Slash templates are now [[Snippets]] and cannot _just_ be instantiated via [[Slash Commands]], but through [[Commands]] and custom keybindings as well. Awesomeness. + * **And my page templates broke!?** Yeah, same story as with [[Snippets]]: the format for defining these changed a bit, but should be easy to update to the new format: check [[Page Templates]]. +* The [[Getting Started]] page (that is embedded in the `index` page that is auto-generated when creating a new space) has been updated to include instructions on how to import the [[Library/Core]] library. * **Directives have now been removed** from the code base. Please use [[Live Queries]] and [[Live Templates]] instead. If you hadn’t migrated yet and want to auto migrate, downgrade your SilverBullet version to 0.5.11 (e.g. using the `zefhemel/silverbullet:0.5.11` docker image) and run the {[Directive: Convert Entire Space to Live/Templates]} command with that version. * (Hopefully subtle) **breaking change** in how tags work (see [[Objects]]): * Every object now has a `tag` attribute, signifying the “main” tag for that object (e.g. `page`, `item`) @@ -12,17 +19,14 @@ _Not yet released, this will likely become 0.6.0._ * The new `itags` attribute (available in many objects) includes both the `tag`, `tags` as well as any tags inherited from the page the object appears in. * Page tags now no longer need to appear at the top of the page, but can appear anywhere as long as they are the only thing appearing in a paragraph with no additional text, see [[Objects$page]]. * New [[Markdown/Code Widgets|Code Widget]]: `toc` to manually include a [[Table of Contents]] -* New template type: [[Live Template Widgets]] allowing you to automatically add templates to the top or bottom of your pages (based on some criteria). Using this feature it possible to implement [[Table of Contents]] and [[Linked Mentions]] without having “hard coded” into SilverBullet itself. -* **“Breaking” change:** Two features are now no longer hardcoded into SilverBullet, but can be activated quite easily using [[Live Template Widgets]] (see their respective documentation pages on instructions on how to do this): - * [[Table of Contents]] - * [[Linked Mentions]] -* Filter list (used by [[Page Picker]] and [[Command Palette]]) improvements: +* Filter list (used by [[Page Picker]], [[Template Picker]] and [[Command Palette]]) improvements: * Better ranking * Better positioning of modal (especially on mobile) * Better mouse behavior * Templates: * Somewhat nicer rendering of {{templateVars}} (notice the gray background) * Rendering of [[Markdown/Code Widgets]] (such as live queries and templates) **are now disabled** on template pages, which should make them less confusing to read and interpret. +* The `indexPage` [[SETTINGS]] can now contain template variables, such as `{{today}}` * Backend work in preparation for supporting more “serverless” deployments (e.g. Cloudflare workers and Deno Deploy) in the future * Move from [Oak](https://oakserver.github.io/oak/) to [Hono](https://hono.dev/) * Support for in-process plug loading (without workers) @@ -73,7 +77,7 @@ _Not yet released, this will likely become 0.6.0._ --- ## 0.5.6 * Various optimization and bug fixes -* Experimental idea: [[Template Sets]] +* Experimental idea: [[Libraries]] * The `Alt-Shift-n` key was previously bound to both {[Page: New]} and {[Quick Note]}. That won’t work, so now it’s just bound to {[Quick Note]} * The `Alt-q` command is now bound to the new {[Live Queries and Templates: Refresh All]} command, refreshing all [[Live Queries]] and [[Live Templates]] on the page. This is to get y’all prepared to move away from directives. * It’s likely that version 0.6.0 **will remove directives**, so please switch over to live queries and templates, e.g. using... @@ -89,7 +93,7 @@ _Not yet released, this will likely become 0.6.0._ ## 0.5.4 * We’re on a journey to rethink [[Templates]]: * It is now _recommended_ you tag all your templates with a `#template` tag, this will exclude them from [[Objects]] indexing and may in the future be used to do better template name completion (but not yet). - * New feature: Introducing [[Slash Templates]], allowing you to create custom [[Slash Commands]]. This deprecates snippets and page templates, because [[Slash Templates]] are awesomer. + * New feature: Introducing [[Snippets]], allowing you to create custom [[Slash Commands]]. This deprecates snippets and page templates, because [[Snippets]] are awesomer. * Many styling fixes and improvements to [[Live Queries]] and [[Live Templates]] * Added a “source” button to [[Live Queries]] and [[Live Templates]] for better debugging (showing you the markdown code rendered by the template so you can more easily detect issues) * [[Live Queries]]: @@ -125,14 +129,14 @@ _Not yet released, this will likely become 0.6.0._ Oh boy, this is a big one. This release brings you the following: * [[Objects]]: a more generic system for indexing and querying content in your space, including the ability to define your own custom object “types” (dubbed [[Tags]]). See the referenced pages for examples. -* [[Live Queries]] and [[Live Templates]]: ultimately will replace [[🔌 Directive]] in future versions and **[[🔌 Directive]] is now deprecated.** They differ from directives in that they don’t materialize their output into the page itself, but rather render them on the fly so only the query/template instantiation is kept on disk. All previous directive examples on this website how now been replaced with [[Live Templates]] and [[Live Queries]]. To ease the conversion there is {[Directive: Convert Query to Live Query]} command: just put your cursor inside of an existing (query) directive and run it to auto-convert. -* The query syntax used in [[Live Queries]] (but also used in [[🔌 Directive]]) has been significantly expanded, although there may still be bugs. There’s still more value to be unlocked here in future releases. +* [[Live Queries]] and [[Live Templates]]: ultimately will replace directives in future versions and **directives are now deprecated.** They differ from directives in that they don’t materialize their output into the page itself, but rather render them on the fly so only the query/template instantiation is kept on disk. All previous directive examples on this website how now been replaced with [[Live Templates]] and [[Live Queries]]. To ease the conversion there is {[Directive: Convert Query to Live Query]} command: just put your cursor inside of an existing (query) directive and run it to auto-convert. +* The query syntax used in [[Live Queries]] (but also used in directives) has been significantly expanded, although there may still be bugs. There’s still more value to be unlocked here in future releases. * The previous “backlinks” plug is now built into SilverBullet as [[Linked Mentions]] and appears at the bottom of every page (if there are incoming links). You can toggle linked mentions via {[Mentions: Toggle]}. * A whole bunch of [[PlugOS]] syscalls have been updated. I’ll do my best update known existing plugs, but if you built existing ones some things may have broken. Please report anything broken in [Github issues](https://github.com/silverbulletmd/silverbullet/issues). -* This release effectively already removes the `#eval` [[🔌 Directive]] (it’s still there, but likely not working), this directive needs some rethinking. Join us on [Discord](https://discord.gg/EvXbFucTxn) if you have a use case for it and how you use/want to use it. +* This release effectively already removes the `#eval` (it’s still there, but likely not working), this directive needs some rethinking. Join us on [Discord](https://discord.gg/EvXbFucTxn) if you have a use case for it and how you use/want to use it. **Important**: -* If you have plugs such as “backlinks” or “graphview” installed, please remove them (or to be safe: all plugs) from the `_plug` folder in your space after the upgrade. Then, also remove them from your [[PLUGS]] page. The backlinks plug is now included by default (named [[Linked Mentions]]), and GraphView still needs to be updated (although it’s been kind of abandoned by the author). +* If you have plugs such as “backlinks” or “graphview” installed, please remove them (or to be safe: all plugs) from the `_plug` folder in your space after the upgrade. Then, also remove them from your `PLUGS` page. The backlinks plug is now included by default (named [[Linked Mentions]]), and GraphView still needs to be updated (although it’s been kind of abandoned by the author). Due to significant changes in how data is stored, likely your space will be resynced to all your clients once you upgrade. Just in case you may also want to {[Space: Reindex]} your space. If things are really broken, try the {[Debug: Reset Client]} command. @@ -142,7 +146,7 @@ Due to significant changes in how data is stored, likely your space will be resy The big change in this release is that SilverBullet now supports two [[Client Modes|client modes]]: _online_ mode and _sync_ mode. Read more about them here: [[Client Modes]]. Other notable changes: -* Massive reshuffling of built-in [[🔌 Plugs]], splitting the old “core” plug into [[Plugs/Editor]], [[Plugs/Template]] and [[Plugs/Index]]. +* Massive reshuffling of built-in [[Plugs]], splitting the old “core” plug into [[Plugs/Editor]], [[Plugs/Template]] and [[Plugs/Index]]. * Directives in [[Live Preview]] now always take up a single line height. * [[Plugs/Tasks]] now support custom states (not just `[x]` and `[ ]`), for example: * [IN PROGRESS] An in progress task diff --git a/website/Commands.md b/website/Commands.md index f03700a..eb0c01f 100644 --- a/website/Commands.md +++ b/website/Commands.md @@ -1,3 +1,5 @@ -Commands define actions that SilverBullet can perform. They range from simple edit commands, such as {[Text: Bold]}, but may be more elaborate such as {[Page: Rename]}. At a technical level, all commands are implemented via [[Plugs]]. +Commands define actions that SilverBullet can perform. They range from simple edit commands, such as {[Text: Bold]}, but may be more elaborate such as {[Page: Rename]}. + +SilverBullet ships with a lot of commands built in, but custom ones can also be defined using [[Templates]] and [[Plugs]]. All available commands appear in the [[Command Palette]] but may have key bindings as well (these key bindings appear in the [[Command Palette]] and are configurable in [[SETTINGS]]). \ No newline at end of file diff --git a/website/Federation.md b/website/Federation.md index bd82c04..70b3e7c 100644 --- a/website/Federation.md +++ b/website/Federation.md @@ -4,7 +4,7 @@ This enables a few things: * **Linking and browsing** to other publicly hosted SilverBullet spaces (or websites adhering to its [[API]]). For instance the [[!silverbullet.md/CHANGELOG|SilverBullet CHANGELOG]] without leaving the comfort of your own SilverBullet client. * **Reusing** content from externally hosted sources, such as: - * _Templates_, e.g. by federating with `silverbullet.md/template` will give you access to the example templates hosted there without manually copying and pasting them and automatically pull in the latest version. So you can, for instance, use `render [[!silverbullet.md/template/page]]` to use the [[template/page]] template. See [[Template Sets]] for more on this use case. + * _Templates_, e.g. by federating with `silverbullet.md/template` will give you access to the example templates hosted there without manually copying and pasting them and automatically pull in the latest version. So you can, for instance, use `render [[!silverbullet.md/template/page]]` to use the [[Library/Core/Query/Page]] template. See [[Libraries]] for more on this use case. * _Data_: such as tasks, item, data hosted elsewhere that you want to query from your own space. **Note:** Federation does not support authentication yet, so all federated spaces need to be unauthenticated and will be _read-only_. diff --git a/website/Frontmatter.md b/website/Frontmatter.md index c8c04f7..2231ef7 100644 --- a/website/Frontmatter.md +++ b/website/Frontmatter.md @@ -19,6 +19,19 @@ Here is another example: ## This is a section This is content +For convenience, you may use the `attribute.subAttribute` notation, which internally will expand: + +```yaml +attribute.subAttribute: 10 +``` + +to + +```yaml +attribute: + subAttribute: 10 +``` + # Special attributes While SilverBullet allows arbitrary metadata to be added to pages, there are a few attributes with special meaning: diff --git a/website/Getting Started.md b/website/Getting Started.md index 7325581..8b5c379 100644 --- a/website/Getting Started.md +++ b/website/Getting Started.md @@ -1,17 +1,23 @@ +Welcome to SilverBullet. Since you’re starting fresh, you may want to kick off by importing the [[Library/Core]] [[Libraries|library]] of templates and pages. You can do so easily with the button below. Just push it — you know you want to! + +{[Library: Import|Import Core Library]("!silverbullet.md/Library/Core/")} + +Did that? Let’s proceed. + ## Getting started The best way to get a good feel for what SilverBullet is to immediately start playing with it. Here are some things for you to try: -* Click on the page picker (book icon) icon at the top right, or hit `Cmd-k` (Mac) or `Ctrl-k` (Linux and Windows) to open the **page switcher**. +* Click on the page picker (book icon) icon at the top right, or hit `Cmd-k` (Mac) or `Ctrl-k` (Linux and Windows) to open the [[Page Picker]]. * Type the name of a non-existent page to create it. * You _can_ create pages in folders (if you’re into that type of thing) simply by putting slashes (`/`) in the name (even on Windows), e.g. `My Folder/My Page`. Don’t worry about that folder existing, we’ll automatically create it if it doesn’t. -* Click on the terminal icon (top right), hit `Cmd-/` (Mac) or `Ctrl-/` (Linux and Windows), or tap the screen with 3 fingers at the same time (on mobile) to open the **command palette**. The {[Stats: Show]} one is a safe one to try. +* Click on the terminal icon (top right), or hit `Cmd-/` (Mac) or `Ctrl-/` (Linux and Windows), or tap the screen with 3 fingers at the same time (on mobile) to open the [[Command Palette]]. The {[Stats: Show]} one is a safe one to try. +* Click on the “T” icon (top right), or hit `Cmd-Shift-t` (Mac) or `Ctrl-Shift-t` (Linux and Windows) to open the [[Template Picker]] and see what templates you have installed (which should be a few after importing the Core library) * Select some text and hit `Alt-m` to ==highlight== it, or `Cmd-b` (Mac) or `Ctrl-b` (Windows/Linux) to make it **bold**, or `Cmd-i` (Mac) or `Ctrl-i` (Windows/Linux) to make it _italic_. * Click a link somewhere on this page to navigate there. When you link to a new page it will initially show up in red (to indicate it does not yet exist), but once you click it — you will create the page automatically (only for real when you actually enter some text). * Start typing `[[` somewhere to insert your own page link (with completion). * [ ] Tap this box 👈 to mark this task as done. * Start typing `:party` to trigger the emoji picker 🎉 * Type `/` somewhere in the text to invoke a **slash command**. -* Hit `Cmd-p` (Mac) or `Ctrl-p` (Windows, Linux) to show a preview for the current page on the side. * If this is matching your personality type, you can click this button {[Editor: Toggle Vim Mode]} to toggle Vim mode. If you cannot figure out how to exit it, just click that button again. _Phew!_ Notice that as you move your cursor around on this page and you get close to or “inside” marked up text, you will get to see the underlying [[Markdown]] code. This experience is what we refer to as “live preview” — generally your text looks clean, but you still can see what’s under the covers and edit it directly, as opposed to [WYSIWYG](https://en.wikipedia.org/wiki/WYSIWYG) that some other applications use. To move your cursor somewhere using your mouse without navigating or activating (e.g. a wiki, regular link, or a button) hold `Alt` when you click. Holding `Cmd` or `Ctrl` when clicking a link will open it in a new tab or window. @@ -36,4 +42,4 @@ Beyond that, you can find more information about SilverBullet on its official we 1. Through its [regular website link](https://silverbullet.md/) 2. Directly without leaving SilverBullet, through [[Federation]], just click on this: [[SilverBullet]] (note that all of these will be read-only, for obvious reasons) -To keep up with the latest and greatest going-ons in SilverBullet land, keep an eye on the [[CHANGELOG]], and regularly update your SilverBullet instance (`silverbullet upgrade` if you’re running the Deno version). If you run into any issues or have ideas on how to make SilverBullet even awesomer (yes, that’s a word), [join the conversation on GitHub](https://github.com/silverbulletmd/silverbullet). \ No newline at end of file +To keep up with the latest and greatest going-ons in SilverBullet land, keep an eye on the [[CHANGELOG]]. \ No newline at end of file diff --git a/website/Guide/Deployment/Cloudflare and Portainer.md b/website/Guide/Deployment/Cloudflare and Portainer.md deleted file mode 100644 index 3ed4fb6..0000000 --- a/website/Guide/Deployment/Cloudflare and Portainer.md +++ /dev/null @@ -1,247 +0,0 @@ -In this guide we will show you how to deploy silverbullet and cloudflare in containers, making them "talk/communicate" in the same private network just for them. - -This guide assumes that you have already deployed Portainer. If not, see [this official guide](https://docs.portainer.io/start/install-ce/server/docker/linux) from Portainer to deploy it on Linux. - -### Brief - -This guide will be divided into three parts, in the first we'll set up Silverbullet with Cloudflare. In the second, we will set up Cloudflare from the beginning to access Silverbullet from outside our LAN using [Tunnels](https://www.cloudflare.com/products/tunnel/). And in the third step, we protect our Silverbullet instance with [Access Zero Trust](https://www.cloudflare.com/products/zero-trust/access/) for authentication. - -# 1 - Deploy Silverbullet and Cloudflare in Portainer - -## Prepare the Template -We will prepare a template in Portainer where we will add the configuration of a ==docker-compose.yaml== that will run our containers, and we will be able to move the stack to another server/host if necessary using the same configuration. - -First, go to **Home** > (Your environment name, default is **local**) > **App Templates** > **Custom Templates** and click on the blue button in the right corner > "**Add Custom Template**". -![](create-custom-template.png) - -### Name - -Choose a name for the silverbullet stack, we chose "**silverbullet-docker**", very imaginative... 😊. - -### Description - -Fill the description with your own words; this is up to you because it is optional. - -### Icon Url - -Copy and paste this url to get the icon. ``https://raw.githubusercontent.com/silverbulletmd/silverbullet/main/web/images/logo.ico`` - -### Platform - -Choose Linux - -### Type - -Standalone - -### Build Method - -As for the Build method choose “**Web Editor**” and copy-paste this ==docker-compose.yaml== configuration: - -```yaml -version: '3.9' -services: - silverbullet: - image: zefhemel/silverbullet - container_name: silverbullet - restart: unless-stopped - ## To enable additional options, such as authentication, set environment variables, e.g. - environment: - - PUID=1000 - - PGID=1000 - #- SB_USER=username:1234 #feel free to remove this if not needed - volumes: - - space:/space:rw - ports: - - 3000:3000 - networks: - - silverbullet - - cloudflared: - container_name: cloudflared-tunnel - image: cloudflare/cloudflared - restart: unless-stopped - command: tunnel run - environment: - # If deploying in to Portainer add your token value here! - # If deploying manually create a ".env" file and add the variable and the value of the token. - - TUNNEL_TOKEN=your-token-value-here! - #- TUNNEL_TOKEN=${TUNNEL_TOKEN} - depends_on: - - silverbullet - networks: - - silverbullet - -networks: - silverbullet: - external: true - -volumes: - space: -``` - -We will replace "your-token-value-here" with a real token value in the next steps. - -Once you have this, go to the bottom of the page and click **Actions** > **Create Custom Template**. -![](create-custom-template-4.png) - -Now we have to build the network before we can deploy it. - -**NOTE***: If you got a *Error code 8: Attempt to write a readonly database* when running `docker compose up`. - -Ensure that the directory on the host system that is mounted as /space in your container has the correct permissions. For example: - -```shell -sudo chown -R 1000:1000 /path/to/space -sudo chmod -R 755 /path/to/space -``` - -## Create the network for silverbullet - -Go to **Home** > **Networks** > **Add Network**. - -### Name - -Choose "**silverbullet**" because that is the name we are already using in the ==docker-compose.yaml==. - -You can leave all the other options by default or change them to suit your network needs. - -![](create-network-1.png) -![](create-network-2.png) -Click **Create Network** at the bottom of the page. -![](create-network-4.png) - -## Deploying the Stack - -Go to **Home** > **Local** > **App Templates** > **Custom Templates**. - -Go into the **silverbullet-docker** and click on **Edit**. -![](deploy-stack-3.png) -Click on **Deploy the stack**. -![](deploy-stack-2.png) -Give it a few seconds and you will get a notification that both containers are running. 😇 - -Only the silverbullet container should be working properly by this point, as we haven't finished with Cloudflare yet. -![](view-containers-1.png) - -## Verification - -In a web browser in your local network (if your server is in your LAN) write the IP address of your server and add the port 3000 at the end, like this: -``http://your-ip-address:3000 `` - -Right now the connection to silverbullet is **HTTP** and PWA([Progressive Web Apps](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps)) and offline mode will not work yet. Don’t worry we will get into that later, but for now, it should be working correctly. Try to type something and sync it to your server. - ---- - -# 2 - Set up Cloudflare with Tunnels. - -Now we are going to use Cloudflare to be able to connect to SilverBullet from outside our network and have a valid SSL certificate without opening any ports or needing a static IPv4 address from our ISP or changing our router configuration. - -You will need three things: - -* An account with Cloudflare ☁️. -* A debit/credit card 💳. -* A domain name (you can buy it on [Njalla](https://njal.la/) 😉. Your real name will not be shown if someone uses whois tools). - -We assume you've already [signed up to Cloudflare](https://www.cloudflare.com/), if not you can go and do it now. It's free but you'll need to add a real debit/credit card to have access to the tunnels and zero access. If you don't want to do that, you can use **alternatives** like [Caddy](https://caddyserver.com/docs/quick-starts/reverse-proxy) or [Nginx](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) for reverse proxy and [Authelia](https://www.authelia.com/) or you can use the [basic authentication built-in](https://silverbullet.md/Authentication) for authentication. - -## Add your Site/Domain Name to Cloudflare - -Follow the [official docs](https://developers.cloudflare.com/fundamentals/get-started/setup/add-site/) of Cloudflare on how to add a site, it's really easy, just remember to change the name servers (DNS) to the ones suggested by Cloudflare in the website where you bought your domain name. -![](create-site-cloudflare-1.png) -Like this (This is Njalla config panel) -![](create-site-cloudflare-custom_dns.png) - -## Setup Tunnel - -Without opening any ports or touching the firewall, we set up this tunnel to connect it to our server. - -Click on **Zero Trust** once you have added your site/domain name. -![](setup-tunnel-1.png) -Click on **Create Tunnel**. -![](setup-tunnels-2.png) -Choose a name for your tunnel, I chose "myhome", very imaginative again 😛. And then click on **Save Tunnel**. -![](setup-tunnels-3.png) - -Since we have already set up a container of Cloudflare, just copy the token you are given. And be careful, if someone gets your token they will be able to make a tunnel connection to your server. -![](setup-tunnels-4_2.png) - -Now that you have the token value of your tunnel, it's time to configure the cloudflare container in Portainer. Let's go there. - -Go to **App Templates** > **Custom Templates** > **Edit**. -![](deploy-stack-3.png) -Replace “your-token-value-here!” with your token value. -![](setup-tunnels-6.png) -Click on **Update the template**.**** - -Next, go to **Stacks** and click on the stack “**silverbullet-docker**”, or the name of your choice, then click **Remove**. -![](remove-stack-1.png) -Click **Remove** to confirm. Don't worry, this will only remove the stack and the containers attached to it, not the template. -![](remove-stack-2.png) -Then go to **App Templates**. - -Go into the **silverbullet-docker** and click on **Edit**. -![](deploy-stack-3.png) -Click **Deploy Stack**. -![](deploy-stack-2.png) -Come back to Cloudflare and in the Connectors section you will see that a connection has been made to your server. Click **Next**. -![](setup-tunnels-7.png) -Click **Add a public hostname**. -![](setup-tunnels-9.png) -Fill in the **subdomain** field with the name you want to use to access silverbullet. Choose your domain name and for **Type** choose **HTTP** and the **URL** should be **silverbullet:3000**. - -![](setup-tunnels-11.png) -Check now with **silberbullet.your-domain-name.com**. You should be able to access it. - -# 3 - Set up Cloudflare Zero Access Trust (Auth). - -We assume you've already [signed up to Cloudflare](https://www.cloudflare.com/), if not you can go and do it now, it's free but you'll need to add a real debit/credit card to have access to the tunnels and zero access. If you don't want to do that, you can use **alternatives** like [Caddy](https://caddyserver.com/docs/quick-starts/reverse-proxy) or [Nginx](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) for reverse proxy and [Authelia](https://www.authelia.com/) or you can use the [BasicAuth build-in](https://silverbullet.md/Authentication) for authentication. - -Go to **Access** > **Applications** and click **Add an application** from the Zero Trust panel. -![](add-application-clodflare-3.png) - -Select **Self-Hosted**. -![](add-application-clodflare-2.png) -Choose a name for your application and use the same name for the subdomain you chose in the previous steps. In our case both are **silverbullet**. -![](add-application-clodflare-4.png) -Leave the rest of the page as default and click **Next** at the bottom of the page. - -Now it's time to select the name of the policy, the action and the duration of the session. - -Select a descriptive **Name** for future troubleshooting, select **Allow** for the **Action** and leave the session duration at its default. - -In the **Configure rules** section, select **Emails** if you want to use emails (or you can use a range of IPs, specific countries...) for verification, and enter the emails you want to allow access to Silverbullet. -![](add-application-clodflare-5.png) -Leave the rest of the page as default and click **Next** at the bottom of the page. - -On the next page, leave everything as default and click on **Add Application** at the bottom of the page. - -Go to **silverbullet.your-domain-name.com** and you should see a page like this: -![](add-application-clodflare-6.png) -Going back to the Zero Trust overview, we are now going to create some special rules to allow some specific files from silverbullet without authentication. The same thing happens with other auth applications such as [Authelia](https://silverbullet.md/Authelia). - -Create a new self-hosted application in Cloudflare, we suggest the name **silverbullet bypass**. - -And add the following **paths**: - -``` -.client/manifest.json -.client/[a-zA-Z0-9_-]+.png -service_worker.js -``` - -Leave the rest as default and click **Next** at the bottom of the page. -![](add-application-clodflare-7.png) -For the policy name we suggest **silverbullet bypass paths**, as for the **Action** you need to select **Bypass**, and in the Configure Rules **Select** **Everyone** or you can exclude a range of IP's or countries if required. - -Leave the rest as default and click **Next** at the bottom of the page. -![](add-application-clodflare-8.png) -These rules only take effect on the specific paths, you can read more about [Policy inheritance on Cloudflare.](https://developers.cloudflare.com/cloudflare-one/policies/access/app-paths/) - -On the next page, leave everything as default and click on **Add Application** at the bottom of the page. - -Go and check your **silberbullet.your-domain-name.com** everything should be working correctly. - -Now the connection to silverbullet is **HTTPS** and PWA ([Progressive Web Apps](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps)) and offline mode will work. - -I hope this guide has been helpful. diff --git a/website/Guide/Deployment/Cloudflare and Portainer/add-application-clodflare-2.png b/website/Guide/Deployment/Cloudflare and Portainer/add-application-clodflare-2.png deleted file mode 100644 index 2376eeb..0000000 Binary files a/website/Guide/Deployment/Cloudflare and Portainer/add-application-clodflare-2.png and /dev/null differ diff --git a/website/Guide/Deployment/Cloudflare and Portainer/add-application-clodflare-3.png b/website/Guide/Deployment/Cloudflare and Portainer/add-application-clodflare-3.png deleted file mode 100644 index a5b4541..0000000 Binary files a/website/Guide/Deployment/Cloudflare and Portainer/add-application-clodflare-3.png and /dev/null differ diff --git a/website/Guide/Deployment/Cloudflare and Portainer/add-application-clodflare-4.png b/website/Guide/Deployment/Cloudflare and Portainer/add-application-clodflare-4.png deleted file mode 100644 index 61f58eb..0000000 Binary files a/website/Guide/Deployment/Cloudflare and Portainer/add-application-clodflare-4.png and /dev/null differ diff --git a/website/Guide/Deployment/Cloudflare and Portainer/add-application-clodflare-5.png b/website/Guide/Deployment/Cloudflare and Portainer/add-application-clodflare-5.png deleted file mode 100644 index c352a72..0000000 Binary files a/website/Guide/Deployment/Cloudflare and Portainer/add-application-clodflare-5.png and /dev/null differ diff --git a/website/Guide/Deployment/Cloudflare and Portainer/add-application-clodflare-6.png b/website/Guide/Deployment/Cloudflare and Portainer/add-application-clodflare-6.png deleted file mode 100644 index 4540502..0000000 Binary files a/website/Guide/Deployment/Cloudflare and Portainer/add-application-clodflare-6.png and /dev/null differ diff --git a/website/Guide/Deployment/Cloudflare and Portainer/add-application-clodflare-7.png b/website/Guide/Deployment/Cloudflare and Portainer/add-application-clodflare-7.png deleted file mode 100644 index edea008..0000000 Binary files a/website/Guide/Deployment/Cloudflare and Portainer/add-application-clodflare-7.png and /dev/null differ diff --git a/website/Guide/Deployment/Cloudflare and Portainer/add-application-clodflare-8.png b/website/Guide/Deployment/Cloudflare and Portainer/add-application-clodflare-8.png deleted file mode 100644 index a12c952..0000000 Binary files a/website/Guide/Deployment/Cloudflare and Portainer/add-application-clodflare-8.png and /dev/null differ diff --git a/website/Guide/Deployment/Cloudflare and Portainer/create-custom-template-4.png b/website/Guide/Deployment/Cloudflare and Portainer/create-custom-template-4.png deleted file mode 100644 index bea2056..0000000 Binary files a/website/Guide/Deployment/Cloudflare and Portainer/create-custom-template-4.png and /dev/null differ diff --git a/website/Guide/Deployment/Cloudflare and Portainer/create-custom-template.png b/website/Guide/Deployment/Cloudflare and Portainer/create-custom-template.png deleted file mode 100644 index fc2513e..0000000 Binary files a/website/Guide/Deployment/Cloudflare and Portainer/create-custom-template.png and /dev/null differ diff --git a/website/Guide/Deployment/Cloudflare and Portainer/create-network-1.png b/website/Guide/Deployment/Cloudflare and Portainer/create-network-1.png deleted file mode 100644 index 9a2cd0c..0000000 Binary files a/website/Guide/Deployment/Cloudflare and Portainer/create-network-1.png and /dev/null differ diff --git a/website/Guide/Deployment/Cloudflare and Portainer/create-network-2.png b/website/Guide/Deployment/Cloudflare and Portainer/create-network-2.png deleted file mode 100644 index bd76c55..0000000 Binary files a/website/Guide/Deployment/Cloudflare and Portainer/create-network-2.png and /dev/null differ diff --git a/website/Guide/Deployment/Cloudflare and Portainer/create-network-4.png b/website/Guide/Deployment/Cloudflare and Portainer/create-network-4.png deleted file mode 100644 index 673e767..0000000 Binary files a/website/Guide/Deployment/Cloudflare and Portainer/create-network-4.png and /dev/null differ diff --git a/website/Guide/Deployment/Cloudflare and Portainer/create-site-cloudflare-1.png b/website/Guide/Deployment/Cloudflare and Portainer/create-site-cloudflare-1.png deleted file mode 100644 index f330ec8..0000000 Binary files a/website/Guide/Deployment/Cloudflare and Portainer/create-site-cloudflare-1.png and /dev/null differ diff --git a/website/Guide/Deployment/Cloudflare and Portainer/create-site-cloudflare-custom_dns.png b/website/Guide/Deployment/Cloudflare and Portainer/create-site-cloudflare-custom_dns.png deleted file mode 100644 index 6f91fb9..0000000 Binary files a/website/Guide/Deployment/Cloudflare and Portainer/create-site-cloudflare-custom_dns.png and /dev/null differ diff --git a/website/Guide/Deployment/Cloudflare and Portainer/deploy-stack-2.png b/website/Guide/Deployment/Cloudflare and Portainer/deploy-stack-2.png deleted file mode 100644 index 7adc86b..0000000 Binary files a/website/Guide/Deployment/Cloudflare and Portainer/deploy-stack-2.png and /dev/null differ diff --git a/website/Guide/Deployment/Cloudflare and Portainer/deploy-stack-3.png b/website/Guide/Deployment/Cloudflare and Portainer/deploy-stack-3.png deleted file mode 100644 index aea5c62..0000000 Binary files a/website/Guide/Deployment/Cloudflare and Portainer/deploy-stack-3.png and /dev/null differ diff --git a/website/Guide/Deployment/Cloudflare and Portainer/remove-stack-1.png b/website/Guide/Deployment/Cloudflare and Portainer/remove-stack-1.png deleted file mode 100644 index fa37480..0000000 Binary files a/website/Guide/Deployment/Cloudflare and Portainer/remove-stack-1.png and /dev/null differ diff --git a/website/Guide/Deployment/Cloudflare and Portainer/remove-stack-2.png b/website/Guide/Deployment/Cloudflare and Portainer/remove-stack-2.png deleted file mode 100644 index 6ce1b43..0000000 Binary files a/website/Guide/Deployment/Cloudflare and Portainer/remove-stack-2.png and /dev/null differ diff --git a/website/Guide/Deployment/Cloudflare and Portainer/setup-tunnel-1.png b/website/Guide/Deployment/Cloudflare and Portainer/setup-tunnel-1.png deleted file mode 100644 index c4f72f0..0000000 Binary files a/website/Guide/Deployment/Cloudflare and Portainer/setup-tunnel-1.png and /dev/null differ diff --git a/website/Guide/Deployment/Cloudflare and Portainer/setup-tunnels-11.png b/website/Guide/Deployment/Cloudflare and Portainer/setup-tunnels-11.png deleted file mode 100644 index 5a7810c..0000000 Binary files a/website/Guide/Deployment/Cloudflare and Portainer/setup-tunnels-11.png and /dev/null differ diff --git a/website/Guide/Deployment/Cloudflare and Portainer/setup-tunnels-2.png b/website/Guide/Deployment/Cloudflare and Portainer/setup-tunnels-2.png deleted file mode 100644 index 5410e80..0000000 Binary files a/website/Guide/Deployment/Cloudflare and Portainer/setup-tunnels-2.png and /dev/null differ diff --git a/website/Guide/Deployment/Cloudflare and Portainer/setup-tunnels-3.png b/website/Guide/Deployment/Cloudflare and Portainer/setup-tunnels-3.png deleted file mode 100644 index 5410e80..0000000 Binary files a/website/Guide/Deployment/Cloudflare and Portainer/setup-tunnels-3.png and /dev/null differ diff --git a/website/Guide/Deployment/Cloudflare and Portainer/setup-tunnels-4_2.png b/website/Guide/Deployment/Cloudflare and Portainer/setup-tunnels-4_2.png deleted file mode 100644 index bf171b8..0000000 Binary files a/website/Guide/Deployment/Cloudflare and Portainer/setup-tunnels-4_2.png and /dev/null differ diff --git a/website/Guide/Deployment/Cloudflare and Portainer/setup-tunnels-6.png b/website/Guide/Deployment/Cloudflare and Portainer/setup-tunnels-6.png deleted file mode 100644 index 413ff28..0000000 Binary files a/website/Guide/Deployment/Cloudflare and Portainer/setup-tunnels-6.png and /dev/null differ diff --git a/website/Guide/Deployment/Cloudflare and Portainer/setup-tunnels-7.png b/website/Guide/Deployment/Cloudflare and Portainer/setup-tunnels-7.png deleted file mode 100644 index 8bfd946..0000000 Binary files a/website/Guide/Deployment/Cloudflare and Portainer/setup-tunnels-7.png and /dev/null differ diff --git a/website/Guide/Deployment/Cloudflare and Portainer/setup-tunnels-9.png b/website/Guide/Deployment/Cloudflare and Portainer/setup-tunnels-9.png deleted file mode 100644 index 02fca12..0000000 Binary files a/website/Guide/Deployment/Cloudflare and Portainer/setup-tunnels-9.png and /dev/null differ diff --git a/website/Guide/Deployment/Cloudflare and Portainer/view-containers-1.png b/website/Guide/Deployment/Cloudflare and Portainer/view-containers-1.png deleted file mode 100644 index 8faada6..0000000 Binary files a/website/Guide/Deployment/Cloudflare and Portainer/view-containers-1.png and /dev/null differ diff --git a/website/Install/Network and Internet.md b/website/Install/Network and Internet.md index bae3b17..d52001f 100644 --- a/website/Install/Network and Internet.md +++ b/website/Install/Network and Internet.md @@ -14,4 +14,3 @@ People have found various simple to more complex ways of achieving this. * Using [[Deployments/ngrok]] is likely the easiest solution to exposing your _locally running_ SilverBullet to the Internet. Note that “locally running” can mean your own local machine, but can still refer to running it on a server in your network (like a Raspberry Pi). * [[Deployments/Caddy]]: the easiest solution to expose SilverBullet running on a publicly accessible server to the Internet (but local network as well using Tailscale) * [[Authelia]] setup hints -* [[Guide/Deployment/Cloudflare and Portainer]] diff --git a/website/Keyboard Shortcuts.md b/website/Keyboard Shortcuts.md index d2c145e..6f49a69 100644 --- a/website/Keyboard Shortcuts.md +++ b/website/Keyboard Shortcuts.md @@ -9,8 +9,6 @@ Particularly useful keyboard shortcuts (that you may not know about). | Cmd-z/Ctrl-z | Undo the latest change | | Cmd-u/Ctrl-u | Go one change ahead | | Alt-h | Navigate to the home page | -| Ctrl-Alt-t | Toggle table of contents| -| Ctrl-Alt-m | Toggle mentions | | Cmd-Shift-f/Ctrl-Shift-f | Search for text across your entire space | # System | Combination (Mac/Win-Linux) | Action | @@ -20,8 +18,6 @@ Particularly useful keyboard shortcuts (that you may not know about). | Cmd-Shift-p/Ctrl-Shift-p | Update plugs (from the `PLUGS` file) | | Alt-q | Refresh all live queries and templates on this page | | Cmd-p/Ctrl-p | Toggle markdown preview | -| Ctrl-Alt-t | Toggle table of contents| -| Ctrl-Alt-m | Toggle mentions | | Cmd-Shift-f/Ctrl-Shift-f | Search for text across your entire space | # Navigation diff --git a/website/Libraries.md b/website/Libraries.md new file mode 100644 index 0000000..31e8b31 --- /dev/null +++ b/website/Libraries.md @@ -0,0 +1,16 @@ +A lot of useful functionality in SilverBullet is implemented through [[Templates]], as well as regular [[Pages]]. Some of these you will create yourself for your own specific use, but many are generic and generally useful. Libraries offer a way to _distribute_ sets of templates and pages easily. + +# What’s in a library +Here are some things that a library may provide: +* Various [[Slash Commands]], such as `/today`, `/task`, `/table`. +* Useful [[Page Templates]] +* Useful widgets such as [[Table of Contents]] or [[Linked Mentions]] +* Useful pages that help you perform maintenance on your space, like detecting broken links, such as [[Library/Core/Page/Maintenance]]. + +# What libraries are on offer? +Libraries are still a young concept in SilverBullet and therefore we’re still exploring how to organize and structure these. + +Currently, we have the following libraries available: + +* [[Library/Core]]: this is the library you want to import _for sure_. Just do it. +* [[Library/Journal]]: for the journalers among us. diff --git a/website/Library/Core.md b/website/Library/Core.md new file mode 100644 index 0000000..b597a1f --- /dev/null +++ b/website/Library/Core.md @@ -0,0 +1,25 @@ +This library is highly recommended for everybody to import immediately. It provides a lot of functionality you’ll likely appreciate (and may be used to having out of the box, if you’re longer-term SilverBullet user). + +Some random examples: +* [[Table of Contents]] and [[Linked Mentions]] +* All the slash commands you know and love, ranging from `/h1` to `/task` to `/table` to `/code` to `/query` to `/template` to `/today` to... +* Some useful general purpose pages such as [[Library/Core/Page/Maintenance]] and [[Library/Core/Page/Template Index]]. + +# Installation +To import this library, run the {[Library: Import]} command in your SilverBullet space and enter: + + !silverbullet.md/Library/Core/ + +# Included templates +```query +template +where name =~ /^{{escapeRegexp @page.name}}\// +render [[Library/Core/Query/Template]] +``` + +# Included utility pages +```query +page +where name =~ /^{{escapeRegexp @page.name}}\// and tags != "template" +render [[Library/Core/Query/Page]] +``` diff --git a/website/Library/Core/New Page/Page Template.md b/website/Library/Core/New Page/Page Template.md new file mode 100644 index 0000000..4f7d40e --- /dev/null +++ b/website/Library/Core/New Page/Page Template.md @@ -0,0 +1,13 @@ +--- +description: Define a new Page Template +tags: template +hooks.newPage: + suggestedName: Library/Personal/Page/ + forPrefix: Library/Personal/Page/ + confirmName: true +frontmatter: | + tags: template + displayName: "|^|" + hooks.newPage: + suggestedName: Library/Personal/Page/ +--- diff --git a/website/Library/Core/New Page/Quick Note.md b/website/Library/Core/New Page/Quick Note.md new file mode 100644 index 0000000..06617c1 --- /dev/null +++ b/website/Library/Core/New Page/Quick Note.md @@ -0,0 +1,9 @@ +--- +description: "Create a quick note" +tags: template +hooks.newPage: + suggestedName: "Inbox/{{today}} {{time}}" + confirmName: false + command: "Quick Note" + key: "Alt-Shift-n" +--- diff --git a/website/Library/Core/New Page/Snippet Template.md b/website/Library/Core/New Page/Snippet Template.md new file mode 100644 index 0000000..b3ea759 --- /dev/null +++ b/website/Library/Core/New Page/Snippet Template.md @@ -0,0 +1,12 @@ +--- +tags: template +hooks.newPage: + suggestedName: "template/snippet/" + confirmName: true + forPrefix: template/snippet/ +description: Define a new Snippet Template +frontmatter: | + tags: template + hooks.snippet: + name: "|^|" +--- diff --git a/website/Library/Core/New Page/Template.md b/website/Library/Core/New Page/Template.md new file mode 100644 index 0000000..10d84e6 --- /dev/null +++ b/website/Library/Core/New Page/Template.md @@ -0,0 +1,10 @@ +--- +tags: template +description: Define a new template +hooks.newPage: + suggestedName: template/ + forPrefix: template/ +frontmatter: + tags: template +--- +|^| \ No newline at end of file diff --git a/website/Library/Core/Page/Maintenance.md b/website/Library/Core/Page/Maintenance.md new file mode 100644 index 0000000..cde943d --- /dev/null +++ b/website/Library/Core/Page/Maintenance.md @@ -0,0 +1,36 @@ +We would like to keep our space clean, these are some tools that help you do that. + +# Broken links +This shows all internal links that are broken. + +```template +query: | + link where toPage and pageExists(toPage) = false +template: | + {{#if .}} + {{#each .}} + * [[{{ref}}]]: broken link to [[{{toPage}}]] + {{/each}} + {{else}} + No broken links, all good! + {{/if}} +``` + +# Conflict copies +These are pages that have conflicted copies (as a result of sync), have a look at them as well as their original (non-conflicted versions) and decide which one to keep. + +```template +query: | + page where name =~ /\.conflicted\.\d+$/ + select + name as conflictedName, + replace(name, /\.conflicted\.\d+$/, "") as cleanName +template: | + {{#if .}} + {{#each .}} + * [[{{cleanName}}]]: confict copy [[{{conflictedName}}]] + {{/each}} + {{else}} + No conflicting pages! + {{/if}} +``` diff --git a/website/Library/Core/Page/Template Index.md b/website/Library/Core/Page/Template Index.md new file mode 100644 index 0000000..4758c02 --- /dev/null +++ b/website/Library/Core/Page/Template Index.md @@ -0,0 +1,47 @@ +This page lists all templates currently available in your space. + +# New Page +$newPage +These [[!silverbullet.md/Page Templates]] are available through the {[Page: From Template]} command. + +```query +template where hooks.newPage render [[Library/Core/Query/Template]] +``` + +# Snippets +$snippets +These can be used as [[!silverbullet.md/Snippets]] via [[!silverbullet.md/Slash Commands]]: + +```query +template where hooks.snippet render [[Library/Core/Query/Template]] +``` + +# Widgets +$widgets +Widgets can either be automatically attached to the top or bottom of pages (matching certain criteria) or used inline via [[!silverbullet.md/Live Templates]]. + +## Top +```query +template +where hooks.top +order by order +render [[Library/Core/Query/Template]] +``` + +## Bottom +```query +template +where hooks.bottom +order by order +render [[Library/Core/Query/Template]] +``` + +## Inline +Use these as `page` in [[!silverbullet.md/Live Templates]] to render useful things in your pages: + +```query +template +where name =~ /\/Widget\// and hooks.top = null and hooks.bottom = null +order by order +render [[Library/Core/Query/Template]] +``` diff --git a/website/template/pages/page.md b/website/Library/Core/Query/Page.md similarity index 98% rename from website/template/pages/page.md rename to website/Library/Core/Query/Page.md index 6df0f78..ee67153 100644 --- a/website/template/pages/page.md +++ b/website/Library/Core/Query/Page.md @@ -2,5 +2,4 @@ tags: template description: A page reference link as a list item --- - * [[{{name}}]] \ No newline at end of file diff --git a/website/template/task.md b/website/Library/Core/Query/Task.md similarity index 50% rename from website/template/task.md rename to website/Library/Core/Query/Task.md index 45e19f3..4c80103 100644 --- a/website/template/task.md +++ b/website/Library/Core/Query/Task.md @@ -1,7 +1,5 @@ --- tags: template -type: query -description: generic task template that supports updating the status back in the origin page -order: 1 +description: Generic task template that supports updating the status back in the origin page --- * [{{state}}] [[{{ref}}]] {{name}} \ No newline at end of file diff --git a/website/Library/Core/Query/Template.md b/website/Library/Core/Query/Template.md new file mode 100644 index 0000000..0bf123a --- /dev/null +++ b/website/Library/Core/Query/Template.md @@ -0,0 +1,5 @@ +#template +* [[{{ref}}]] {{description}} +{{#if usage}} + * **Usage:** {{usage}} +{{/if}} \ No newline at end of file diff --git a/website/Library/Core/Snippet/code.md b/website/Library/Core/Snippet/code.md new file mode 100644 index 0000000..4c75c52 --- /dev/null +++ b/website/Library/Core/Snippet/code.md @@ -0,0 +1,8 @@ +--- +description: Insert a fenced code block +tags: template +hooks.snippet.slashCommand: code +--- +```|^| + +``` diff --git a/website/Library/Core/Snippet/frontmatter.md b/website/Library/Core/Snippet/frontmatter.md new file mode 100644 index 0000000..4e7d87b --- /dev/null +++ b/website/Library/Core/Snippet/frontmatter.md @@ -0,0 +1,11 @@ +--- +tags: template +description: Insert Frontmatter +hooks.snippet: + slashCommand: frontmatter + insertAt: page-start + command: "Insert Frontmatter" +--- +--- +|^| +--- diff --git a/website/Library/Core/Snippet/h1.md b/website/Library/Core/Snippet/h1.md new file mode 100644 index 0000000..0b4a3d0 --- /dev/null +++ b/website/Library/Core/Snippet/h1.md @@ -0,0 +1,8 @@ +--- +tags: template +description: Make this a level 1 heading +hooks.snippet: + slashCommand: h1 + match: "^#*\\s*" +--- +# \ No newline at end of file diff --git a/website/Library/Core/Snippet/h2.md b/website/Library/Core/Snippet/h2.md new file mode 100644 index 0000000..b8c31ae --- /dev/null +++ b/website/Library/Core/Snippet/h2.md @@ -0,0 +1,8 @@ +--- +tags: template +description: Make this a level 2 heading +hooks.snippet: + slashCommand: h2 + match: "^#*\\s*" +--- +## \ No newline at end of file diff --git a/website/Library/Core/Snippet/h3.md b/website/Library/Core/Snippet/h3.md new file mode 100644 index 0000000..b918906 --- /dev/null +++ b/website/Library/Core/Snippet/h3.md @@ -0,0 +1,8 @@ +--- +tags: template +description: Make this a level 3 heading +hooks.snippet: + slashCommand: h3 + match: "^#*\\s*" +--- +### \ No newline at end of file diff --git a/website/Library/Core/Snippet/h4.md b/website/Library/Core/Snippet/h4.md new file mode 100644 index 0000000..c0761eb --- /dev/null +++ b/website/Library/Core/Snippet/h4.md @@ -0,0 +1,8 @@ +--- +tags: template +description: Make this a level 4 heading +hooks.snippet: + slashCommand: h4 + match: "^#*\\s*" +--- +#### \ No newline at end of file diff --git a/website/Library/Core/Snippet/hr.md b/website/Library/Core/Snippet/hr.md new file mode 100644 index 0000000..b2ce4af --- /dev/null +++ b/website/Library/Core/Snippet/hr.md @@ -0,0 +1,6 @@ +--- +tags: template +description: Insert a horizontal rule +hooks.snippet.slashCommand: hr +--- +--- diff --git a/website/template/slash/note-admonition.md b/website/Library/Core/Snippet/note-admonition.md similarity index 54% rename from website/template/slash/note-admonition.md rename to website/Library/Core/Snippet/note-admonition.md index 52a46d8..f02355c 100644 --- a/website/template/slash/note-admonition.md +++ b/website/Library/Core/Snippet/note-admonition.md @@ -1,9 +1,7 @@ --- tags: template -type: slash -trigger: "note-admonition" -displayName: "/note-admonition" description: Insert a "note" admonition +hooks.snippet.slashCommand: note-admonition --- > **note** Note > |^| diff --git a/website/Library/Core/Snippet/query.md b/website/Library/Core/Snippet/query.md new file mode 100644 index 0000000..0648b34 --- /dev/null +++ b/website/Library/Core/Snippet/query.md @@ -0,0 +1,8 @@ +--- +description: Insert a live query +tags: template +hooks.snippet.slashCommand: query +--- +```query +|^| +``` diff --git a/website/Library/Core/Snippet/table.md b/website/Library/Core/Snippet/table.md new file mode 100644 index 0000000..0761950 --- /dev/null +++ b/website/Library/Core/Snippet/table.md @@ -0,0 +1,8 @@ +--- +tags: template +description: Insert a table +hooks.snippet.slashCommand: table +--- +| Header A | Header B | +|----------|----------| +| Cell A|^| | Cell B | \ No newline at end of file diff --git a/website/Library/Core/Snippet/task.md b/website/Library/Core/Snippet/task.md new file mode 100644 index 0000000..07b902b --- /dev/null +++ b/website/Library/Core/Snippet/task.md @@ -0,0 +1,10 @@ +--- +tags: template +description: Turn the current line into a task +hooks.snippet: + slashCommand: task + matchRegex: "^(\\s*)[\\-\\*]?\\s*(\\[[ xX]\\])?\\s*" + command: "Turn into task" + key: "Ctrl-q t" +--- +$1* [ ] \ No newline at end of file diff --git a/website/Library/Core/Snippet/template.md b/website/Library/Core/Snippet/template.md new file mode 100644 index 0000000..24efd95 --- /dev/null +++ b/website/Library/Core/Snippet/template.md @@ -0,0 +1,8 @@ +--- +description: Insert an inline template +tags: template +hooks.snippet.slashCommand: "template" +--- +```template +page: "[[|^|]]" +``` diff --git a/website/Library/Core/Snippet/today.md b/website/Library/Core/Snippet/today.md new file mode 100644 index 0000000..a8f31e7 --- /dev/null +++ b/website/Library/Core/Snippet/today.md @@ -0,0 +1,6 @@ +--- +tags: template +description: "Today's date" +hooks.snippet.slashCommand: today +--- +{{today}} \ No newline at end of file diff --git a/website/Library/Core/Snippet/tomorrow.md b/website/Library/Core/Snippet/tomorrow.md new file mode 100644 index 0000000..82fb4c6 --- /dev/null +++ b/website/Library/Core/Snippet/tomorrow.md @@ -0,0 +1,6 @@ +--- +tags: template +description: "Tomorrow's date" +hooks.snippet.slashCommand: tomorrow +--- +{{tomorrow}} \ No newline at end of file diff --git a/website/template/slash/warning-admonition.md b/website/Library/Core/Snippet/warning-admonition.md similarity index 54% rename from website/template/slash/warning-admonition.md rename to website/Library/Core/Snippet/warning-admonition.md index f69e5e7..b1ddfc1 100644 --- a/website/template/slash/warning-admonition.md +++ b/website/Library/Core/Snippet/warning-admonition.md @@ -1,9 +1,7 @@ --- -tags: template -type: slash -trigger: "warning-admonition" -displayName: "/warning-admonition" description: Insert a "warning" admonition +tags: template +hooks.snippet.slashCommand: warning-admonition --- > **warning** Warning > |^| diff --git a/website/template/widget/federated-template-copy.md b/website/Library/Core/Widget/Federated Template Copy.md similarity index 81% rename from website/template/widget/federated-template-copy.md rename to website/Library/Core/Widget/Federated Template Copy.md index 25aa4ca..5bb011c 100644 --- a/website/template/widget/federated-template-copy.md +++ b/website/Library/Core/Widget/Federated Template Copy.md @@ -3,8 +3,7 @@ description: > Adds a convenient "Copy to my space" button to any template found via federation tags: template -type: widget:bottom -where: 'name =~ /^!/ and tags="template"' +hooks.bottom.where: 'name =~ /^!/ and tags="template"' --- # Template actions * {[Page: Copy|Copy to my space]}: use this template in your space by making a local copy of it (and tweaking it as you like) \ No newline at end of file diff --git a/website/template/widget/linked-mentions.md b/website/Library/Core/Widget/Linked Mentions.md similarity index 82% rename from website/template/widget/linked-mentions.md rename to website/Library/Core/Widget/Linked Mentions.md index 69cdc6b..a2c9554 100644 --- a/website/template/widget/linked-mentions.md +++ b/website/Library/Core/Widget/Linked Mentions.md @@ -1,8 +1,7 @@ --- -description: Adds Linked Mentions to all pages +description: Adds Linked Mentions to pages tags: template -type: widget:bottom -where: 'true' +hooks.bottom.where: 'true' --- ```template # We need to escape handlebars directives here, since we're embedding diff --git a/website/Library/Core/Widget/Linked Tasks.md b/website/Library/Core/Widget/Linked Tasks.md new file mode 100644 index 0000000..57412e2 --- /dev/null +++ b/website/Library/Core/Widget/Linked Tasks.md @@ -0,0 +1,9 @@ +--- +tags: template +description: | + Shows all tasks that contain a link the current page. For instance a task that references `[[John]]` in its name, would appear on the `John` page. +--- + +```query +task where name =~ /\[\[{{escapeRegexp @page.name}}\]\]/ where done = false render [[Library/Core/Query/Task]] +``` \ No newline at end of file diff --git a/website/Library/Core/Widget/Table of Contents.md b/website/Library/Core/Widget/Table of Contents.md new file mode 100644 index 0000000..06052d2 --- /dev/null +++ b/website/Library/Core/Widget/Table of Contents.md @@ -0,0 +1,11 @@ +--- +description: Adds a Table of Contents to pages +tags: template +hooks.top: + where: 'true' + # Show all the way at the top + order: 0 +--- +```toc +minHeaders: 3 +``` diff --git a/website/template/live/tagged-tasks.md b/website/Library/Core/Widget/Tagged Tasks.md similarity index 63% rename from website/template/live/tagged-tasks.md rename to website/Library/Core/Widget/Tagged Tasks.md index a3b27d4..f9d4bd1 100644 --- a/website/template/live/tagged-tasks.md +++ b/website/Library/Core/Widget/Tagged Tasks.md @@ -1,11 +1,9 @@ --- tags: template -type: live description: Queries all tasks tagged with a specific tag. usage: Pass in the tag to filter on as the `value` of this template -order: 2 --- ```query -task where tags = "{{.}}" and done = false render [[template/tasks/task]] +task where tags = "{{.}}" and done = false render [[Library/Core/Query/Task]] ``` diff --git a/website/Library/Journal.md b/website/Library/Journal.md new file mode 100644 index 0000000..f10fa23 --- /dev/null +++ b/website/Library/Journal.md @@ -0,0 +1,28 @@ +This library contains some useful page templates for journalers. Want to easily create a daily or weekly note? These templates can get you started. Instantiate them via {[Page: From Template]}. + +# Installation +To import this library, run the {[Library: Import]} command in your SilverBullet space and enter: + + !silverbullet.md/Library/Journal/ + +# Included templates +```query +page +where name =~ /^{{escapeRegexp @page.name}}\// +render [[Library/Core/Query/Page]] +``` + +# Tips +Do you want your space’s start page to always be either your daily or weekly note? You can! + +To set the _daily note_ as the default page, set the following in [[SETTINGS]]: + +```yaml +indexPage: "Journal/Day/{{today}}" +``` + +And for the _weekly note_: + +```yaml +indexPage: "Journal/Week/{{weekStart}}" +``` diff --git a/website/Library/Journal/New Page/Daily Note.md b/website/Library/Journal/New Page/Daily Note.md new file mode 100644 index 0000000..cacff82 --- /dev/null +++ b/website/Library/Journal/New Page/Daily Note.md @@ -0,0 +1,12 @@ +--- +description: "Your daily note template" +tags: template +hooks.newPage: + suggestedName: "Journal/Day/{{today}}" + confirmName: false + openIfExists: true + forPrefix: "Journal/Day/" + command: "Open Daily Note" + key: "Alt-Shift-d" +--- +* |^| \ No newline at end of file diff --git a/website/Library/Journal/New Page/Weekly Note.md b/website/Library/Journal/New Page/Weekly Note.md new file mode 100644 index 0000000..99904ca --- /dev/null +++ b/website/Library/Journal/New Page/Weekly Note.md @@ -0,0 +1,13 @@ +--- +description: "Open your weekly note page" +tags: template +hooks.newPage: + suggestedName: "Journal/Week/{{weekStart}}" + forPrefix: "Journal/Week/" + confirmName: false + openIfExists: true + command: "Open Weekly Note" + key: "Alt-Shift-w" +--- +# Goals +* |^| \ No newline at end of file diff --git a/website/Linked Mentions.md b/website/Linked Mentions.md index 6d974f7..3f53cc7 100644 --- a/website/Linked Mentions.md +++ b/website/Linked Mentions.md @@ -1,3 +1,3 @@ -Linked Mentions are references from other pages to the current page. Technically, they’re not a built-in feature, but you can easily implement them using [[Live Template Widgets]]. +On this site you see a section “Linked Mentions” appear along the bottom of every page that has incoming links. 👇🏻 This section lists all pages that contain a _link_ to the current page. This allows for easy navigation, and may help you discover linked concepts through these links. -To enable linked mentions being added to your pages, include the [[template/widget/linked-mentions]] template in your space, either through copy and pasting or through [[Federation]]. +Linked Mentions are not a feature built into SilverBullet directly, but can easily be added by importing the [[Library/Core|core library]]. \ No newline at end of file diff --git a/website/Live Embeds.md b/website/Live Embeds.md new file mode 100644 index 0000000..018cb27 --- /dev/null +++ b/website/Live Embeds.md @@ -0,0 +1,13 @@ +Live Embeds allow you to embed internet content into your page inside of an iframe. This is useful to embed youtube videos or other websites. + +```embed +url: https://youtu.be/BbNbZgOwB-Y +``` + +Note, there is specific support for YouTube videos — it automatically sets the width and height, and replaces the URL with an embed URL. + +The body of an `embed` block is written in [[YAML]] and supports the following attributes: + +* `url` (mandatory): the URL of the content to embed +* `height` (optional): the height of the embedded page in pixels +* `width` (optional): the width of the embedded page in pixels diff --git a/website/Live Queries.md b/website/Live Queries.md index 039dc08..6111212 100644 --- a/website/Live Queries.md +++ b/website/Live Queries.md @@ -8,16 +8,16 @@ order by lastModified desc where size > 100 select name limit 10 -render [[template/page]] +render [[Library/Core/Query/Page]] ``` -It’s most convenient to use `/query` [[Slash Commands]] to insert a query in a page. +It’s most convenient to use the `/query` [[Snippets|snippet]] to insert a query in a page. For those comfortable reading such things, [here you can find the full query grammar](https://github.com/silverbulletmd/silverbullet/blob/main/common/markdown_parser/query.grammar). The general syntax is to specify a `querySource` followed by a number of clauses that modify or restrict. If you haven’t already, check out how [[Objects]] work in SilverBullet. # Clauses -## `where` [[@expression]] +## where [[@expression]] A `where` clause filters out all objects that do not match a certain condition. You can have multiple `where` clauses if you like, which will have the same effect as combining them with the `and` keyword. Here is a simple example based on a custom tag `#person` (see [[Objects]] on how this works): @@ -35,19 +35,19 @@ To query all `person`s that are above age 21, we can use the following `where` c ```query person where page = "{{@page.name}}" and age > 21 ``` -## `order by` [[@expression]] +## order by [[@expression]] To sort results, an `order by` clause can be used, optionally with `desc` to order in descending order (ascending is the default): ```query person where page = "{{@page.name}}" order by age desc ``` -## `limit` [[@expression]] +## limit [[@expression]] To limit the number of results, you can use a `limit` clause: ```query person where page = "{{@page.name}}" limit 1 ``` -## `select` +## select You can use the `select` clause to select only specific attributes from the result set. You can use it either simply as `select attribute1, attribute2` but also select the value of certain expressions and give them a name via the `select age + 1 as nextYear` syntax: ```query @@ -56,22 +56,22 @@ where page = "{{@page.name}}" select name, age, age + 1 as nextYear ``` -## `render each [[template]]` and `render all [[template]]` +## render each `[[template]]` and render all `[[template]]` $render By default, results are rendered as a table. To instead render results using [[Templates|a template]], use the `render` clause, which comes in two shapes `render each` where the template is instantiated for _each_ result (the `each` keyword is optional): ```query person where page = "{{@page.name}}" -render each [[template/person]] +render each [[internal-template/person]] ``` -And `render all` where the entire result set is passed to the template as a list so the template can do its own iteration using `#each`, which you could then use to e.g. build a table (using this [[template/people]] template, for instance): +And `render all` where the entire result set is passed to the template as a list so the template can do its own iteration using `#each`, which you could then use to e.g. build a table (using this [[internal-template/people]] template, for instance): ```query person where page = "{{@page.name}}" -render all [[template/people]] +render all [[internal-template/people]] ``` # Expressions $expression diff --git a/website/Live Template Widgets.md b/website/Live Template Widgets.md index 5306645..aed51d7 100644 --- a/website/Live Template Widgets.md +++ b/website/Live Template Widgets.md @@ -1,41 +1,24 @@ -Live Template Widgets allow you to automatically render templated markdown widgets to the top or bottom of pages matching specific criteria. +Live Template Widgets allow you to automatically render templated markdown widgets at the top or bottom of pages matching specific criteria. > **warning** Warning > This feature is still _experimental_, aspects of it may change, or it could be removed altogether. -If you have no idea what that means or what you would use this for; you probably don’t need this feature. Don’t worry about it. - # Defining -Live Template Widgets follow the same pattern as other [[Templates]] with a few additional attributes: +Live Template Widgets are regular [[Templates]] that set a `hooks.top` or `hooks.bottom` attribute (depending on where you would like them to appear), specifying: -* `tags`: should be set to `template` as for any other template -* `type`: should be set to `widget:top` or `widget:bottom` depending on where you would like it to appear * `where`: should contain an [[Live Queries$expression]] that evaluates to true for the _pages_ you would like to apply this template to, usually this checks for a specific tag, but it can be any expression. Think of this as a `where` clause that should match for the pages this template is applied to. -* `priority` (optional): in case you have multiple templates that have matching `where` expression, the one with the priority set to the lowest number wins. -* `enabled` (defaults to `true`): in case you want to (temporarily) disable this template, set this to `false`. +* `order` (optional): in case you have multiple templates that have matching `where` expression, the one with the priority set to the lowest number wins. # Example -The following widget template applies to all pages tagged with `person` (see the `where`). It uses the [[!silverbullet.md/template/live/incoming]] template, to show all incomplete tasks that reference this particular page. - -Indeed, you can use [[Live Queries]] and [[Live Templates]] here as well. +The following widget template applies to all pages tagged with `person` (see the `where`). It uses the [[Library/Core/Widget/Linked Tasks]] template to show all incomplete tasks that contain a link to the current page. --- tags: template - type: widget:top - where: 'tags = "person"' + hooks.top.where: 'tags = "person"' --- ## Incoming tasks ```template - page: "[[!silverbullet.md/template/live/incoming]]" + page: "[[!silverbullet.md/Library/Core/Widget/Linked Tasks]]" ``` -## Table of contents -The [[Table of Contents]] as it appears on this site is implemented using a template: [[template/widget/toc]]. - -## Linked Mentions -The [[Linked Mentions]] as they appear on this site are also implemented using a template: [[template/widget/linked-mentions]]. - -## Plug widget template -This site uses the [[internal-template/plug-widget]] template for pages tagged with `plug`, such as [[Plugs/Editor]], [[Plugs/Github]] and [[Plugs/Mermaid]]. - - +More examples can be found [[Library/Core/Page/Template Index$widgets|here]]. diff --git a/website/Live Templates.md b/website/Live Templates.md index bfb3ab6..459358d 100644 --- a/website/Live Templates.md +++ b/website/Live Templates.md @@ -1,13 +1,12 @@ -Live templates render [[Templates]] inline in a page. They’re called “Live” because their content updates dynamically. +Live Templates are a type of [[Blocks|block]] that render [[Templates]] inline in a page. -## Syntax -Live Templates are specified using [[Markdown]]‘s fenced code block notation using `template` as a language. The body of the code block specifies the template to use, as well as any arguments to pass to it. +Template blocks are specified using [[Markdown]]‘s fenced code block notation using `template` as a language. The body of the block specifies the template to use, as well as any arguments to pass to it. Generally you’d use it in one of two ways, either using a `page` [[Templates|template]] reference, or an inline `template`: Here’s an example using `page`: ```template -page: "[[template/today]]" +page: "[[internal-template/today]]" ``` And here’s an example using `template`: @@ -27,12 +26,12 @@ value: You can also pass in the result of a [[Live Queries|query]] as a value by setting the `query` attribute: ```template +query: | + tag where parent = "page" select name template: | {{#each .}} * #{{name}} {{/each}} -query: | - tag where parent = "page" select name ``` If you just want to render the raw markdown without handling it as a handlebars template, set `raw` to true: @@ -41,4 +40,3 @@ template: | This is not going to be {{processed}} by Handlebars raw: true ``` - diff --git a/website/Manual.md b/website/Manual.md index c5da8e1..ccbce30 100644 --- a/website/Manual.md +++ b/website/Manual.md @@ -11,7 +11,8 @@ The biggest hurdle to get over with SilverBullet is that you need to get this th * [[Install]]: Installation instructions for various setups * [[Deployments]]: various deployment options * [[Authelia]]: configuring SilverBullet with [Authelia](https://www.authelia.com/) authentication. -* [[Guide/Deployment/Cloudflare and Portainer]]: configuring SilverBullet with a Cloudflare tunnel, portainer and optional Cloudflare zero trust authentication. + +For more additional guides, check out [our community guides](https://community.silverbullet.md/c/guides/6). # User interface SilverBullet’s UI is minimalist by design. Let’s look at the few UI elements and how to use them. @@ -21,10 +22,9 @@ SilverBullet’s UI is minimalist by design. Let’s look at the few UI elements * [[Client Modes]] (the 🔄 button) * [[Index Page]] (the 🏠 button) * [[Page Picker]] + * [[Template Picker]] * [[Command Palette]] -* The main [[Editor]] component contains your page’s text, as well as potentially: - * [[Table of Contents]] - * [[Linked Mentions]] +* The main [[Editor]] component contains your page’s text # Core Concepts These are the core concepts used in SilverBullet: @@ -50,7 +50,7 @@ These are the core concepts used in SilverBullet: # Navigation The main ways to roam your space, beside following page links, are: * [[Page Picker]] -* [[Linked Mentions]] +* [[Template Picker]] * [[Full Text Search]] # Advanced topics @@ -58,8 +58,8 @@ The main ways to roam your space, beside following page links, are: * [[Frontmatter]] * [[Attributes]] * [[Live Queries]] -* [[Templates]] and [[Live Templates]] -* [[Federation]]: it possibly to “sync in” content from outside sources, such as [[Template Sets]] +* [[Templates]] +* [[Federation]]: it possibly to “sync in” content from outside sources, such as [[Libraries]] # Extending SilverBullet A lot of SilverBullet’s functionality is built as [[Plugs]] using the robust [[PlugOS]] extension mechanism. If you are adventurous you can try to build [[Plugs/Development|such plugs yourself]]. diff --git a/website/Markdown/Code Widgets.md b/website/Markdown/Code Widgets.md index d174ac8..ec1982b 100644 --- a/website/Markdown/Code Widgets.md +++ b/website/Markdown/Code Widgets.md @@ -8,20 +8,4 @@ Currently, SilverBullet provides a few code widgets out of the box: * `embed` * `markdown` -In addition, plugs like [[Plugs/KaTeX]] and [[Plugs/Mermaid]] add additional ones. - -## `embed` -This allows you to embed internet content into your page inside of an iframe. This is useful to embed youtube videos or other websites. -and a YouTube video: - -```embed -url: https://youtu.be/BbNbZgOwB-Y -``` - -Note, there is specific support for YouTube videos — it automatically sets the width and height, and replaces the URL with an embed URL. - -The body of an `embed` block is written in [[YAML]] and supports the following attributes: - -* `url` (mandatory): the URL of the content to embed -* `height` (optional): the height of the embedded page in pixels -* `width` (optional): the width of the embedded page in pixels +In addition, plugs like [[Plugs/KaTeX]] and [[Plugs/Mermaid]] add additional ones. \ No newline at end of file diff --git a/website/Objects.md b/website/Objects.md index 8e023e2..aaf7e7d 100644 --- a/website/Objects.md +++ b/website/Objects.md @@ -53,10 +53,10 @@ The following query shows all attributes available for tasks: upnext ``` -Although you may want to render it using a template such as [[template/task]] instead: +Although you may want to render it using a template such as [[Library/Core/Query/Task]] instead: ```query -upnext render [[template/task]] +upnext render [[Library/Core/Query/Task]] ``` ## taskstate diff --git a/website/Page Picker.md b/website/Page Picker.md index 7dca6d4..e1f9133 100644 --- a/website/Page Picker.md +++ b/website/Page Picker.md @@ -4,7 +4,10 @@ The page picker can be invoked by clicking the 📔 icon in the top bar, or by p The main input is the **filter phrase** and can be used to narrow down the list of page results. -If the filter phrase contains `#tags` the results will be filtered based on matching those tags. That means to quickly see a list of all `#template`s you can use `#template` as a filter phrase. +If the filter phrase contains `#tags` the results will be filtered based on matching those tags. + +> **note** Note +> [[Templates]], even though technically regular pages, do not appear in the page picker. To navigate to them, use the [[Template Picker]] instead. Pressing the `Enter` key will open/create the selected page. Pressing `Shift-Enter` will always open or create (if it doesn't already exist) the page _exactly matching_ the filter phrase. Therefore, if you intend to create a new page, simply type the name of the new page and hit `Shift-Enter`. diff --git a/website/Page Templates.md b/website/Page Templates.md index eaf2457..76e9f31 100644 --- a/website/Page Templates.md +++ b/website/Page Templates.md @@ -1,11 +1,28 @@ -The {[Page: From Template]} command enables you to create a new page based on a page template. A page template is a [[Templates|template]] with the `type` attribute (in [[Frontmatter]]) set to `page`. +Page templates enable you to define templates for creating new pages. They can be invoked in a few ways: + +* Explicitly using the {[Page: From Template]} command +* Explicitly using a custom command configured in the template +* Implicitly when configured to be used automatically for a specific _page prefix_. + +A page template is a [[Templates|template]] using the `hooks.newPage` attribute (in [[Frontmatter]]). + +The following configuration options are supported: + +* `suggestedName`: the proposed name for the new page, can use template placeholders such as `{{today}}`. +* `confirmName`: Confirm the suggested page name before creating it (defaults to `true`). +* `openIfExists`: If a page with the `suggestedName` already exists, open it instead of overwriting it. This is useful to implement page templates like [[Library/Journal/New Page/Daily Note]]. +* `forPrefix`: automatically apply (or offer, when multiple page templates match) this page template for page names that start with this prefix. +* `command`: expose the snippet as a [[Commands|command]]. +* `key`: Bind the snippet to a keyboard shortcut (note: this requires to _also_ specify the `command` configuration). +* `mac`: Bind the snippet to a Mac-specific keyboard shortcut. An example: --- tags: template - type: page - pageName: "📕 " + hooks.newPage: + suggestedName: "📕 " + forPrefix: "📕 " --- # {{@page.name}} As recorded on {{today}}. @@ -14,7 +31,7 @@ An example: ## Notes ## Conclusions -Will prompt you to pick a page name (defaulting to “📕 “), and then create the following page (on 2023-08-08) when you pick “📕 Harry Potter” as a page name: +When using the {[Page: From Template]} command and selecting this template, this will prompt you to pick a page name (defaulting to “📕 “), and then create the following page (on 2023-08-08) when you pick “📕 Harry Potter” as a page name: # 📕 Harry Potter As recorded on 2022-08-08. @@ -23,4 +40,6 @@ Will prompt you to pick a page name (defaulting to “📕 “), and then create ## Notes ## Conclusions +In addition, this page template will be used automatically when you create _any_ new page starting with “📕 “ by navigating to a new page matching this prefix, such as “📕 Foundation”. + As with any [[Templates|template]], the `frontmatter` can be used to define [[Frontmatter]] for the new page. \ No newline at end of file diff --git a/website/Plugs.md b/website/Plugs.md index d0d897a..948ea60 100644 --- a/website/Plugs.md +++ b/website/Plugs.md @@ -16,13 +16,13 @@ Plugs are distributed as self-contained JavaScript bundles (ending with `.plug.j # Core plugs These plugs are distributed with SilverBullet and are automatically enabled: ```query -plug where uri = null order by name render [[template/plug]] +plug where uri = null order by name render [[internal-template/plug]] ``` # Third-party plugs These plugs are written either by third parties or distributed separately from the main SB distribution. ```query -plug where uri != null order by name render [[template/plug]] +plug where uri != null order by name render [[internal-template/plug]] ``` Want to develop your own plugs? Have a look at [[Plugs/Development]]. \ No newline at end of file diff --git a/website/Plugs/Share.md b/website/Plugs/Share.md index b7e1f01..30b5f27 100644 --- a/website/Plugs/Share.md +++ b/website/Plugs/Share.md @@ -8,5 +8,5 @@ See the [original RFC](https://github.com/silverbulletmd/silverbullet/discussion Specific implementations for sharing are implemented in other plugs, specifically: ```query -plug where shareSupport = true render [[template/page]] +plug where shareSupport = true render [[Library/Core/Query/Page]] ``` diff --git a/website/Plugs/Tasks.md b/website/Plugs/Tasks.md index ef52438..574823c 100644 --- a/website/Plugs/Tasks.md +++ b/website/Plugs/Tasks.md @@ -48,7 +48,7 @@ There is a [[!silverbullet.md/template/tasks/task]] template you can use to rend Try it (by clicking on the checkbox inside of the query): ```query -task where page = "{{@page.name}}" and name = "Remote toggle me" render [[template/task]] +task where page = "{{@page.name}}" and name = "Remote toggle me" render [[Library/Core/Query/Task]] ``` * [ ] Remote toggle me diff --git a/website/SETTINGS.md b/website/SETTINGS.md index e185a94..51ec7d8 100644 --- a/website/SETTINGS.md +++ b/website/SETTINGS.md @@ -1,20 +1,12 @@ This page contains settings for configuring SilverBullet and its Plugs. Changing any of these will go into effect immediately in most cases except `indexPage` and `customStyles`, which require a reload. ```yaml -# Initial page to load when launching SB +# Initial page to load when launching SB, can contain template variables indexPage: "[[SilverBullet]]" # Load custom CSS styles from the following page, can also be an array customStyles: "[[STYLES]]" -# Template settings -quickNotePrefix: "📥 " -dailyNotePrefix: "📅 " -dailyNoteTemplate: "[[template/page/Daily Note]]" -weeklyNotePrefix: "🗓️ " -weeklyNoteTemplate: "[[template/page/Weekly Note]]" -weeklyNoteMonday: false - # It is possible to override keyboard shortcuts and command priority shortcuts: - command: "{[Stats: Show]}" # Using the command link syntax here diff --git a/website/SilverBullet.md b/website/SilverBullet.md index 66e8333..3431d90 100644 --- a/website/SilverBullet.md +++ b/website/SilverBullet.md @@ -15,13 +15,11 @@ You were told wrong. * [[Install]]: how to install and deploy SilverBullet. * [[Manual]]: how to use this thing. * [[CHANGELOG]]: we’re in active development, so things change rapidly, watch this page to keep up. -* Development: - * [Roadmap](https://github.com/orgs/silverbulletmd/projects/2/views/1): currently planned features and priorities. - * [Issues](https://github.com/silverbulletmd/silverbullet/issues): if you have ideas, or find bugs, please report them. - * [Discussions](https://github.com/silverbulletmd/silverbullet/discussions): got ideas for features or questions? Ask them here. -* Socials: - * [Mastodon](https://fosstodon.org/@silverbulletmd): follow SilverBullet development on [Mastodon](https://joinmastodon.org/) - * [Discord](https://discord.gg/EvXbFucTxn): for more real-time support and discussion, join our Discord! +* [Roadmap](https://github.com/orgs/silverbulletmd/projects/2/views/1): currently planned features and priorities. +* [Issues](https://github.com/silverbulletmd/silverbullet/issues): if you have ideas, or find bugs, please report them. +* [Community](https://community.silverbullet.md): join our discourse community! +* [Discord](https://discord.gg/EvXbFucTxn): for more real-time support and discussion! +* [Mastodon](https://fosstodon.org/@silverbulletmd): follow SilverBullet development on [Mastodon](https://joinmastodon.org/) # Features Some highlights: @@ -29,13 +27,13 @@ Some highlights: * SilverBullet runs in any modern browser (including mobile ones) as a [[PWA]] in two [[Client Modes]] ([[Client Modes$online|online]] and [[Client Modes$sync|synced]] mode), where the _synced mode_ enables **100% offline operation**, keeping a copy of content in the browser’s local ([IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)) database, syncing back to the server when a network connection is available. * SilverBullet provides an enjoyable [[Markdown]] writing experience with a clean UI, rendering text using [[Live Preview|live preview]], further **reducing visual noise** while still providing direct access to the underlying markdown syntax. * SilverBullet supports wiki-style **page linking** using the `[[page link]]` syntax. Incoming links are indexed and appear as [[Linked Mentions]] at the bottom of the pages linked to thereby providing _bi-directional linking_. -* SilverBullet allows you to boost your writing/knowledge collection productivity using its various [[Templates]] mechanism. * SilverBullet is optimized for **keyboard-based operation**: * Quickly navigate between pages using the **page switcher** (triggered with `Cmd-k` on Mac or `Ctrl-k` on Linux and Windows). * Run commands via their keyboard shortcuts or the **command palette** (triggered with `Cmd-/` or `Ctrl-/` on Linux and Windows). * Use [[Slash Commands]] to perform common text editing operations. -* SilverBullet is a platform for [[End-User Programming]] through its support for [[Objects]], [[Live Queries]] and [[Live Templates]], allowing to make parts of your pages _dynamic_. -* SilverBullet can be extended using [[Plugs]]. +* SilverBullet is a platform for [[End-User Programming]] through its support for [[Objects]], [[Live Queries]], [[Live Templates]] and [[Live Template Widgets]], allowing to make parts of your pages and UI dynamic. +* SilverBullet allows you to boost your writing/knowledge collection productivity using its various [[Templates]] mechanism. +* SilverBullet can be extended using [[Libraries]] and [[Plugs]]. * **Self-hosted**: you own your data. All content is stored as plain files in a folder on disk (if you so choose). Back up, sync, edit, publish, script with any additional tools you like. * SilverBullet is free, [**open source**, MIT licensed](https://github.com/silverbulletmd/silverbullet) software. @@ -87,10 +85,10 @@ function helloWorld() { # Beyond Markdown Where things get slightly more interesting is when using features like [[Live Queries]] that allow you to query all types of [[Objects]] indexed based on the pages in your [[Spaces]]. -Let’s explore this with a meta example of using this functionality for this very website. All pages in this space that represent a [[Plugs|plug]] are tagged with the `#plug` [[Tags|tag]]. Now, if we want to render a list of all plugs in one place using the [[template/plug]] template, we can simply do this as follows: +Let’s explore this with a meta example of using this functionality for this very website. All pages in this space that represent a [[Plugs|plug]] are tagged with the `#plug` [[Tags|tag]]. Now, if we want to render a list of all plugs in one place using the [[internal-template/plug]] template, we can simply do this as follows: ```query -plug render [[template/plug]] +plug render [[internal-template/plug]] ``` Hover over that list and click the edit button to see the query that generates this view. diff --git a/website/Slash Commands.md b/website/Slash Commands.md index 83f3392..ed7a8f8 100644 --- a/website/Slash Commands.md +++ b/website/Slash Commands.md @@ -1,11 +1,5 @@ -Slash commands are built-in to SilverBullet. You can trigger them by typing a `/` in your text (after whitespace). +Slash commands are quick ways to perform repetitive tasks, 99% of the time this will mean inserting a [[Snippets|snippet]]. -The [[Plugs/Editor]] plug provides a few helpful ones: +You can trigger a slash command by typing a `/` in your text (after some white space). -* `/h1` through `/h4` to turn the current line into a header -* `/hr` to insert a horizontal rule (`---`) -* `/table` to insert a markdown table (whoever can remember this syntax without it) -* `/today` to insert today’s date -* `/tomorrow` to insert tomorrow’s date - -You can create custom slash commands using [[Slash Templates]]. +Technically, no slash commands ship with SilverBullet directly. However, you can easily add useful ones via [[Libraries]]. diff --git a/website/Slash Templates.md b/website/Slash Templates.md deleted file mode 100644 index d1edc1d..0000000 --- a/website/Slash Templates.md +++ /dev/null @@ -1,43 +0,0 @@ -Slash templates allow you to define custom [[Slash Commands]] that expand “snippet style” templates inline. - -# Definition -You can define a slash template by creating a [[Templates|template page]] with a template tag and `trigger` attribute. If you’d like to (temporarily) disable the template, you can set the `enabled` attribute to `false`. - -Example: - - --- - tags: template - trigger: meeting-notes - --- - ## Meeting notes for {{today}}! - - |^| - -## Frontmatter -A template’s [[Frontmatter]] is interpreted by SilverBullet’s [[Templates|template]] engine and removed when instantiated. However, to still include frontmatter after instantiation, you can use the `frontmatter` attribute. - -Example: - - --- - tags: template - trigger: meeting-notes - frontmatter: | - date: {{today}} - --- - ## Meeting notes for {{today}}! - - |^| - -Which will expand into e.g. - - --- - date: 2023-11-11 - --- - ## Meeting notes for 2023-11-11 - - . - -When the page already contains frontmatter before invoking the slash command, it will be augmented with the additional frontmatter specified by the template. - -# Use -You can _trigger_ the slash template by typing `/` (e.g. `/meeting-notes`) in any page. diff --git a/website/Snippets.md b/website/Snippets.md new file mode 100644 index 0000000..05974ae --- /dev/null +++ b/website/Snippets.md @@ -0,0 +1,51 @@ +Snippets allow you to define custom [[Slash Commands]] that expand snippet style templates inline. + +# Definition +You define a snippet by creating a [[Templates|template]] with a `hooks.snippet` attribute that configures the snippet. The following configuration options are supported: + +* `slashCommand`: expose the snippet via the `/my-snippet` notation. +* `command`: expose the snippet as a [[Commands|command]]. +* `key`: Bind the snippet to a keyboard shortcut (note: this requires to _also_ specify the `command` configuration). +* `mac`: Bind the snippet to a Mac-specific keyboard shortcut. +* `matchRegex` (advanced use only): match the current line against a regular expression, and replace the match with the template’s body. +* `insertAt`: by default a snippet is inserted at the cursor position, but alternatively it can be inserted at: `line-start`, `line-end`, `page-start` or `page-end`. + +Minimal example: + + --- + tags: template + hooks.snippet: + slashCommand: meeting-notes + --- + ## Meeting notes for {{today}}! + + |^| + +## Frontmatter +A template’s [[Frontmatter]] is interpreted by SilverBullet’s [[Templates|template]] engine and removed when instantiated. However, to inject frontmatter after instantiation, you can use the `frontmatter` attribute. + +Example: + + --- + tags: template + hooks.snippet.slashCommand: meeting-notes + frontmatter: | + date: {{today}} + --- + ## Meeting notes for {{today}}! + + |^| + +Which will expand into e.g. + + --- + date: 2023-11-11 + --- + ## Meeting notes for 2023-11-11 + + . + +When the page already contained frontmatter before inserting the snippet, it will be augmented with the additional frontmatter specified by the template. + +# Use +A snippet can be _triggered_ via the specified `slashCommand` via `/slashCommand` or via {[Open Command Palette]} and/or its associate key bindings when `command`, `key`/`mac` are specified. diff --git a/website/Table of Contents.md b/website/Table of Contents.md index 3a4c039..cd7a767 100644 --- a/website/Table of Contents.md +++ b/website/Table of Contents.md @@ -1,4 +1,4 @@ -You can add a table of contents to a page using the `toc` [[Markdown/Code Widgets|Code Widget]]. +You can add a table of contents to a page using `toc` [[Blocks]]. In its most basic form it looks like this (click the edit button to see the code): @@ -10,12 +10,7 @@ You can use it in two ways: 1. _Manually_, by adding a `toc` widget to the pages where you’d like to render a ToC 2. _Automatically_, using a [[Live Template Widgets|Live Template Widget]] -To have a ToC added to all pages with a larger (e.g. 3) number of headings, it is recommended to use [[template/widget/toc|this template widget]]. You can do this by either copy and pasting it into your own space, or by using [[Federation]] and have it included in your space that way: - -```yaml -federation: -- uri: silverbullet.md/template/widget/toc -``` +To have a ToC added to all pages with a larger (e.g. 3) number of headings, it is recommended to use [[Library/Core/Widget/Table of Contents|this template widget]]. ## Configuration In the body of the `toc` code widget you can configure a few options: @@ -29,3 +24,5 @@ Example: header: false minHeaders: 1 ``` + +Want to add a table of contents to all your pages automatically? You can — that’s functionality available via the [[Library/Core]] library. \ No newline at end of file diff --git a/website/Template Picker.md b/website/Template Picker.md new file mode 100644 index 0000000..9dadecb --- /dev/null +++ b/website/Template Picker.md @@ -0,0 +1 @@ +The template picker is used to navigate between [[Templates]]. Functionally it’s equivalent to the [[Page Picker]] except that it only shows pages tagged with `template`. diff --git a/website/Template Sets.md b/website/Template Sets.md deleted file mode 100644 index fc6bf7c..0000000 --- a/website/Template Sets.md +++ /dev/null @@ -1,66 +0,0 @@ -> **warning** Experimental -> This is still an experimental idea. These templates may change, be renamed etc. prepare yourself for breakage. - -This is an attempt at collecting useful, reusable templates so you don’t have to reinvent the wheel. - -While you may just copy & paste these templates to your own space, the most convenient ways to use them is using [[Federation]]. This will synchronize these templates into your space and make them available for use instantly. - -To set this up, add the following to your [[SETTINGS]]: - -```yaml -federate: -- uri: silverbullet.md/template -``` - -If you don’t want to sync _all_ these templates, you can use more specific URIs, e.g. -```yaml -federate: -- uri: silverbullet.md/template/page/ -``` -to just get the page templates, for instance. - -To reference a template, use the [[Federation]] syntax, e.g. `[[!silverbullet.md/template/task]]`. - -# Page Templates -Use these [[Page Templates]] with the {[Page: From Template]} command. - -```query -template where type = "page" render [[template/documented-template]] -``` - -# Slash Templates -These can be used as [[Slash Templates]]: - -```query -template where type = "slash" render [[template/documented-template]] -``` - -# Live Templates -Use these as `page` in [[Live Templates]]. - -```query -template -where type = "live" -order by order -render [[template/documented-template]] -``` - -# Live Query -Use these in your `render` clauses in [[Live Queries]]. - -```query -template -where type = "query" -order by order -render [[template/documented-template]] -``` - -# Live Widget Templates -Use these to add various useful [[Live Template Widgets]] to your pages. - -```query -template -where type =~ /^widget:/ and name =~ /^template\// -order by order -render [[template/documented-template]] -``` diff --git a/website/Templates.md b/website/Templates.md index 567b12c..8e0d16d 100644 --- a/website/Templates.md +++ b/website/Templates.md @@ -1,54 +1,52 @@ -Templates are reusable pieces of markdown content, usually with placeholders that are replaced once instantiated. +Templates are reusable pieces of (markdown) content, often containing placeholders that are replaced once instantiated. -There are two general uses for templates: +Templates are kept in your space. They’re effectively regular [[Pages]], and are [[Tags|tagged]] with the `template` tag. + +Templates do not appear in the [[Page Picker]], but instead appear in the [[Template Picker]]. They are not offered as auto complete suggestions when creating regular page links, only when doing so in the context of e.g. a [[Live Queries]] and [[Live Templates]]. + +In SilverBullet templates serve a few purposes: 1. _Live_ uses, where page content is dynamically updated based on templates: * [[Live Queries]] * [[Live Templates]] * [[Live Template Widgets]] 2. _One-off_ uses, where a template is instantiated once and inserted into an existing or new page: - * [[Slash Templates]] + * [[Snippets]] * [[Page Templates]] -# Creating templates +# Definition Templates are regular pages [[Tags|tagged]] with the `#template` tag. Note that, when tagged inline (by putting `#template` at the beginning of the page), the tag will be removed when the template is instantiated. -**Naming**: it’s common, although not required, to use a `template/` prefix when naming templates. - Tagging a page with a `#template` tag (either in the [[Frontmatter]] or using a [[Tags]] at the very beginning of the page content) does a few things: -1. It will make the page appear when completing template names, e.g. in `render` clauses in [[Live Queries]], or after the `page` key in [[Live Templates]]. -2. It excludes the page from being indexed for [[Objects]], that is: any tasks, items, paragraphs etc. will not appear in your space’s object database. Which is usually what you want. -3. It registers your templates to be used as [[Slash Templates]] as well as [[Page Templates]]. +1. It will make the page appear when completing template names, e.g. in `render` clauses in [[Live Queries]], or after the `page` key in [[Live Templates]]. +2. The template page no longer appear in the [[Page Picker]], instead you now navigate to it using the [[Template Picker]]. +4. It can register your templates to be used as [[Snippets]], [[Page Templates]] or [[Live Template Widgets]]. ## Frontmatter [[Frontmatter]] has special meaning in templates. The following attributes are used: * `tags`: should always be set to `template` -* `type` (optional): should be set to `page` for [[Page Templates]] and to `frontmatter` for [[Live Template Widgets]] -* `trigger` (optional): defines the slash command name for [[Slash Templates]] * `displayName` (optional): defines an alternative name to use when e.g. showing the template picker for [[Page Templates]], or when template completing a `render` clause in a [[Live Templates]]. -* `pageName` (optional, [[Page Templates]] only): specify a (template for a) page name. -* `frontmatter` (optional): defines [[Frontmatter]] to be added/used in the rendered template. This can either be specified as a string or as an object. +* `description` (optional): may appear in various UIs to give more information about the template. +* `frontmatter` (optional): defines [[Frontmatter]] to be added/used in the _rendered_ template. This can either be specified as a string or as an object. +* `hooks` (optional): hook the template into various parts of the system, look at [[Page Templates]], [[Snippets]] and [[Live Template Widgets]] for details. An example: --- tags: template - type: page - trigger: one-on-one - displayName: "1:1 template" - pageName: "1-1s/" + hooks.newPage.suggestedName: "Meetings/{{today}}" frontmatter: dateCreated: "{{today}}" --- # {{today}} * |^| -# Template content -Templates consist of markdown, but can also include [Handlebars syntax](https://handlebarsjs.com/), such as `{{today}}`, and `{{#each .}}`. +# Content +Templates consist of plain markdown text, but can also include [Handlebars syntax](https://handlebarsjs.com/), such as `{{today}}`, and `{{#each .}}`. -The special `|^|` marker can be used to specify the desired cursor position after the template is included. +A special `|^|` marker can be used to specify the desired cursor position after the template is included. ## Handlebar helpers There are a number of built-in handlebars helpers you can use: diff --git a/website/template/people.md b/website/internal-template/people.md similarity index 100% rename from website/template/people.md rename to website/internal-template/people.md diff --git a/website/template/person.md b/website/internal-template/person.md similarity index 76% rename from website/template/person.md rename to website/internal-template/person.md index b182b7a..1fe1e66 100644 --- a/website/template/person.md +++ b/website/internal-template/person.md @@ -1 +1,2 @@ +#template * Person **{{name}}** has {{age}} \ No newline at end of file diff --git a/website/template/plug.md b/website/internal-template/plug.md similarity index 100% rename from website/template/plug.md rename to website/internal-template/plug.md diff --git a/website/internal-template/today.md b/website/internal-template/today.md new file mode 100644 index 0000000..8c65f58 --- /dev/null +++ b/website/internal-template/today.md @@ -0,0 +1,2 @@ +#template +Today is {{today}}! diff --git a/website/template/debug.md b/website/template/debug.md deleted file mode 100644 index 87a91d1..0000000 --- a/website/template/debug.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -tags: template -type: query -order: 100 -description: | - Renders its object value in a `key: value` format -usage: | - Can be used by passing in a YAML object in a template via `value` or in a `render` clause of a query ---- -{{#each .}} -{{@key}}: {{.}} -{{/each}} - ---- \ No newline at end of file diff --git a/website/template/documented-template.md b/website/template/documented-template.md deleted file mode 100644 index 7e67f1f..0000000 --- a/website/template/documented-template.md +++ /dev/null @@ -1,5 +0,0 @@ -#template -* [[{{ref}}|{{#if displayName}}{{displayName}}{{else}}{{ref}}{{/if}}]] {{description}} -{{#if usage}} - * **Usage:** {{usage}} -{{/if}} \ No newline at end of file diff --git a/website/template/live/conflicts.md b/website/template/live/conflicts.md deleted file mode 100644 index 88432c6..0000000 --- a/website/template/live/conflicts.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -tags: template -type: live -description: Lists all pages with ".conflicted" in the name, created as a result of a synchronization conflict. ---- - -### Conflicting pages -```query -page where name =~ /\.conflicted/ render [[template/pages/page]] -``` diff --git a/website/template/live/incoming.md b/website/template/live/incoming.md deleted file mode 100644 index 0dae1b5..0000000 --- a/website/template/live/incoming.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -tags: template -type: live -description: | - Shows all tasks that reference (tag) the current page. For instance a task that references `[[John]]` in its name, would appear on the `John` page if it would use this [[sets/tasks/incoming]] template. -order: 2 ---- - -```query -task where name =~ /\[\[{{escapeRegexp @page.name}}\]\]/ where done = false render [[template/task]] -``` \ No newline at end of file diff --git a/website/template/page.md b/website/template/page.md deleted file mode 100644 index 6770f4f..0000000 --- a/website/template/page.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -tags: template -type: query -order: 1 -description: A page reference link as a list item ---- - -* [[{{name}}]] \ No newline at end of file diff --git a/website/template/page/Daily Note.md b/website/template/page/Daily Note.md deleted file mode 100644 index 1501f4b..0000000 --- a/website/template/page/Daily Note.md +++ /dev/null @@ -1 +0,0 @@ -* |^| \ No newline at end of file diff --git a/website/template/page/Weekly Note.md b/website/template/page/Weekly Note.md deleted file mode 100644 index 09a3452..0000000 --- a/website/template/page/Weekly Note.md +++ /dev/null @@ -1,3 +0,0 @@ -This is my weekly note template. - -|^| \ No newline at end of file diff --git a/website/template/page/slash-template.md b/website/template/page/slash-template.md deleted file mode 100644 index 4f2aee5..0000000 --- a/website/template/page/slash-template.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -tags: template -type: page -displayName: Slash Template -pageName: "template/slash/" -description: Define a new Slash Template -frontmatter: - tags: template - trigger: "|^|" ---- diff --git a/website/template/page/template.md b/website/template/page/template.md deleted file mode 100644 index c8019e0..0000000 --- a/website/template/page/template.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -tags: template -type: page -displayName: Page Template -pageName: "template/page/" -description: Define a new Page Template -frontmatter: - tags: template - displayName: "|^|" - type: page ---- diff --git a/website/template/slash/table.md b/website/template/slash/table.md deleted file mode 100644 index dfb6bda..0000000 --- a/website/template/slash/table.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -tags: template -trigger: "table" ---- -| Header A | Header B | -|----------|----------| -| |^| | Cell B | diff --git a/website/template/today.md b/website/template/today.md deleted file mode 100644 index 9ae80ac..0000000 --- a/website/template/today.md +++ /dev/null @@ -1 +0,0 @@ -#template Today is {{today}}! \ No newline at end of file diff --git a/website/template/widget/toc.md b/website/template/widget/toc.md deleted file mode 100644 index 47f3225..0000000 --- a/website/template/widget/toc.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -description: Adds a Table of Contents to all pages -tags: template -type: widget:top -where: 'true' ---- -```toc -minHeaders: 3 -```