From d58db6aa1acef7084feca5cc221e98f325cfbfb5 Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Thu, 9 Nov 2023 09:26:44 +0100 Subject: [PATCH] No longer index templates tagged as #template --- plug-api/lib/frontmatter.ts | 25 ++++++--- plug-api/lib/query.ts | 2 +- plugs/directive/command.ts | 8 ++- plugs/directive/query_directive.ts | 4 +- plugs/directive/template_directive.ts | 7 ++- plugs/directive/util.ts | 8 +-- plugs/index/command.ts | 18 +++++-- plugs/markdown/html_render.ts | 4 -- plugs/query/query.ts | 4 +- plugs/template/api.ts | 51 ++++++++++++++++++ plugs/template/index.ts | 7 +++ plugs/template/plug_api.ts | 24 +++++++++ plugs/template/template.plug.yaml | 16 +++++- plugs/template/template.ts | 34 +++++------- plugs/template/types.ts | 10 ++++ plugs/template/util.test.ts | 76 +++++++++++++++++++++++++++ plugs/template/util.ts | 47 +++++++++++++++++ web/editor_ui.tsx | 24 ++++----- website/Templates.md | 2 +- 19 files changed, 307 insertions(+), 64 deletions(-) create mode 100644 plugs/template/api.ts create mode 100644 plugs/template/index.ts create mode 100644 plugs/template/plug_api.ts create mode 100644 plugs/template/types.ts create mode 100644 plugs/template/util.test.ts create mode 100644 plugs/template/util.ts diff --git a/plug-api/lib/frontmatter.ts b/plug-api/lib/frontmatter.ts index 5fd040cd..9f517cb9 100644 --- a/plug-api/lib/frontmatter.ts +++ b/plug-api/lib/frontmatter.ts @@ -11,12 +11,17 @@ import { export type FrontMatter = { tags: string[] } & Record; -// Extracts front matter (or legacy "meta" code blocks) from a markdown document +export type FrontmatterExtractOptions = { + removeKeys?: string[]; + removeTags?: string[] | true; + removeFrontmatterSection?: boolean; +}; + +// Extracts front matter from a markdown document // optionally removes certain keys from the front matter export async function extractFrontmatter( tree: ParseTree, - removeKeys: string[] = [], - removeFrontmatterSection = false, + options: FrontmatterExtractOptions = {}, ): Promise { let data: FrontMatter = { tags: [], @@ -37,6 +42,12 @@ export async function extractFrontmatter( if (!data.tags.includes(tagname)) { data.tags.push(tagname); } + if ( + options.removeTags === true || options.removeTags?.includes(tagname) + ) { + // Ugly hack to remove the hashtag + h.children![0].text = ""; + } }); } // Find FrontMatter and parse it @@ -55,10 +66,10 @@ export async function extractFrontmatter( if (typeof data.tags === "string") { data.tags = (data.tags as string).split(/,\s*/); } - if (removeKeys.length > 0) { + if (options.removeKeys && options.removeKeys.length > 0) { let removedOne = false; - for (const key of removeKeys) { + for (const key of options.removeKeys) { if (key in newData) { delete newData[key]; removedOne = true; @@ -69,7 +80,9 @@ export async function extractFrontmatter( } } // If nothing is left, let's just delete this whole block - if (Object.keys(newData).length === 0 || removeFrontmatterSection) { + if ( + Object.keys(newData).length === 0 || options.removeFrontmatterSection + ) { return null; } } catch (e: any) { diff --git a/plug-api/lib/query.ts b/plug-api/lib/query.ts index e37af6f8..a1613295 100644 --- a/plug-api/lib/query.ts +++ b/plug-api/lib/query.ts @@ -97,7 +97,7 @@ export function evalQueryExpression( return !(val1.length === val2.length && val1.every((v) => val2.includes(v))); } - return val1 !== val2; + return val1 != val2; } case "=~": { if (!Array.isArray(val2)) { diff --git a/plugs/directive/command.ts b/plugs/directive/command.ts index 40bc3428..4341ab02 100644 --- a/plugs/directive/command.ts +++ b/plugs/directive/command.ts @@ -28,7 +28,9 @@ export async function updateDirectivesOnPageCommand() { } const text = await editor.getText(); const tree = await markdown.parseMarkdown(text); - const metaData = await extractFrontmatter(tree, ["$disableDirectives"]); + const metaData = await extractFrontmatter(tree, { + removeKeys: ["$disableDirectives"], + }); if (isFederationPath(currentPage)) { console.info("Current page is a federation page, not updating directives."); @@ -173,7 +175,9 @@ async function updateDirectivesForPage( const pageMeta = await space.getPageMeta(pageName); const currentText = await space.readPage(pageName); const tree = await markdown.parseMarkdown(currentText); - const metaData = await extractFrontmatter(tree, ["$disableDirectives"]); + const metaData = await extractFrontmatter(tree, { + removeKeys: ["$disableDirectives"], + }); if (isFederationPath(pageName)) { console.info("Current page is a federation page, not updating directives."); diff --git a/plugs/directive/query_directive.ts b/plugs/directive/query_directive.ts index d4ce0fc4..a0beea60 100644 --- a/plugs/directive/query_directive.ts +++ b/plugs/directive/query_directive.ts @@ -1,7 +1,7 @@ import { events } from "$sb/syscalls.ts"; import { replaceTemplateVars } from "../template/template.ts"; -import { renderTemplate } from "./util.ts"; +import { renderQueryTemplate } from "./util.ts"; import { jsonToMDTable } from "./util.ts"; import { ParseTree, parseTreeToAST } from "$sb/lib/tree.ts"; import { astToKvQuery } from "$sb/lib/parse-query.ts"; @@ -38,7 +38,7 @@ export async function queryDirectiveRenderer( // console.log("Parsed query", parsedQuery); const allResults = results.flat(); if (parsedQuery.render) { - const rendered = await renderTemplate( + const rendered = await renderQueryTemplate( pageMeta, parsedQuery.render, allResults, diff --git a/plugs/directive/template_directive.ts b/plugs/directive/template_directive.ts index e6a53db5..dbd96e09 100644 --- a/plugs/directive/template_directive.ts +++ b/plugs/directive/template_directive.ts @@ -8,6 +8,7 @@ import { directiveRegex } from "./directives.ts"; import { updateDirectives } from "./command.ts"; import { resolvePath, rewritePageRefs } from "$sb/lib/resolve.ts"; import { PageMeta } from "$sb/types.ts"; +import { renderTemplate } from "../template/plug_api.ts"; const templateRegex = /\[\[([^\]]+)\]\]\s*(.*)\s*/; @@ -52,7 +53,7 @@ export async function templateDirectiveRenderer( templateText = await space.readPage(templatePath); } const tree = await markdown.parseMarkdown(templateText); - await extractFrontmatter(tree, [], true); // Remove entire frontmatter section, if any + await extractFrontmatter(tree, { removeFrontmatterSection: true }); // Remove entire frontmatter section, if any // Resolve paths in the template rewritePageRefs(tree, templatePath); @@ -63,9 +64,7 @@ export async function templateDirectiveRenderer( // if it's a template injection (not a literal "include") if (directive === "use") { - newBody = await handlebars.renderTemplate(newBody, parsedArgs, { - page: pageMeta, - }); + newBody = await renderTemplate(newBody, pageMeta, parsedArgs); // Recursively render directives const tree = await markdown.parseMarkdown(newBody); diff --git a/plugs/directive/util.ts b/plugs/directive/util.ts index d1275b6a..2b09139f 100644 --- a/plugs/directive/util.ts +++ b/plugs/directive/util.ts @@ -1,6 +1,7 @@ import { handlebars, space } from "$sb/syscalls.ts"; import { handlebarHelpers } from "../../common/syscalls/handlebar_helpers.ts"; import { PageMeta } from "$sb/types.ts"; +import { cleanTemplate, renderTemplate } from "../template/plug_api.ts"; export function defaultJsonTransformer(_k: string, v: any) { if (v === undefined) { @@ -53,13 +54,14 @@ export function jsonToMDTable( return lines.join("\n"); } -export async function renderTemplate( +export async function renderQueryTemplate( pageMeta: PageMeta, - renderTemplate: string, + templatePage: string, data: any[], renderAll: boolean, ): Promise { - let templateText = await space.readPage(renderTemplate); + let templateText = await space.readPage(templatePage); + templateText = await cleanTemplate(templateText); if (!renderAll) { templateText = `{{#each .}}\n${templateText}\n{{/each}}`; } diff --git a/plugs/index/command.ts b/plugs/index/command.ts index a5f73250..40c5d93d 100644 --- a/plugs/index/command.ts +++ b/plugs/index/command.ts @@ -2,6 +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"; export async function reindexCommand() { await editor.flashNotification("Performing full page reindex..."); @@ -42,9 +43,16 @@ export async function processIndexQueue(messages: MQMessage[]) { } export async function parseIndexTextRepublish({ name, text }: IndexEvent) { - // console.log("Reindexing", name); - await events.dispatchEvent("page:index", { - name, - tree: await markdown.parseMarkdown(text), - }); + if (isTemplate(text)) { + console.log("Indexing", name, "as template"); + await events.dispatchEvent("page:indexTemplate", { + name, + tree: await markdown.parseMarkdown(text), + }); + } else { + await events.dispatchEvent("page:index", { + name, + tree: await markdown.parseMarkdown(text), + }); + } } diff --git a/plugs/markdown/html_render.ts b/plugs/markdown/html_render.ts index 1a9a55e3..61e00107 100644 --- a/plugs/markdown/html_render.ts +++ b/plugs/markdown/html_render.ts @@ -41,9 +41,5 @@ export function renderHtml(t: Tag | null): string { if (t.name === Fragment) { return body; } - // if (t.body) { return `<${t.name}${attrs}>${body}`; - // } else { - // return `<${t.name}${attrs}/>`; - // } } diff --git a/plugs/query/query.ts b/plugs/query/query.ts index 71406859..e4e2d0bf 100644 --- a/plugs/query/query.ts +++ b/plugs/query/query.ts @@ -2,7 +2,7 @@ import type { WidgetContent } from "$sb/app_event.ts"; import { events, language, space, system } from "$sb/syscalls.ts"; import { parseTreeToAST } from "$sb/lib/tree.ts"; import { astToKvQuery } from "$sb/lib/parse-query.ts"; -import { jsonToMDTable, renderTemplate } from "../directive/util.ts"; +import { jsonToMDTable, renderQueryTemplate } from "../directive/util.ts"; import { loadPageObject, replaceTemplateVars } from "../template/template.ts"; export async function widget( @@ -44,7 +44,7 @@ export async function widget( } else { if (parsedQuery.render) { // Configured a custom rendering template, let's use it! - const rendered = await renderTemplate( + const rendered = await renderQueryTemplate( pageObject, parsedQuery.render, allResults, diff --git a/plugs/template/api.ts b/plugs/template/api.ts new file mode 100644 index 00000000..9443419e --- /dev/null +++ b/plugs/template/api.ts @@ -0,0 +1,51 @@ +import { handlebars, markdown, YAML } from "$sb/syscalls.ts"; +import type { PageMeta } from "$sb/types.ts"; +import { extractFrontmatter } from "$sb/lib/frontmatter.ts"; +import { TemplateObject } from "./types.ts"; +import { renderToText } from "$sb/lib/tree.ts"; + +/** + * Strips the template from its frontmatter and renders it. + * The assumption is that the frontmatter has already been parsed and should not appear in thhe rendered output. + * @param templateText the template text + * @param data data to be rendered by the template + * @param globals a set of global variables + * @returns + */ +export async function renderTemplate( + templateText: string, + pageMeta: PageMeta, + data: any = {}, +): Promise { + const tree = await markdown.parseMarkdown(templateText); + const frontmatter: Partial = await extractFrontmatter(tree, { + removeFrontmatterSection: true, + removeTags: ["template"], + }); + templateText = renderToText(tree).trimStart(); + // console.log(`Trimmed template: |${templateText}|`); + // If a 'frontmatter' key was specified in the frontmatter, use that as the frontmatter + if (frontmatter.frontmatter) { + if (typeof frontmatter.frontmatter === "string") { + templateText = "---\n" + frontmatter.frontmatter + "---\n" + templateText; + } else { + templateText = "---\n" + (await YAML.stringify(frontmatter.frontmatter)) + + "---\n" + templateText; + } + } + return handlebars.renderTemplate(templateText, data, { page: pageMeta }); +} + +/** + * Strips a template text from its frontmatter and #template tag + */ +export async function cleanTemplate( + templateText: string, +): Promise { + const tree = await markdown.parseMarkdown(templateText); + await extractFrontmatter(tree, { + removeFrontmatterSection: true, + removeTags: ["template"], + }); + return renderToText(tree).trimStart(); +} diff --git a/plugs/template/index.ts b/plugs/template/index.ts new file mode 100644 index 00000000..9881a398 --- /dev/null +++ b/plugs/template/index.ts @@ -0,0 +1,7 @@ +import type { IndexTreeEvent } from "$sb/app_event.ts"; +import { system } from "$sb/syscalls.ts"; + +export async function indexTemplate({ name, tree }: IndexTreeEvent) { + // Just delegate to the index plug + await system.invokeFunction("index.indexPage", { name, tree }); +} diff --git a/plugs/template/plug_api.ts b/plugs/template/plug_api.ts new file mode 100644 index 00000000..37a64675 --- /dev/null +++ b/plugs/template/plug_api.ts @@ -0,0 +1,24 @@ +import type { PageMeta } from "$sb/types.ts"; +import { system } from "../../plug-api/syscalls.ts"; + +export function renderTemplate( + templateText: string, + pageMeta: PageMeta, + data: any = {}, +): Promise { + return system.invokeFunction( + "template.renderTemplate", + templateText, + pageMeta, + data, + ); +} + +export function cleanTemplate( + templateText: string, +): Promise { + return system.invokeFunction( + "template.cleanTemplate", + templateText, + ); +} diff --git a/plugs/template/template.plug.yaml b/plugs/template/template.plug.yaml index ce72a20b..63ac2c1e 100644 --- a/plugs/template/template.plug.yaml +++ b/plugs/template/template.plug.yaml @@ -1,5 +1,19 @@ name: template functions: + # API + renderTemplate: + path: api.ts:renderTemplate + cleanTemplate: + path: api.ts:cleanTemplate + + insertTemplateText: + path: template.ts:insertTemplateText + + + indexTemplate: + path: ./index.ts:indexTemplate + events: + - page:indexTemplate templateSlashCommand: path: ./template.ts:templateSlashComplete @@ -10,8 +24,6 @@ functions: path: ./template.ts:insertSlashTemplate # Template commands - insertTemplateText: - path: "./template.ts:insertTemplateText" applyLineReplace: path: ./template.ts:applyLineReplace insertFrontMatter: diff --git a/plugs/template/template.ts b/plugs/template/template.ts index 71037463..a74a3811 100644 --- a/plugs/template/template.ts +++ b/plugs/template/template.ts @@ -4,20 +4,19 @@ import { renderToText } from "$sb/lib/tree.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 { ObjectValue, PageMeta } from "$sb/types.ts"; +import { PageMeta } from "$sb/types.ts"; import { CompleteEvent, SlashCompletion } from "$sb/app_event.ts"; import { getObjectByRef, queryObjects } from "../index/plug_api.ts"; - -export type TemplateObject = ObjectValue<{ - trigger?: string; // has to start with # for now - scope?: string; - frontmatter?: Record | string; -}>; +import { TemplateObject } from "./types.ts"; +import { renderTemplate } from "./api.ts"; export async function templateSlashComplete( completeEvent: CompleteEvent, ): Promise { - const allTemplates = await queryObjects("template", {}); + const allTemplates = await queryObjects("template", { + // Only return templates that have a trigger + filter: ["!=", ["attr", "trigger"], ["null"]], + }); return allTemplates.map((template) => ({ label: template.trigger!, detail: "template", @@ -31,14 +30,7 @@ export async function insertSlashTemplate(slashCompletion: SlashCompletion) { const pageObject = await loadPageObject(slashCompletion.pageName); let templateText = await space.readPage(slashCompletion.templatePage); - templateText = await replaceTemplateVars(templateText, pageObject); - const parseTree = await markdown.parseMarkdown(templateText); - const frontmatter = await extractFrontmatter(parseTree, [], true); - templateText = renderToText(parseTree).trim(); - if (frontmatter.frontmatter) { - templateText = "---\n" + (await YAML.stringify(frontmatter.frontmatter)) + - "---\n" + templateText; - } + templateText = await renderTemplate(templateText, pageObject); const cursorPos = await editor.getCursor(); const carretPos = templateText.indexOf("|^|"); @@ -76,10 +68,12 @@ export async function instantiateTemplateCommand() { ); const parseTree = await markdown.parseMarkdown(text); - const additionalPageMeta = await extractFrontmatter(parseTree, [ - "$name", - "$disableDirectives", - ]); + const additionalPageMeta = await extractFrontmatter(parseTree, { + removeKeys: [ + "$name", + "$disableDirectives", + ], + }); const tempPageMeta: PageMeta = { tags: ["page"], diff --git a/plugs/template/types.ts b/plugs/template/types.ts new file mode 100644 index 00000000..260a59a2 --- /dev/null +++ b/plugs/template/types.ts @@ -0,0 +1,10 @@ +import { ObjectValue } from "$sb/types.ts"; + +export type TemplateFrontmatter = { + trigger?: string; // slash command name + scope?: string; + // Frontmatter can be encoded as an object (in which case we'll serialize it) or as a string + frontmatter?: Record | string; +}; + +export type TemplateObject = ObjectValue; diff --git a/plugs/template/util.test.ts b/plugs/template/util.test.ts new file mode 100644 index 00000000..5f0723ea --- /dev/null +++ b/plugs/template/util.test.ts @@ -0,0 +1,76 @@ +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 new file mode 100644 index 00000000..da96808a --- /dev/null +++ b/plugs/template/util.ts @@ -0,0 +1,47 @@ +const frontMatterRegex = /^---\n(([^\n]|\n)*?)---\n/; +const yamlKvRegex = /^\s*(\w+):\s*(.*)/; +const yamlListItemRegex = /^\s*-\s+(.+)/; + +/** + * 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 lines = frontmatterText.split("\n"); + let inTagsSection = false; + for (const line of lines) { + const yamlKv = yamlKvRegex.exec(line); + if (yamlKv) { + const [key, value] = yamlKv.slice(1); + // Looking for a 'tags' key + if (key === "tags") { + inTagsSection = true; + // 'template' there? Yay! + if (value.split(/,\s*/).includes("template")) { + return true; + } + } else { + inTagsSection = false; + } + } + const yamlListem = yamlListItemRegex.exec(line); + if (yamlListem && inTagsSection) { + // List item is 'template'? Yay! + if (yamlListem[1] === "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/web/editor_ui.tsx b/web/editor_ui.tsx index fe3b5927..0c9bc1c7 100644 --- a/web/editor_ui.tsx +++ b/web/editor_ui.tsx @@ -25,15 +25,15 @@ export class MainUI { viewState: AppViewState = initialViewState; viewDispatch: (action: Action) => void = () => {}; - constructor(private editor: Client) { + constructor(private client: Client) { // Make keyboard shortcuts work even when the editor is in read only mode or not focused globalThis.addEventListener("keydown", (ev) => { - if (!editor.editorView.hasFocus) { + if (!client.editorView.hasFocus) { if ((ev.target as any).closest(".cm-editor")) { // In some cm element, let's back out return; } - if (runScopeHandlers(editor.editorView, ev, "editor")) { + if (runScopeHandlers(client.editorView, ev, "editor")) { ev.preventDefault(); } } @@ -52,7 +52,7 @@ export class MainUI { ev.preventDefault(); this.viewDispatch({ type: "show-palette", - context: editor.getContext(), + context: client.getContext(), }); } }); @@ -63,7 +63,7 @@ export class MainUI { this.viewState = viewState; this.viewDispatch = dispatch; - const editor = this.editor; + const editor = this.client; useEffect(() => { if (viewState.currentPage) { @@ -78,8 +78,8 @@ export class MainUI { }, [viewState.uiOptions.forcedROMode]); useEffect(() => { - this.editor.rebuildEditorState(); - this.editor.dispatchAppEvent("editor:modeswitch"); + this.client.rebuildEditorState(); + this.client.dispatchAppEvent("editor:modeswitch"); }, [viewState.uiOptions.vimMode]); useEffect(() => { @@ -208,24 +208,24 @@ export class MainUI { // If we support syncOnly, don't show this toggle button ? [{ icon: RefreshCwIcon, - description: this.editor.syncMode + description: this.client.syncMode ? "Currently in Sync mode, click to switch to Online mode" : "Currently in Online mode, click to switch to Sync mode", - class: this.editor.syncMode ? "sb-enabled" : undefined, + class: this.client.syncMode ? "sb-enabled" : undefined, callback: () => { (async () => { - const newValue = !this.editor.syncMode; + const newValue = !this.client.syncMode; if (newValue) { localStorage.setItem("syncMode", "true"); - this.editor.flashNotification( + this.client.flashNotification( "Now switching to sync mode, one moment please...", ); await sleep(1000); location.reload(); } else { localStorage.removeItem("syncMode"); - this.editor.flashNotification( + this.client.flashNotification( "Now switching to online mode, one moment please...", ); await sleep(1000); diff --git a/website/Templates.md b/website/Templates.md index bf485f5b..6ff87a4e 100644 --- a/website/Templates.md +++ b/website/Templates.md @@ -3,7 +3,7 @@ For various use cases, SilverBullet uses [Handlebars templates](https://handleba Generally templates are stored in your space as regular pages, which allows for reuse. Some examples include [[template/task]] and [[template/page]]. As a convention, we often name templates with a `template/` prefix, although this is purely a convention. -[[Live Templates]] allow templates to be define inline, for instance: +[[Live Templates]] allow templates to be defined inline, for instance: ```template template: | Hello, {{name}}! Today is _{{today}}_