Awesome frontmatter (#617)

Live Frontmatter Templates
This commit is contained in:
Zef Hemel
2024-01-04 20:08:12 +01:00
committed by GitHub
parent 9040993232
commit 91027af5fe
53 changed files with 646 additions and 342 deletions
+5 -4
View File
@@ -11,13 +11,13 @@ export const builtins: Record<string, Record<string, string>> = {
ref: "!string",
name: "!string",
displayName: "string",
aliases: "array",
aliases: "string[]",
created: "!date",
lastModified: "!date",
perm: "!rw|ro",
contentType: "!string",
size: "!number",
tags: "array",
tags: "string[]",
},
task: {
ref: "!string",
@@ -27,11 +27,11 @@ export const builtins: Record<string, Record<string, string>> = {
state: "!string",
deadline: "string",
pos: "!number",
tags: "array",
tags: "string[]",
},
taskstate: {
ref: "!string",
tags: "!array",
tags: "!string[]",
state: "!string",
count: "!number",
page: "!string",
@@ -76,6 +76,7 @@ export const builtins: Record<string, Record<string, string>> = {
pos: "!number",
type: "string",
trigger: "string",
forTags: "string[]",
},
};
+82
View File
@@ -0,0 +1,82 @@
import { CodeWidgetContent } from "$sb/types.ts";
import { editor, language, markdown, space } from "$sb/syscalls.ts";
import { extractFrontmatter } from "$sb/lib/frontmatter.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";
// 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 renderFrontmatterWidget(): Promise<
CodeWidgetContent | null
> {
const text = await editor.getText();
const pageMeta = await loadPageObject(await editor.getCurrentPage());
const parsedMd = await markdown.parseMarkdown(text);
const frontmatter = await extractFrontmatter(parsedMd);
const allFrontMatterTemplates = await queryObjects<TemplateObject>(
"template",
{
filter: ["=", ["attr", "type"], ["string", "frontmatter"]],
orderBy: [{ expr: ["attr", "priority"], desc: false }],
},
);
let templateText = fallbackTemplate;
// 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(
await language.parseLanguage("expression", template.where!),
);
const parsedExpression = expressionToKvQueryExpression(exprAST[1]);
if (evalQueryExpression(parsedExpression, pageMeta)) {
// Match! We're happy
templateText = await space.readPage(template.ref);
break;
}
}
const summaryText = await renderTemplate(
templateText,
pageMeta,
frontmatter,
);
// console.log("Rendered", summaryText);
return {
markdown: summaryText.text,
banner: "frontmatter",
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: "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);
}
+10 -1
View File
@@ -186,4 +186,13 @@ functions:
lintYAML:
path: lint.ts:lintYAML
events:
- editor:lint
- editor:lint
renderFrontmatterWidget:
path: frontmatter.ts:renderFrontmatterWidget
env: client
panelWidget: frontmatter
editFrontmatter:
path: frontmatter.ts:editFrontmatter
+1 -1
View File
@@ -48,7 +48,7 @@ export async function renderTOC(): Promise<CodeWidgetContent | null> {
return false;
});
if (headers.length < headerThreshold) {
console.log("Not enough headers, not showing TOC", headers.length);
// Not enough headers, not showing TOC
return null;
}
// console.log("Headers", headers);
+14 -1
View File
@@ -267,7 +267,10 @@ function render(
}
case "Hashtag":
return {
name: "strong",
name: "span",
attrs: {
class: "hashtag",
},
body: t.children![0].text!,
};
@@ -414,6 +417,16 @@ function render(
case "Entity":
return t.children![0].text!;
case "TemplateDirective": {
return {
name: "span",
attrs: {
class: "template-directive",
},
body: renderToText(t),
};
}
// Text
case undefined:
return t.text!;
+9 -20
View File
@@ -1,11 +1,7 @@
import type { LintEvent } from "$sb/app_event.ts";
import { events, language, space } from "$sb/syscalls.ts";
import {
findNodeOfType,
parseTreeToAST,
traverseTreeAsync,
} from "$sb/lib/tree.ts";
import { astToKvQuery } from "$sb/lib/parse-query.ts";
import { events, space } from "$sb/syscalls.ts";
import { findNodeOfType, traverseTreeAsync } from "$sb/lib/tree.ts";
import { parseQuery } from "$sb/lib/parse-query.ts";
import { loadPageObject, replaceTemplateVars } from "../template/template.ts";
import { cleanPageRef, resolvePath } from "$sb/lib/resolve.ts";
import { CodeWidgetContent, LintDiagnostic } from "$sb/types.ts";
@@ -18,13 +14,9 @@ export async function widget(
const pageObject = await loadPageObject(pageName);
try {
const queryAST = parseTreeToAST(
await language.parseLanguage(
"query",
await replaceTemplateVars(bodyText, pageObject),
),
const parsedQuery = await parseQuery(
await replaceTemplateVars(bodyText, pageObject),
);
const parsedQuery = astToKvQuery(queryAST[1]);
if (!parsedQuery.limit) {
parsedQuery.limit = ["number", 1000];
@@ -115,13 +107,10 @@ export async function lintQuery(
}
const bodyText = codeText.children![0].text!;
try {
const queryAST = parseTreeToAST(
await language.parseLanguage(
"query",
await replaceTemplateVars(bodyText, pageObject),
),
const parsedQuery = await parseQuery(
await replaceTemplateVars(bodyText, pageObject),
);
const parsedQuery = astToKvQuery(queryAST[1]);
const allSources = await allQuerySources();
if (
parsedQuery.querySource &&
@@ -141,7 +130,7 @@ export async function lintQuery(
);
try {
await space.getPageMeta(templatePage);
} catch (e: any) {
} catch {
diagnostics.push({
from: codeText.from!,
to: codeText.to!,
+7 -1
View File
@@ -1,11 +1,17 @@
import { ObjectValue } from "$sb/types.ts";
export type TemplateFrontmatter = {
trigger?: string; // slash command name
displayName?: string;
type?: "page";
// Frontmatter can be encoded as an object (in which case we'll serialize it) or as a string
frontmatter?: Record<string, any> | string;
// Specific for slash templates
trigger?: string;
// Specific for frontmatter templates
where?: string; // expression (SB query style)
priority?: number; // When multiple templates match, the one with the highest priority is used
};
export type TemplateObject = ObjectValue<TemplateFrontmatter>;
+10 -3
View File
@@ -36,20 +36,27 @@ export function buildHandebarOptions(pageMeta: PageMeta) {
};
}
export function defaultJsonTransformer(_k: string, v: any) {
export function defaultJsonTransformer(v: any): string {
if (v === undefined) {
return "";
}
if (typeof v === "string") {
return v.replaceAll("\n", " ").replaceAll("|", "\\|");
}
if (Array.isArray(v)) {
return v.map(defaultJsonTransformer).join(", ");
} else if (typeof v === "object") {
return Object.entries(v).map(([k, v]: [string, any]) =>
`${k}: ${defaultJsonTransformer(v)}`
).join(", ");
}
return "" + v;
}
// Nicely format an array of JSON objects as a Markdown table
export function jsonToMDTable(
jsonArray: any[],
valueTransformer: (k: string, v: any) => string = defaultJsonTransformer,
valueTransformer: (v: any) => string = defaultJsonTransformer,
): string {
const headers = new Set<string>();
for (const entry of jsonArray) {
@@ -79,7 +86,7 @@ export function jsonToMDTable(
for (const val of jsonArray) {
const el = [];
for (const prop of headerList) {
const s = valueTransformer(prop, val[prop]);
const s = valueTransformer(val[prop]);
el.push(s);
}
lines.push("|" + el.join("|") + "|");