Rebuilt frontmatter templates as template widgets

This commit is contained in:
Zef Hemel
2024-01-08 17:08:35 +01:00
parent 5b3dd500e4
commit 848e11a773
45 changed files with 471 additions and 483 deletions
+6 -1
View File
@@ -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");
}
+2 -1
View File
@@ -76,7 +76,8 @@ export const builtins: Record<string, Record<string, string>> = {
pos: "!number",
type: "string",
trigger: "string",
forTags: "string[]",
where: "string",
priority: "number",
},
};
+2 -1
View File
@@ -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,
+13 -30
View File
@@ -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
-61
View File
@@ -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<CodeWidgetContent | null> {
if (await clientStore.get(hideMentionsKey)) {
return null;
}
const page = await editor.getCurrentPage();
const linksResult = await queryObjects<LinkObject>("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:
`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-refresh-cw"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>`,
invokeFunction: "index.refreshWidgets",
},
{
description: "Hide",
svg:
`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-eye-off"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>`,
invokeFunction: "index.toggleMentions",
},
],
};
}
}
@@ -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<TemplateObject>(
"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<
`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-refresh-cw"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>`,
invokeFunction: "index.refreshWidgets",
},
{
description: "Edit",
svg:
`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-edit"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></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);
}
+30 -29
View File
@@ -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<CodeWidgetContent | null> {
if (await clientStore.get(hideTOCKey)) {
return null;
export async function widget(
bodyText: string,
): Promise<CodeWidgetContent | null> {
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<CodeWidgetContent | null> {
}
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<CodeWidgetContent | null> {
return {
markdown: renderedMd,
buttons: [
{
description: "Edit",
svg:
`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-edit"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>`,
invokeFunction: "query.editButton",
},
{
description: "Reload",
svg:
`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-refresh-cw"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>`,
invokeFunction: "index.refreshWidgets",
},
{
description: "Hide",
svg:
`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-eye-off"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>`,
invokeFunction: "index.toggleTOC",
},
],
};
}
+6 -1
View File
@@ -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(
-4
View File
@@ -29,7 +29,3 @@ functions:
path: "./preview.ts:previewClickHandler"
events:
- preview:click
markdownWidget:
path: ./widget.ts:markdownWidget
codeWidget: markdown
-20
View File
@@ -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<WidgetContent> {
const mdTree = await markdown.parseMarkdown(bodyText);
const html = renderMarkdownToHtml(mdTree, {
smartHardBreak: true,
});
return Promise.resolve({
html: html,
script: `
document.addEventListener("click", () => {
api({type: "blur"});
});`,
});
}
+44 -40
View File
@@ -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<CodeWidgetContent> {
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<LintDiagnostic[]> {
+20 -5
View File
@@ -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 }
-76
View File
@@ -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,
);
});
-26
View File
@@ -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(),