Rebuilt frontmatter templates as template widgets
This commit is contained in:
parent
5b3dd500e4
commit
848e11a773
@ -36,6 +36,7 @@ export const builtinLanguages: Record<string, Language> = {
|
||||
"template": StreamLanguage.define(yamlLanguage),
|
||||
"embed": StreamLanguage.define(yamlLanguage),
|
||||
"data": StreamLanguage.define(yamlLanguage),
|
||||
"toc": StreamLanguage.define(yamlLanguage),
|
||||
"javascript": javascriptLanguage,
|
||||
"js": javascriptLanguage,
|
||||
"typescript": typescriptLanguage,
|
||||
|
@ -66,9 +66,6 @@ export class HttpSpacePrimitives implements SpacePrimitives {
|
||||
async fetchFileList(): Promise<FileMeta[]> {
|
||||
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) {
|
||||
|
@ -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,
|
||||
);
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ export function render(
|
||||
lang: string,
|
||||
body: string,
|
||||
pageName: string,
|
||||
): Promise<CodeWidgetContent> {
|
||||
): Promise<CodeWidgetContent | null> {
|
||||
return syscall("codeWidget.render", lang, body, pageName);
|
||||
}
|
||||
|
||||
|
@ -127,14 +127,13 @@ export type ObjectQuery = Omit<Query, "prefix">;
|
||||
export type CodeWidgetCallback = (
|
||||
bodyText: string,
|
||||
pageName: string,
|
||||
) => Promise<CodeWidgetContent>;
|
||||
) => Promise<CodeWidgetContent | null>;
|
||||
|
||||
export type CodeWidgetContent = {
|
||||
html?: string;
|
||||
markdown?: string;
|
||||
script?: string;
|
||||
buttons?: CodeWidgetButton[];
|
||||
banner?: string;
|
||||
};
|
||||
|
||||
export type CodeWidgetButton = {
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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,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,
|
||||
|
@ -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
|
||||
|
||||
|
@ -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);
|
||||
}
|
@ -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",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -29,7 +29,3 @@ functions:
|
||||
path: "./preview.ts:previewClickHandler"
|
||||
events:
|
||||
- preview:click
|
||||
|
||||
markdownWidget:
|
||||
path: ./widget.ts:markdownWidget
|
||||
codeWidget: markdown
|
||||
|
@ -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"});
|
||||
});`,
|
||||
});
|
||||
}
|
@ -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[]> {
|
||||
|
@ -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 }
|
||||
|
@ -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,
|
||||
);
|
||||
});
|
@ -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(),
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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");
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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) =>
|
||||
`<button data-button="${idx}" title="${button.description}">${button.svg}</button> `
|
||||
).join("")
|
||||
}</div>${
|
||||
banner ? `<div class="sb-banner">${escapeHtml(banner)}</div>` : ""
|
||||
}${html}`;
|
||||
}</div>${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, "<").replace(
|
||||
/>/g,
|
||||
">",
|
||||
);
|
||||
}
|
||||
|
@ -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",
|
||||
),
|
||||
|
@ -104,7 +104,7 @@ export function mountIFrame(
|
||||
preloadedIFrame: PreloadedIFrame,
|
||||
client: Client,
|
||||
widgetHeightCacheKey: string | null,
|
||||
content: WidgetContent | Promise<WidgetContent>,
|
||||
content: WidgetContent | null | Promise<WidgetContent | null>,
|
||||
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<WidgetContent>,
|
||||
content: WidgetContent | null | Promise<WidgetContent | null>,
|
||||
onMessage?: (message: any) => void,
|
||||
) {
|
||||
// console.log("Claiming iframe");
|
||||
|
@ -48,7 +48,7 @@ export class PanelWidgetHook implements Hook<PanelWidgetT> {
|
||||
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'.`,
|
||||
);
|
||||
|
@ -429,8 +429,6 @@
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.sb-markdown-widget,
|
||||
.sb-markdown-top-widget:has(*),
|
||||
.sb-markdown-bottom-widget:has(*) {
|
||||
|
@ -11,7 +11,7 @@ export function codeWidgetSyscalls(
|
||||
lang: string,
|
||||
body: string,
|
||||
pageName: string,
|
||||
): Promise<CodeWidgetContent> => {
|
||||
): Promise<CodeWidgetContent | null> => {
|
||||
const langCallback = codeWidgetHook.codeWidgetCallbacks.get(
|
||||
lang,
|
||||
);
|
||||
|
@ -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.
|
||||
|
||||
|
@ -1 +1,3 @@
|
||||
Linked mentions
|
||||
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]].
|
||||
|
@ -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]].
|
||||
|
||||
|
34
website/Live Template Widgets.md
Normal file
34
website/Live Template Widgets.md
Normal file
@ -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]].
|
||||
|
||||
|
@ -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: |
|
||||
|
@ -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**
|
||||
```
|
@ -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`.
|
||||
|
@ -1,6 +1,4 @@
|
||||
---
|
||||
tags: plug
|
||||
---
|
||||
#plug
|
||||
|
||||
The `editor` plug implements foundational editor functionality for SilverBullet.
|
||||
|
||||
|
@ -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
|
||||
|
@ -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]}.
|
||||
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
|
||||
```
|
||||
|
@ -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]]
|
||||
```
|
||||
|
@ -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.
|
||||
|
@ -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}}
|
19
website/template/widget/linked-mentions.md
Normal file
19
website/template/widget/linked-mentions.md
Normal file
@ -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}}"
|
||||
```
|
9
website/template/widget/toc.md
Normal file
9
website/template/widget/toc.md
Normal file
@ -0,0 +1,9 @@
|
||||
---
|
||||
description: Adds a Table of Contents to all pages
|
||||
tags: template
|
||||
type: widget:top
|
||||
where: 'true'
|
||||
---
|
||||
```toc
|
||||
minHeaders: 3
|
||||
```
|
Loading…
Reference in New Issue
Block a user