diff --git a/common/languages.ts b/common/languages.ts index 0d45325f..566a05c2 100644 --- a/common/languages.ts +++ b/common/languages.ts @@ -36,6 +36,7 @@ export const builtinLanguages: Record = { "template": StreamLanguage.define(yamlLanguage), "embed": StreamLanguage.define(yamlLanguage), "data": StreamLanguage.define(yamlLanguage), + "toc": StreamLanguage.define(yamlLanguage), "javascript": javascriptLanguage, "js": javascriptLanguage, "typescript": typescriptLanguage, diff --git a/common/spaces/http_space_primitives.ts b/common/spaces/http_space_primitives.ts index eca8e5b3..8ca25749 100644 --- a/common/spaces/http_space_primitives.ts +++ b/common/spaces/http_space_primitives.ts @@ -66,9 +66,6 @@ export class HttpSpacePrimitives implements SpacePrimitives { async fetchFileList(): Promise { const resp = await this.authenticatedFetch(`${this.url}/index.json`, { method: "GET", - headers: { - Accept: "application/json", - }, }); if ( @@ -94,6 +91,10 @@ export class HttpSpacePrimitives implements SpacePrimitives { `${this.url}/${encodeURI(name)}`, { method: "GET", + headers: { + // This header won't trigger CORS preflight requests but can be interpreted on the server + Accept: "application/octet-stream", + }, }, ); if (res.status === 404) { diff --git a/plug-api/lib/cheap_yaml.test.ts b/plug-api/lib/cheap_yaml.test.ts index 492a7c22..799cfb31 100644 --- a/plug-api/lib/cheap_yaml.test.ts +++ b/plug-api/lib/cheap_yaml.test.ts @@ -1,5 +1,5 @@ import { assertEquals } from "../../test_deps.ts"; -import { determineTags } from "./cheap_yaml.ts"; +import { determineTags, isTemplate } from "./cheap_yaml.ts"; Deno.test("cheap yaml", () => { assertEquals([], determineTags("")); @@ -14,3 +14,77 @@ Deno.test("cheap yaml", () => { determineTags(`tags:\n- "#bla"\n- template`), ); }); + +Deno.test("Test template extraction", () => { + assertEquals( + isTemplate(`--- +name: bla +tags: template +--- + +Sup`), + true, + ); + + assertEquals( + isTemplate(`--- +tags: template, something else +--- +`), + true, + ); + + assertEquals( + isTemplate(`--- +tags: something else, template +--- +`), + true, + ); + + assertEquals( + isTemplate(`--- +tags: +- bla +- template +--- +`), + true, + ); + + assertEquals( + isTemplate(`#template`), + true, + ); + + assertEquals( + isTemplate(` #template This is a template`), + true, + ); + + assertEquals( + isTemplate(`--- +tags: +- bla +somethingElse: +- template +--- +`), + false, + ); + + assertEquals( + isTemplate(`--- +name: bla +tags: aefe +--- + +Sup`), + false, + ); + + assertEquals( + isTemplate(`Sup`), + false, + ); +}); diff --git a/plug-api/lib/cheap_yaml.ts b/plug-api/lib/cheap_yaml.ts index feae44f9..547ee3ea 100644 --- a/plug-api/lib/cheap_yaml.ts +++ b/plug-api/lib/cheap_yaml.ts @@ -34,3 +34,28 @@ export function determineTags(yamlText: string): string[] { } return tags; } + +const frontMatterRegex = /^---\n(([^\n]|\n)*?)---\n/; + +/** + * Quick and dirty way to check if a page is a template or not + * @param pageText + * @returns + */ +export function isTemplate(pageText: string): boolean { + const frontmatter = frontMatterRegex.exec(pageText); + // Poor man's YAML frontmatter parsing + if (frontmatter) { + pageText = pageText.slice(frontmatter[0].length); + const frontmatterText = frontmatter[1]; + const tags = determineTags(frontmatterText); + if (tags.includes("template")) { + return true; + } + } + // Or if the page text starts with a #template tag + if (/^\s*#template(\W|$)/.test(pageText)) { + return true; + } + return false; +} diff --git a/plug-api/silverbullet-syscall/code_widget.ts b/plug-api/silverbullet-syscall/code_widget.ts index 38f39866..22f672fd 100644 --- a/plug-api/silverbullet-syscall/code_widget.ts +++ b/plug-api/silverbullet-syscall/code_widget.ts @@ -5,7 +5,7 @@ export function render( lang: string, body: string, pageName: string, -): Promise { +): Promise { return syscall("codeWidget.render", lang, body, pageName); } diff --git a/plug-api/types.ts b/plug-api/types.ts index f43a12af..2c0b93d9 100644 --- a/plug-api/types.ts +++ b/plug-api/types.ts @@ -127,14 +127,13 @@ export type ObjectQuery = Omit; export type CodeWidgetCallback = ( bodyText: string, pageName: string, -) => Promise; +) => Promise; export type CodeWidgetContent = { html?: string; markdown?: string; script?: string; buttons?: CodeWidgetButton[]; - banner?: string; }; export type CodeWidgetButton = { diff --git a/plugs/federation/federation.ts b/plugs/federation/federation.ts index 18281021..3aeb7660 100644 --- a/plugs/federation/federation.ts +++ b/plugs/federation/federation.ts @@ -120,7 +120,12 @@ export async function readFile( ): Promise<{ data: Uint8Array; meta: FileMeta } | undefined> { const url = federatedPathToUrl(name); console.log("Fetching federated file", url); - const r = await nativeFetch(url); + const r = await nativeFetch(url, { + method: "GET", + headers: { + Accept: "application/octet-stream", + }, + }); if (r.status === 503) { throw new Error("Offline"); } diff --git a/plugs/index/builtins.ts b/plugs/index/builtins.ts index 39067724..d6ee569f 100644 --- a/plugs/index/builtins.ts +++ b/plugs/index/builtins.ts @@ -76,7 +76,8 @@ export const builtins: Record> = { pos: "!number", type: "string", trigger: "string", - forTags: "string[]", + where: "string", + priority: "number", }, }; diff --git a/plugs/index/command.ts b/plugs/index/command.ts index 3ece21ba..4fd8d984 100644 --- a/plugs/index/command.ts +++ b/plugs/index/command.ts @@ -2,7 +2,7 @@ import { editor, events, markdown, mq, space, system } from "$sb/syscalls.ts"; import { sleep } from "$sb/lib/async.ts"; import { IndexEvent } from "$sb/app_event.ts"; import { MQMessage } from "$sb/types.ts"; -import { isTemplate } from "../template/util.ts"; +import { isTemplate } from "$sb/lib/cheap_yaml.ts"; export async function reindexCommand() { await editor.flashNotification("Performing full page reindex..."); @@ -64,6 +64,7 @@ export async function parseIndexTextRepublish({ name, text }: IndexEvent) { tree: parsed, }); } else { + console.log("Indexing", name, "as page"); await events.dispatchEvent("page:index", { name, tree: parsed, diff --git a/plugs/index/index.plug.yaml b/plugs/index/index.plug.yaml index 8c4af5de..2c05f169 100644 --- a/plugs/index/index.plug.yaml +++ b/plugs/index/index.plug.yaml @@ -155,44 +155,27 @@ functions: command: name: "Page: Extract" - # Mentions panel (postscript) - toggleMentions: - path: "./linked_mentions.ts:toggleMentions" - command: - name: "Mentions: Toggle" - key: ctrl-alt-m - priority: 5 - - renderMentions: - path: "./linked_mentions.ts:renderMentions" - panelWidget: bottom - # TOC - toggleTOC: - path: toc.ts:toggleTOC - command: - name: "Table of Contents: Toggle" - key: ctrl-alt-t - priority: 5 + tocWidget: + path: toc.ts:widget + codeWidget: toc + renderMode: markdown - renderTOC: - path: toc.ts:renderTOC + # Template Widgets + renderTemplateWidgetsTop: + path: template_widget.ts:renderTemplateWidgets env: client panelWidget: top + renderTemplateWidgetsBottom: + path: template_widget.ts:renderTemplateWidgets + env: client + panelWidget: bottom + refreshWidgets: - path: toc.ts:refreshWidgets + path: template_widget.ts:refreshWidgets lintYAML: path: lint.ts:lintYAML events: - editor:lint - - renderFrontmatterWidget: - path: frontmatter.ts:renderFrontmatterWidget - env: client - panelWidget: frontmatter - - editFrontmatter: - path: frontmatter.ts:editFrontmatter - diff --git a/plugs/index/linked_mentions.ts b/plugs/index/linked_mentions.ts deleted file mode 100644 index f6c52fbb..00000000 --- a/plugs/index/linked_mentions.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { clientStore, codeWidget, editor, system } from "$sb/syscalls.ts"; -import { CodeWidgetContent } from "$sb/types.ts"; -import { queryObjects } from "./api.ts"; -import { LinkObject } from "./page_links.ts"; - -const hideMentionsKey = "hideMentions"; - -export async function toggleMentions() { - let hideMentions = await clientStore.get(hideMentionsKey); - hideMentions = !hideMentions; - await clientStore.set(hideMentionsKey, hideMentions); - await codeWidget.refreshAll(); -} - -export async function renderMentions(): Promise { - if (await clientStore.get(hideMentionsKey)) { - return null; - } - - const page = await editor.getCurrentPage(); - const linksResult = await queryObjects("link", { - // Query all links that point to this page - filter: ["and", ["!=", ["attr", "page"], ["string", page]], ["=", [ - "attr", - "toPage", - ], ["string", page]]], - }); - if (linksResult.length === 0) { - // Don't show the panel if there are no links here. - return null; - } else { - let renderedMd = "# Linked Mentions\n"; - for (const link of linksResult) { - let snippet = await system.invokeFunction( - "markdown.markdownToHtml", - link.snippet, - ); - // strip HTML tags - snippet = snippet.replace(/<[^>]*>?/gm, ""); - renderedMd += `* [[${link.ref}]]: ...${snippet}...\n`; - } - return { - markdown: renderedMd, - buttons: [ - { - description: "Reload", - svg: - ``, - invokeFunction: "index.refreshWidgets", - }, - - { - description: "Hide", - svg: - ``, - invokeFunction: "index.toggleMentions", - }, - ], - }; - } -} diff --git a/plugs/index/frontmatter.ts b/plugs/index/template_widget.ts similarity index 56% rename from plugs/index/frontmatter.ts rename to plugs/index/template_widget.ts index 465d8c62..afffcf6a 100644 --- a/plugs/index/frontmatter.ts +++ b/plugs/index/template_widget.ts @@ -1,22 +1,26 @@ +import { + codeWidget, + editor, + language, + markdown, + space, +} from "$sb/silverbullet-syscall/mod.ts"; +import { parseTreeToAST, renderToText } from "$sb/lib/tree.ts"; import { CodeWidgetContent } from "$sb/types.ts"; -import { editor, language, markdown, space } from "$sb/syscalls.ts"; -import { extractFrontmatter } from "$sb/lib/frontmatter.ts"; +import { loadPageObject } from "../template/template.ts"; import { queryObjects } from "./api.ts"; import { TemplateObject } from "../template/types.ts"; -import { renderTemplate } from "../template/plug_api.ts"; -import { loadPageObject } from "../template/template.ts"; import { expressionToKvQueryExpression } from "$sb/lib/parse-query.ts"; import { evalQueryExpression } from "$sb/lib/query.ts"; -import { parseTreeToAST } from "$sb/lib/tree.ts"; +import { renderTemplate } from "../template/plug_api.ts"; +import { extractFrontmatter } from "$sb/lib/frontmatter.ts"; +import { rewritePageRefs } from "$sb/lib/resolve.ts"; -// Somewhat decent looking default template -const fallbackTemplate = `{{#each .}} -{{#ifEq @key "tags"}}{{else}}**{{@key}}**: {{.}} -{{/ifEq}} -{{/each}} -{{#if tags}}_Tagged with_ {{#each tags}}#{{.}} {{/each}}{{/if}}`; +export async function refreshWidgets() { + await codeWidget.refreshAll(); +} -export async function renderFrontmatterWidget(): Promise< +export async function renderTemplateWidgets(side: "top" | "bottom"): Promise< CodeWidgetContent | null > { const text = await editor.getText(); @@ -27,11 +31,11 @@ export async function renderFrontmatterWidget(): Promise< const allFrontMatterTemplates = await queryObjects( "template", { - filter: ["=", ["attr", "type"], ["string", "frontmatter"]], + filter: ["=", ["attr", "type"], ["string", `widget:${side}`]], orderBy: [{ expr: ["attr", "priority"], desc: false }], }, ); - let templateText = fallbackTemplate; + 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) { const exprAST = parseTreeToAST( @@ -40,19 +44,25 @@ export async function renderFrontmatterWidget(): Promise< const parsedExpression = expressionToKvQueryExpression(exprAST[1]); if (evalQueryExpression(parsedExpression, pageMeta)) { // Match! We're happy - templateText = await space.readPage(template.ref); - break; + const templateText = await space.readPage(template.ref); + // templateBits.push(await space.readPage(template.ref)); + let renderedTemplate = (await renderTemplate( + templateText, + pageMeta, + frontmatter, + )).text; + + const parsedMarkdown = await markdown.parseMarkdown(renderedTemplate); + rewritePageRefs(parsedMarkdown, template.ref); + renderedTemplate = renderToText(parsedMarkdown); + + templateBits.push(renderedTemplate); } } - const summaryText = await renderTemplate( - templateText, - pageMeta, - frontmatter, - ); + const summaryText = templateBits.join(""); // console.log("Rendered", summaryText); return { - markdown: summaryText.text, - banner: "frontmatter", + markdown: summaryText, buttons: [ { description: "Reload", @@ -60,23 +70,6 @@ export async function renderFrontmatterWidget(): Promise< ``, invokeFunction: "index.refreshWidgets", }, - { - description: "Edit", - svg: - ``, - invokeFunction: "index.editFrontmatter", - }, - { - description: "", - svg: "", - widgetTarget: true, - invokeFunction: "index.editFrontmatter", - }, ], }; } - -export async function editFrontmatter() { - // 4 = after the frontmatter (--- + newline) - await editor.moveCursor(4, true); -} diff --git a/plugs/index/toc.ts b/plugs/index/toc.ts index a71002d2..2007719a 100644 --- a/plugs/index/toc.ts +++ b/plugs/index/toc.ts @@ -1,14 +1,8 @@ -import { - clientStore, - codeWidget, - editor, - markdown, -} from "$sb/silverbullet-syscall/mod.ts"; -import { renderToText, traverseTree } from "$sb/lib/tree.ts"; +import { editor, markdown, YAML } from "$sb/syscalls.ts"; import { CodeWidgetContent } from "$sb/types.ts"; +import { renderToText, traverseTree } from "$sb/lib/tree.ts"; -const hideTOCKey = "hideTOC"; -const headerThreshold = 3; +const defaultHeaderThreshold = 0; type Header = { name: string; @@ -16,21 +10,19 @@ type Header = { level: number; }; -export async function toggleTOC() { - let hideTOC = await clientStore.get(hideTOCKey); - hideTOC = !hideTOC; - await clientStore.set(hideTOCKey, hideTOC); - await codeWidget.refreshAll(); -} +type TocConfig = { + minHeaders?: number; + header?: boolean; +}; -export async function refreshWidgets() { - await codeWidget.refreshAll(); -} - -export async function renderTOC(): Promise { - if (await clientStore.get(hideTOCKey)) { - return null; +export async function widget( + bodyText: string, +): Promise { + let config: TocConfig = {}; + if (bodyText.trim() !== "") { + config = await YAML.parse(bodyText); } + const page = await editor.getCurrentPage(); const text = await editor.getText(); const tree = await markdown.parseMarkdown(text); @@ -47,17 +39,26 @@ export async function renderTOC(): Promise { } return false; }); + + let headerThreshold = defaultHeaderThreshold; + if (config.minHeaders) { + headerThreshold = config.minHeaders; + } if (headers.length < headerThreshold) { // Not enough headers, not showing TOC return null; } + let headerText = "# Table of Contents\n"; + if (config.header === false) { + headerText = ""; + } // console.log("Headers", headers); // Adjust level down if only sub-headers are used const minLevel = headers.reduce( (min, header) => Math.min(min, header.level), 6, ); - const renderedMd = "# Table of Contents\n" + + const renderedMd = headerText + headers.map((header) => `${ " ".repeat((header.level - minLevel) * 2) @@ -67,18 +68,18 @@ export async function renderTOC(): Promise { return { markdown: renderedMd, buttons: [ + { + description: "Edit", + svg: + ``, + invokeFunction: "query.editButton", + }, { description: "Reload", svg: ``, invokeFunction: "index.refreshWidgets", }, - { - description: "Hide", - svg: - ``, - invokeFunction: "index.toggleTOC", - }, ], }; } diff --git a/plugs/markdown/api.ts b/plugs/markdown/api.ts index 7f7fb449..6612755e 100644 --- a/plugs/markdown/api.ts +++ b/plugs/markdown/api.ts @@ -36,8 +36,13 @@ export async function expandCodeWidgets( renderToText(codeTextNode!), pageName, ); + if (!result) { + return { + text: "", + }; + } // Only do this for "markdown" widgets, that is: that can render to markdown - if (result.markdown) { + if (result.markdown !== undefined) { const parsedBody = await parseMarkdown(result.markdown); // Recursively process return expandCodeWidgets( diff --git a/plugs/markdown/markdown.plug.yaml b/plugs/markdown/markdown.plug.yaml index ecde3a78..9d434ffa 100644 --- a/plugs/markdown/markdown.plug.yaml +++ b/plugs/markdown/markdown.plug.yaml @@ -29,7 +29,3 @@ functions: path: "./preview.ts:previewClickHandler" events: - preview:click - - markdownWidget: - path: ./widget.ts:markdownWidget - codeWidget: markdown diff --git a/plugs/markdown/widget.ts b/plugs/markdown/widget.ts deleted file mode 100644 index e2b3b10a..00000000 --- a/plugs/markdown/widget.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { markdown } from "$sb/syscalls.ts"; -import type { WidgetContent } from "$sb/app_event.ts"; -import { renderMarkdownToHtml } from "./markdown_render.ts"; - -export async function markdownWidget( - bodyText: string, -): Promise { - const mdTree = await markdown.parseMarkdown(bodyText); - - const html = renderMarkdownToHtml(mdTree, { - smartHardBreak: true, - }); - return Promise.resolve({ - html: html, - script: ` - document.addEventListener("click", () => { - api({type: "blur"}); - });`, - }); -} diff --git a/plugs/query/query.ts b/plugs/query/query.ts index d8f72cc0..b8bc6f5e 100644 --- a/plugs/query/query.ts +++ b/plugs/query/query.ts @@ -4,7 +4,12 @@ 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 } from "$sb/types.ts"; +import { + CodeWidgetContent, + LintDiagnostic, + PageMeta, + Query, +} from "$sb/types.ts"; import { jsonToMDTable, renderQueryTemplate } from "../template/util.ts"; export async function widget( @@ -12,53 +17,33 @@ export async function widget( pageName: string, ): Promise { const pageObject = await loadPageObject(pageName); - try { + let resultMarkdown = ""; const parsedQuery = await parseQuery( await replaceTemplateVars(bodyText, pageObject), ); - if (!parsedQuery.limit) { - parsedQuery.limit = ["number", 1000]; - } - - const eventName = `query:${parsedQuery.querySource}`; - - let resultMarkdown = ""; - - // 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, + const results = await performQuery( + parsedQuery, + pageObject, ); - if (results.length === 0) { - // This means there was no handler for the event which means it's unsupported - return { - html: - `**Error:** Unsupported query source '${parsedQuery.querySource}'`, - }; + if (results.length === 0 && !parsedQuery.renderAll) { + resultMarkdown = "No results"; } else { - const allResults = results.flat(); - if (allResults.length === 0) { - resultMarkdown = "No results"; + 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 { - if (parsedQuery.render) { - // Configured a custom rendering template, let's use it! - const templatePage = resolvePath(pageName, parsedQuery.render); - const rendered = await renderQueryTemplate( - pageObject, - templatePage, - allResults, - 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(allResults); - } + // 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); } } @@ -84,6 +69,25 @@ export async function widget( } } +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 { diff --git a/plugs/query/template.ts b/plugs/query/template.ts index 6d19fda9..8cad01b4 100644 --- a/plugs/query/template.ts +++ b/plugs/query/template.ts @@ -4,14 +4,20 @@ import { CodeWidgetContent, PageMeta } from "$sb/types.ts"; import { renderTemplate } from "../template/plug_api.ts"; import { renderToText } from "$sb/lib/tree.ts"; import { rewritePageRefs, rewritePageRefsInString } from "$sb/lib/resolve.ts"; +import { performQuery } from "./query.ts"; +import { parseQuery } from "$sb/lib/parse-query.ts"; type TemplateConfig = { // Pull the template from a page page?: string; // Or use a string directly template?: string; - // Optional argument to pass + // To feed data into the template you can either use a concrete value value?: any; + + // Or a query + query?: string; + // If true, don't render the template, just use it as-is raw?: boolean; }; @@ -38,11 +44,20 @@ export async function widget( templateText = await space.readPage(templatePage); } - const value = config.value - ? JSON.parse( + let value: any; + + if (config.value) { + value = JSON.parse( await replaceTemplateVars(JSON.stringify(config.value), pageMeta), - ) - : undefined; + ); + } + + if (config.query) { + const parsedQuery = await parseQuery( + await replaceTemplateVars(config.query, pageMeta), + ); + value = await performQuery(parsedQuery, pageMeta); + } let { text: rendered } = config.raw ? { text: templateText } diff --git a/plugs/template/util.test.ts b/plugs/template/util.test.ts deleted file mode 100644 index 5f0723ea..00000000 --- a/plugs/template/util.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { assertEquals } from "../../test_deps.ts"; -import { isTemplate } from "./util.ts"; - -Deno.test("Test template extraction", () => { - assertEquals( - isTemplate(`--- -name: bla -tags: template ---- - -Sup`), - true, - ); - - assertEquals( - isTemplate(`--- -tags: template, something else ---- -`), - true, - ); - - assertEquals( - isTemplate(`--- -tags: something else, template ---- -`), - true, - ); - - assertEquals( - isTemplate(`--- -tags: -- bla -- template ---- -`), - true, - ); - - assertEquals( - isTemplate(`#template`), - true, - ); - - assertEquals( - isTemplate(` #template This is a template`), - true, - ); - - assertEquals( - isTemplate(`--- -tags: -- bla -somethingElse: -- template ---- -`), - false, - ); - - assertEquals( - isTemplate(`--- -name: bla -tags: aefe ---- - -Sup`), - false, - ); - - assertEquals( - isTemplate(`Sup`), - false, - ); -}); diff --git a/plugs/template/util.ts b/plugs/template/util.ts index af851f00..1855d79f 100644 --- a/plugs/template/util.ts +++ b/plugs/template/util.ts @@ -1,34 +1,8 @@ -import { determineTags } from "$sb/lib/cheap_yaml.ts"; import { handlebarHelpers } from "../../common/syscalls/handlebar_helpers.ts"; import { PageMeta } from "$sb/types.ts"; import { handlebars, space } from "$sb/syscalls.ts"; import { cleanTemplate } from "./plug_api.ts"; -const frontMatterRegex = /^---\n(([^\n]|\n)*?)---\n/; - -/** - * Quick and dirty way to check if a page is a template or not - * @param pageText - * @returns - */ -export function isTemplate(pageText: string): boolean { - const frontmatter = frontMatterRegex.exec(pageText); - // Poor man's YAML frontmatter parsing - if (frontmatter) { - pageText = pageText.slice(frontmatter[0].length); - const frontmatterText = frontmatter[1]; - const tags = determineTags(frontmatterText); - if (tags.includes("template")) { - return true; - } - } - // Or if the page text starts with a #template tag - if (/^\s*#template(\W|$)/.test(pageText)) { - return true; - } - return false; -} - export function buildHandebarOptions(pageMeta: PageMeta) { return { helpers: handlebarHelpers(), diff --git a/server/http_server.ts b/server/http_server.ts index 01db7b06..c23236fe 100644 --- a/server/http_server.ts +++ b/server/http_server.ts @@ -460,17 +460,18 @@ export class HttpServer { name, ); if ( - name.endsWith(".md") && !request.headers.has("X-Sync-Mode") && + name.endsWith(".md") && + // This header signififies the requests comes directly from the http_space_primitives client (not the browser) + !request.headers.has("X-Sync-Mode") && + // This Accept header is used by federation to still work with CORS + request.headers.get("Accept") !== + "application/octet-stream" && request.headers.get("sec-fetch-mode") !== "cors" ) { // It can happen that during a sync, authentication expires, this may result in a redirect to the login page and then back to this particular file. This particular file may be an .md file, which isn't great to show so we're redirecting to the associated SB UI page. console.warn( "Request was without X-Sync-Mode nor a CORS request, redirecting to page", ); - // Log all request headers - // for (const [key, value] of request.headers.entries()) { - // console.log("Header", key, value); - // } response.redirect(`/${name.slice(0, -3)}`); return; } diff --git a/web/client.ts b/web/client.ts index 0113bd19..2ca5550b 100644 --- a/web/client.ts +++ b/web/client.ts @@ -315,7 +315,10 @@ export class Client { setTimeout(() => { this.editorView.dispatch({ selection: { anchor: pos as number }, - effects: EditorView.scrollIntoView(pos as number, { y: "start" }), + effects: EditorView.scrollIntoView(pos as number, { + y: "start", + yMargin: 5, + }), }); }); } else if (!stateRestored) { diff --git a/web/cm_plugins/fenced_code.ts b/web/cm_plugins/fenced_code.ts index 008d26f4..2f17f198 100644 --- a/web/cm_plugins/fenced_code.ts +++ b/web/cm_plugins/fenced_code.ts @@ -8,6 +8,7 @@ import { } from "./util.ts"; import { MarkdownWidget } from "./markdown_widget.ts"; import { IFrameWidget } from "./iframe_widget.ts"; +import { isTemplate } from "$sb/lib/cheap_yaml.ts"; export function fencedCodePlugin(editor: Client) { return decoratorStateField((state: EditorState) => { @@ -27,7 +28,8 @@ export function fencedCodePlugin(editor: Client) { const renderMode = editor.system.codeWidgetHook.codeWidgetModes.get( lang, ); - if (codeWidgetCallback) { + // Only custom render when we have a custom renderer, and the current page is not a template + if (codeWidgetCallback && !isTemplate(state.sliceDoc(0, from))) { // We got a custom renderer! const lineStrings = text.split("\n"); diff --git a/web/cm_plugins/iframe_widget.ts b/web/cm_plugins/iframe_widget.ts index 0c6f5c64..7dd4ccb1 100644 --- a/web/cm_plugins/iframe_widget.ts +++ b/web/cm_plugins/iframe_widget.ts @@ -35,14 +35,23 @@ export class IFrameWidget extends WidgetType { case "reload": this.codeWidgetCallback(this.bodyText, this.client.currentPage!) .then( - (widgetContent: WidgetContent) => { - iframe.contentWindow!.postMessage({ - type: "html", - html: widgetContent.html, - script: widgetContent.script, - theme: - document.getElementsByTagName("html")[0].dataset.theme, - }); + (widgetContent: WidgetContent | null) => { + if (widgetContent === null) { + iframe.contentWindow!.postMessage({ + type: "html", + html: "", + theme: + document.getElementsByTagName("html")[0].dataset.theme, + }); + } else { + iframe.contentWindow!.postMessage({ + type: "html", + html: widgetContent.html, + script: widgetContent.script, + theme: + document.getElementsByTagName("html")[0].dataset.theme, + }); + } }, ); break; diff --git a/web/cm_plugins/markdown_widget.ts b/web/cm_plugins/markdown_widget.ts index 29b3bda4..762ace94 100644 --- a/web/cm_plugins/markdown_widget.ts +++ b/web/cm_plugins/markdown_widget.ts @@ -27,12 +27,10 @@ export class MarkdownWidget extends WidgetType { div.className = this.className; const cacheItem = this.client.getWidgetCache(this.cacheKey); if (cacheItem) { - div.innerHTML = this.wrapHtml( - cacheItem.html, - cacheItem.buttons || [], - cacheItem.banner, - ); - this.attachListeners(div, cacheItem.buttons); + div.innerHTML = this.wrapHtml(cacheItem.html, cacheItem.buttons); + if (cacheItem.html) { + this.attachListeners(div, cacheItem.buttons); + } } // Async kick-off of content renderer @@ -90,6 +88,7 @@ export class MarkdownWidget extends WidgetType { }, preserveAttributes: true, }); + // console.log("Got html", html); if (cachedHtml === html) { // HTML still same as in cache, no need to re-render @@ -97,10 +96,11 @@ export class MarkdownWidget extends WidgetType { } div.innerHTML = this.wrapHtml( html, - widgetContent.buttons || [], - widgetContent.banner, + widgetContent.buttons, ); - this.attachListeners(div, widgetContent.buttons); + if (html) { + this.attachListeners(div, widgetContent.buttons); + } // Let's give it a tick, then measure and cache setTimeout(() => { @@ -110,7 +110,6 @@ export class MarkdownWidget extends WidgetType { height: div.offsetHeight, html, buttons: widgetContent.buttons, - banner: widgetContent.banner, }, ); // Because of the rejiggering of the DOM, we need to do a no-op cursor move to make sure it's positioned correctly @@ -124,8 +123,7 @@ export class MarkdownWidget extends WidgetType { private wrapHtml( html: string, - buttons: CodeWidgetButton[], - banner?: string, + buttons: CodeWidgetButton[] = [], ) { if (!html) { return ""; @@ -134,9 +132,7 @@ export class MarkdownWidget extends WidgetType { buttons.filter((button) => !button.widgetTarget).map((button, idx) => ` ` ).join("") - }${ - banner ? `
${escapeHtml(banner)}
` : "" - }${html}`; + }${html}`; } private attachListeners(div: HTMLElement, buttons?: CodeWidgetButton[]) { @@ -255,10 +251,3 @@ function garbageCollectWidgets() { } setInterval(garbageCollectWidgets, 5000); - -function escapeHtml(text: string) { - return text.replace(/&/g, "&").replace(//g, - ">", - ); -} diff --git a/web/cm_plugins/top_bottom_panels.ts b/web/cm_plugins/top_bottom_panels.ts index 1d0d7db0..96c0daf8 100644 --- a/web/cm_plugins/top_bottom_panels.ts +++ b/web/cm_plugins/top_bottom_panels.ts @@ -17,7 +17,7 @@ export function postScriptPrefacePlugin( undefined, editor, `top:${editor.currentPage}`, - "", + "top", topCallback, "sb-markdown-top-widget", ), @@ -34,7 +34,7 @@ export function postScriptPrefacePlugin( undefined, editor, `bottom:${editor.currentPage}`, - "", + "bottom", bottomCallback, "sb-markdown-bottom-widget", ), diff --git a/web/components/widget_sandbox_iframe.ts b/web/components/widget_sandbox_iframe.ts index 275fd6ca..61acb1ea 100644 --- a/web/components/widget_sandbox_iframe.ts +++ b/web/components/widget_sandbox_iframe.ts @@ -104,7 +104,7 @@ export function mountIFrame( preloadedIFrame: PreloadedIFrame, client: Client, widgetHeightCacheKey: string | null, - content: WidgetContent | Promise, + content: WidgetContent | null | Promise, onMessage?: (message: any) => void, ) { const iframe = preloadedIFrame.iframe; @@ -174,26 +174,28 @@ export function mountIFrame( console.warn("Iframe went away or content was not loaded"); return; } - if (resolvedContent.html) { - iframe.contentWindow!.postMessage({ - type: "html", - html: resolvedContent.html, - script: resolvedContent.script, - theme: document.getElementsByTagName("html")[0].dataset.theme, - }); - } else if (resolvedContent.url) { - iframe.contentWindow!.location.href = resolvedContent.url; - if (resolvedContent.height) { - iframe.height = resolvedContent.height + "px"; - if (widgetHeightCacheKey) { - client.setCachedWidgetHeight( - widgetHeightCacheKey!, - resolvedContent.height, - ); + if (resolvedContent) { + if (resolvedContent.html) { + iframe.contentWindow!.postMessage({ + type: "html", + html: resolvedContent.html, + script: resolvedContent.script, + theme: document.getElementsByTagName("html")[0].dataset.theme, + }); + } else if (resolvedContent.url) { + iframe.contentWindow!.location.href = resolvedContent.url; + if (resolvedContent.height) { + iframe.height = resolvedContent.height + "px"; + if (widgetHeightCacheKey) { + client.setCachedWidgetHeight( + widgetHeightCacheKey!, + resolvedContent.height, + ); + } + } + if (resolvedContent.width) { + iframe.width = resolvedContent.width + "px"; } - } - if (resolvedContent.width) { - iframe.width = resolvedContent.width + "px"; } } }).catch(console.error); @@ -202,7 +204,7 @@ export function mountIFrame( export function createWidgetSandboxIFrame( client: Client, widgetHeightCacheKey: string | null, - content: WidgetContent | Promise, + content: WidgetContent | null | Promise, onMessage?: (message: any) => void, ) { // console.log("Claiming iframe"); diff --git a/web/hooks/panel_widget.ts b/web/hooks/panel_widget.ts index 0d30fe8a..3b7bd0c7 100644 --- a/web/hooks/panel_widget.ts +++ b/web/hooks/panel_widget.ts @@ -48,7 +48,7 @@ export class PanelWidgetHook implements Hook { if (!functionDef.panelWidget) { continue; } - if (!["top", "bottom", "frontmatter"].includes(functionDef.panelWidget)) { + if (!["top", "bottom"].includes(functionDef.panelWidget)) { errors.push( `Panel widgets must be attached to either 'top' or 'bottom'.`, ); diff --git a/web/styles/editor.scss b/web/styles/editor.scss index 91fa2eaa..7e675b2f 100644 --- a/web/styles/editor.scss +++ b/web/styles/editor.scss @@ -429,8 +429,6 @@ margin-top: 10px; } - - .sb-markdown-widget, .sb-markdown-top-widget:has(*), .sb-markdown-bottom-widget:has(*) { diff --git a/web/syscalls/code_widget.ts b/web/syscalls/code_widget.ts index 1cb34e9c..5b8626af 100644 --- a/web/syscalls/code_widget.ts +++ b/web/syscalls/code_widget.ts @@ -11,7 +11,7 @@ export function codeWidgetSyscalls( lang: string, body: string, pageName: string, - ): Promise => { + ): Promise => { const langCallback = codeWidgetHook.codeWidgetCallbacks.get( lang, ); diff --git a/website/CHANGELOG.md b/website/CHANGELOG.md index 82b7e6ea..aa69cab4 100644 --- a/website/CHANGELOG.md +++ b/website/CHANGELOG.md @@ -6,8 +6,14 @@ release. _Not yet released, this will likely become 0.6.0._ * **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. -* Custom renderer for [[Frontmatter]], enabling... [[Live Frontmatter Templates]] to specify custom rendering (using [[Templates]] of course) — see some of the plugs pages (e.g. [[Plugs/Editor]], [[Plugs/Git]]) to see what you can do with this (template here: [[internal-template/plug-frontmatter]]). -* Somewhat nicer rendering of {{templateVars}}. +* 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]] +* 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. --- @@ -129,6 +135,6 @@ Other notable changes: * [[Plugs/Tasks]] now support custom states (not just `[x]` and `[ ]`), for example: * [IN PROGRESS] An in progress task * [BLOCKED] A task that’s blocked - [[🔌 Tasks|Read more]] + [[Plugs/Tasks|Read more]] * Removed [[Cloud Links]] support in favor of [[Federation]]. If you still have legacy cloud links, simply replace the 🌩️ with a `!` and things should work as before. diff --git a/website/Linked Mentions.md b/website/Linked Mentions.md index 0fce43c8..6d974f7d 100644 --- a/website/Linked Mentions.md +++ b/website/Linked Mentions.md @@ -1 +1,3 @@ -Linked mentions \ No newline at end of file +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]]. + +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]]. diff --git a/website/Live Frontmatter Templates.md b/website/Live Frontmatter Templates.md deleted file mode 100644 index 624ad602..00000000 --- a/website/Live Frontmatter Templates.md +++ /dev/null @@ -1,36 +0,0 @@ -Live Frontmatter Templates allow you to override the default rendering of [[Frontmatter]] at the top of your pages with a custom template. - -> **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 Frontmatter Templates follow the same pattern as other [[Templates]] with a few additional attributes: - -* `tags`: should be set to `template` as for any other template -* `type`: should be set to `frontmatter` -* `where`: should contain an [[Live Queries$expression]] that evaluates to true for the _pages_ you would like to apply this Live Frontmatter 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 Live Frontmatter Templates that have matching `where` expression, the one with the priority set to the lowest number wins. - -# Example -The following Frontmatter Template applies to all pages tagged with `person` (see the `where`). It first lists all [[Frontmatter]] attributes, followed by a use of the [[!silverbullet.md/template/live/incoming]] template, showing all incomplete tasks that reference this particular page. - -Indeed, you can use [[Live Queries]] and [[Live Templates]] here as well. - - --- - tags: template - type: frontmatter - where: 'tags = "person"' - --- - {{#each .}}**{{@key}}**: {{.}} - {{/each}} - ## Incoming tasks - ```template - page: "[[!silverbullet.md/template/live/incoming]]" - ``` - -## Plug frontmatter template -This site uses the [[internal-template/plug-frontmatter]] template for pages tagged with `plug`, such as [[Plugs/Editor]], [[Plugs/Github]] and [[Plugs/Mermaid]]. - - diff --git a/website/Live Template Widgets.md b/website/Live Template Widgets.md new file mode 100644 index 00000000..0dfe4b0a --- /dev/null +++ b/website/Live Template Widgets.md @@ -0,0 +1,34 @@ +Live Template Widgets allow you to automatically render templated markdown widgets to 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: + +* `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. + +# 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. + + --- + tags: template + type: frontmatter + where: 'tags = "person"' + --- + ## Incoming tasks + ```template + page: "[[!silverbullet.md/template/live/incoming]]" + ``` + +## 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]]. + + diff --git a/website/Live Templates.md b/website/Live Templates.md index 0738f9dc..bfb3ab69 100644 --- a/website/Live Templates.md +++ b/website/Live Templates.md @@ -1,4 +1,4 @@ -Live templates rendering [[Templates]] inline in a page. +Live templates render [[Templates]] inline in a page. They’re called “Live” because their content updates dynamically. ## 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. @@ -16,7 +16,7 @@ template: | Today is {{today}}! ``` -To pass in a value to the template, you can specify the optional `value` attribute: +To pass a literal value to the template, you can specify the optional `value` attribute: ```template template: | Hello, {{name}}! Today is _{{today}}_ @@ -24,6 +24,17 @@ value: name: Pete ``` +You can also pass in the result of a [[Live Queries|query]] as a value by setting the `query` attribute: + +```template +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: ```template template: | diff --git a/website/Markdown/Code Widgets.md b/website/Markdown/Code Widgets.md index 6dcf957d..d174ac8c 100644 --- a/website/Markdown/Code Widgets.md +++ b/website/Markdown/Code Widgets.md @@ -1,28 +1,21 @@ -Code widgets are a SilverBullet-specific “extension” to [[Markdown]]. Technically, it’s not an extension — it just gives new meaning to markdown’s native fenced code blocks — code blocks that start with a triple backtick, specifying a programming language. +Code widgets are a SilverBullet-specific [[Markdown/Extensions|extension]] to [[Markdown]]. Technically, it’s not an extension — it just gives new meaning to markdown’s native fenced code blocks — code blocks that start with a triple backtick, specifying a programming language. -Currently, SilverBullet provides two code widgets as part of its built-in [[Plugs]]: +Currently, SilverBullet provides a few code widgets out of the box: +* `toc`: [[Table of Contents]] +* `query`: [[Live Queries]] +* `template`: [[Live Templates]] * `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, for instance, embed youtube videos. In fact, there is specific support for those. - -Two examples. - -First, embedding the silverbullet.md website inside the silverbullet.md website (inception!): - -```embed -url: https://silverbullet.md -height: 500 -``` - +## `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://www.youtube.com/watch?v=VemS-cqAD5k +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. @@ -32,10 +25,3 @@ The body of an `embed` block is written in [[YAML]] and supports the following a * `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 - -## Markdown -You can embed markdown inside of markdown and live preview it. Is this useful? 🤷 Not particularly, it’s more of a demo of how this works. Nevertheless, to each their own, here’s an example: - -```markdown -This is going to be **bold** -``` \ No newline at end of file diff --git a/website/Markdown/Extensions.md b/website/Markdown/Extensions.md index 8c8d1447..5727e48e 100644 --- a/website/Markdown/Extensions.md +++ b/website/Markdown/Extensions.md @@ -6,6 +6,7 @@ In addition to supporting [[Markdown/Basics|markdown basics]] as standardized by * Generically via [[Markdown/Code Widgets]] * [[Live Queries]] * [[Live Templates]] + * [[Table of Contents]] * [[Anchors]] * [[Markdown/Admonitions]] * Hashtags, e.g. `#mytag`. diff --git a/website/Plugs/Editor.md b/website/Plugs/Editor.md index 282fe12d..449e8779 100644 --- a/website/Plugs/Editor.md +++ b/website/Plugs/Editor.md @@ -1,6 +1,4 @@ ---- -tags: plug ---- +#plug The `editor` plug implements foundational editor functionality for SilverBullet. diff --git a/website/Plugs/Template.md b/website/Plugs/Template.md index a7be3ad2..f71d1de1 100644 --- a/website/Plugs/Template.md +++ b/website/Plugs/Template.md @@ -4,16 +4,12 @@ tags: plug The [[Plugs/Template]] plug implements a few templating mechanisms. # Daily Note - The {[Open Daily Note]} command navigates (or creates) a daily note prefixed with a 📅 emoji by default, but this is configurable via the `dailyNotePrefix` setting in `SETTINGS`. If you have a page template (see above) named `template/page/Daily Note` it will use this as a template, otherwise, the page will just be empty (this path is also configurable via the `dailyNoteTemplate` setting). # Weekly Note - -The {[Open Weekly Note]} command navigates (or creates) a weekly note prefixed -with a 🗓️ emoji by default, but this is configurable via the `weeklyNotePrefix` setting in `SETTINGS`. If you have a page template (see above) named `template/page/Weekly Note` it will use this as a template, otherwise, the page will just be empty. +The {[Open Weekly Note]} command navigates (or creates) a weekly note prefixed with a 🗓️ emoji by default, but this is configurable via the `weeklyNotePrefix` setting in `SETTINGS`. If you have a page template (see above) named `template/page/Weekly Note` it will use this as a template, otherwise, the page will just be empty. # Quick Note - The {[Quick Note]} command will navigate to an empty page named with the current date and time prefixed with a 📥 emoji, but this is configurable via the `quickNotePrefix` in `SETTINGS`. The use case is to take a quick note outside of your current context. # Built-in slash commands diff --git a/website/Table of Contents.md b/website/Table of Contents.md index f960fd35..f5196ec7 100644 --- a/website/Table of Contents.md +++ b/website/Table of Contents.md @@ -1,3 +1,30 @@ -The Table of Contents widget, when enabled, shows a table of contents at the start of the page for any page with 3 headers or more. It is updated whenever hovering the mouse cursor over it. Clicking any of the headers will navigate there within the page. +You can add a table of contents to a page using the `toc` [[Markdown/Code Widgets|Code Widget]]. -You can enable/disable this feature via {[Table of Contents: Toggle]}. \ No newline at end of file +In its most basic form it looks like this (click the edit button to see the code): + +```toc +``` + +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 +``` + +## Configuration +In the body of the `toc` code widget you can configure a few options: + +* `header`: by default a “Table of Contents” header is added to the ToC, set this to `false` to disable rendering this header +* `minHeaders`: only renders a ToC if the number of headers in the current page exceeds this number, otherwise render an empty widget + +Example: +```toc +header: false +minHeaders: 1 +``` diff --git a/website/Template Sets.md b/website/Template Sets.md index bf84381e..3d840230 100644 --- a/website/Template Sets.md +++ b/website/Template Sets.md @@ -54,3 +54,13 @@ where type = "query" order by order render [[template/documented-template]] ``` + +# Live Widget Templates +Use these to add [[Table of Contents]] and [[Linked Mentions]] 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 4be3a2e5..567b12c2 100644 --- a/website/Templates.md +++ b/website/Templates.md @@ -5,7 +5,7 @@ There are two general uses for templates: 1. _Live_ uses, where page content is dynamically updated based on templates: * [[Live Queries]] * [[Live Templates]] - * [[Live Frontmatter Templates]] + * [[Live Template Widgets]] 2. _One-off_ uses, where a template is instantiated once and inserted into an existing or new page: * [[Slash Templates]] * [[Page Templates]] @@ -25,7 +25,7 @@ Tagging a page with a `#template` tag (either in the [[Frontmatter]] or using a [[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 Frontmatter Templates]] +* `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. diff --git a/website/internal-template/plug-frontmatter.md b/website/internal-template/plug-widget.md similarity index 73% rename from website/internal-template/plug-frontmatter.md rename to website/internal-template/plug-widget.md index 13708d7e..eef144a3 100644 --- a/website/internal-template/plug-frontmatter.md +++ b/website/internal-template/plug-widget.md @@ -1,6 +1,6 @@ --- tags: template -type: frontmatter +type: widget:top where: 'tags = "plug"' --- {{#if author}}This page documents a [[Plugs|plug]] created by **{{author}}**. [Repository]({{repo}}).{{else}}This page documents a [[Plugs|plug]] built into SilverBullet.{{/if}} diff --git a/website/template/widget/linked-mentions.md b/website/template/widget/linked-mentions.md new file mode 100644 index 00000000..69cdc6bf --- /dev/null +++ b/website/template/widget/linked-mentions.md @@ -0,0 +1,19 @@ +--- +description: Adds Linked Mentions to all pages +tags: template +type: widget:bottom +where: 'true' +--- +```template +# We need to escape handlebars directives here, since we're embedding +# this template into a template (INCEPTION) +template: | + {{escape "#if ."}} + # Linked Mentions + {{escape "#each ."}} + * [[{{escape "ref"}}]]: `{{escape "snippet"}}` + {{escape "/each"}} + {{escape "/if"}} +query: | + link where toPage = "{{@page.name}}" and page != "{{@page.name}}" +``` diff --git a/website/template/widget/toc.md b/website/template/widget/toc.md new file mode 100644 index 00000000..47f32251 --- /dev/null +++ b/website/template/widget/toc.md @@ -0,0 +1,9 @@ +--- +description: Adds a Table of Contents to all pages +tags: template +type: widget:top +where: 'true' +--- +```toc +minHeaders: 3 +```