Work on #587: revamped templates
This commit is contained in:
parent
c38e6cfc25
commit
70ef6ed9da
@ -64,41 +64,6 @@ export async function queryComplete(completeEvent: CompleteEvent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function templateVariableComplete(completeEvent: CompleteEvent) {
|
||||
const match = /\{\{([\w@]*)$/.exec(completeEvent.linePrefix);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handlebarOptions = buildHandebarOptions({ name: "" } as PageMeta);
|
||||
let allCompletions: any[] = Object.keys(handlebarOptions.helpers).map(
|
||||
(name) => ({ label: name, detail: "helper" }),
|
||||
);
|
||||
allCompletions = allCompletions.concat(
|
||||
Object.keys(handlebarOptions.data).map((key) => ({
|
||||
label: `@${key}`,
|
||||
detail: "global variable",
|
||||
})),
|
||||
);
|
||||
|
||||
const completions = (await events.dispatchEvent(
|
||||
`attribute:complete:_`,
|
||||
{
|
||||
source: "",
|
||||
prefix: match[1],
|
||||
} as AttributeCompleteEvent,
|
||||
)).flat() as AttributeCompletion[];
|
||||
|
||||
allCompletions = allCompletions.concat(
|
||||
attributeCompletionsToCMCompletion(completions),
|
||||
);
|
||||
|
||||
return {
|
||||
from: completeEvent.pos - match[1].length,
|
||||
options: allCompletions,
|
||||
};
|
||||
}
|
||||
|
||||
export function attributeCompletionsToCMCompletion(
|
||||
completions: AttributeCompletion[],
|
||||
) {
|
||||
|
@ -23,11 +23,6 @@ functions:
|
||||
path: ./complete.ts:queryComplete
|
||||
events:
|
||||
- editor:complete
|
||||
handlebarHelperComplete:
|
||||
path: ./complete.ts:templateVariableComplete
|
||||
events:
|
||||
- editor:complete
|
||||
|
||||
# Conversion
|
||||
convertToLiveQuery:
|
||||
path: command.ts:convertToLive
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { CompleteEvent } from "$sb/app_event.ts";
|
||||
import { space } from "$sb/syscalls.ts";
|
||||
import { FileMeta, PageMeta } from "$sb/types.ts";
|
||||
import { cacheFileListing } from "../federation/federation.ts";
|
||||
import { queryObjects } from "../index/plug_api.ts";
|
||||
|
||||
// Completion
|
||||
export async function pageComplete(completeEvent: CompleteEvent) {
|
||||
@ -9,7 +9,16 @@ export async function pageComplete(completeEvent: CompleteEvent) {
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
let allPages: PageMeta[] = await space.listPages();
|
||||
// When we're in fenced code block, we likely want to complete a page name without an alias, and only complete template pages
|
||||
// so let's check if we're in a template context
|
||||
const isInTemplateContext =
|
||||
completeEvent.parentNodes.find((node) => node.startsWith("FencedCode")) &&
|
||||
// either a render [[bla]] clause or page: "[[bla]]" template block
|
||||
/render\s+\[\[|page:\s*["']\[\[/.test(
|
||||
completeEvent.linePrefix,
|
||||
);
|
||||
const tagToQuery = isInTemplateContext ? "template" : "page";
|
||||
let allPages: PageMeta[] = await queryObjects<PageMeta>(tagToQuery, {});
|
||||
const prefix = match[1];
|
||||
if (prefix.startsWith("!")) {
|
||||
// Federation prefix, let's first see if we're matching anything from federation that is locally synced
|
||||
@ -34,12 +43,38 @@ export async function pageComplete(completeEvent: CompleteEvent) {
|
||||
return {
|
||||
from: completeEvent.pos - match[1].length,
|
||||
options: allPages.map((pageMeta) => {
|
||||
return {
|
||||
const completions: any[] = [];
|
||||
if (pageMeta.displayName) {
|
||||
completions.push({
|
||||
label: pageMeta.displayName,
|
||||
boost: pageMeta.lastModified,
|
||||
apply: isInTemplateContext
|
||||
? pageMeta.name
|
||||
: `${pageMeta.name}|${pageMeta.displayName}`,
|
||||
detail: "alias",
|
||||
type: "page",
|
||||
});
|
||||
}
|
||||
if (Array.isArray(pageMeta.aliases)) {
|
||||
for (const alias of pageMeta.aliases) {
|
||||
completions.push({
|
||||
label: alias,
|
||||
boost: pageMeta.lastModified,
|
||||
apply: isInTemplateContext
|
||||
? pageMeta.name
|
||||
: `${pageMeta.name}|${alias}`,
|
||||
detail: "alias",
|
||||
type: "page",
|
||||
});
|
||||
}
|
||||
}
|
||||
completions.push({
|
||||
label: pageMeta.name,
|
||||
boost: pageMeta.lastModified,
|
||||
type: "page",
|
||||
};
|
||||
}),
|
||||
});
|
||||
return completions;
|
||||
}).flat(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -45,14 +45,3 @@ export async function copyPage() {
|
||||
console.log("Navigating to new page");
|
||||
await editor.navigate(newName);
|
||||
}
|
||||
|
||||
export async function newPageCommand() {
|
||||
const allPages = await space.listPages();
|
||||
let pageName = `Untitled`;
|
||||
let i = 1;
|
||||
while (allPages.find((p) => p.name === pageName)) {
|
||||
pageName = `Untitled ${i}`;
|
||||
i++;
|
||||
}
|
||||
await editor.navigate(pageName);
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ export async function cacheFileListing(uri: string): Promise<FileMeta[]> {
|
||||
const r = await nativeFetch(indexUrl, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"X-Sync-Mode": "true",
|
||||
"Cache-Control": "no-cache",
|
||||
},
|
||||
signal: fetchController.signal,
|
||||
@ -119,7 +119,13 @@ export async function readFile(
|
||||
name: string,
|
||||
): Promise<{ data: Uint8Array; meta: FileMeta } | undefined> {
|
||||
const url = federatedPathToUrl(name);
|
||||
const r = await nativeFetch(url);
|
||||
console.log("Fetfching fedderated file", url);
|
||||
const r = await nativeFetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Sync-Mode": "true",
|
||||
},
|
||||
});
|
||||
if (r.status === 503) {
|
||||
throw new Error("Offline");
|
||||
}
|
||||
@ -195,6 +201,7 @@ export async function getFileMeta(name: string): Promise<FileMeta> {
|
||||
const r = await nativeFetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Sync-Mode": "true",
|
||||
"X-Get-Meta": "true",
|
||||
},
|
||||
});
|
||||
|
@ -72,6 +72,7 @@ export async function indexObjects<T>(
|
||||
const allAttributes = new Map<string, string>(); // tag:name -> attributeType
|
||||
for (const obj of objects) {
|
||||
for (const tag of obj.tags) {
|
||||
// The object itself
|
||||
kvs.push({
|
||||
key: [tag, cleanKey(obj.ref, page)],
|
||||
value: obj,
|
||||
@ -79,8 +80,8 @@ export async function indexObjects<T>(
|
||||
// Index attributes
|
||||
const builtinAttributes = builtins[tag];
|
||||
if (!builtinAttributes) {
|
||||
// For non-builtin tags, index all attributes
|
||||
for (
|
||||
// This is not a builtin tag, so we index all attributes (almost, see below)
|
||||
attributeLabel: for (
|
||||
const [attrName, attrValue] of Object.entries(
|
||||
obj as Record<string, any>,
|
||||
)
|
||||
@ -88,6 +89,18 @@ export async function indexObjects<T>(
|
||||
if (attrName.startsWith("$")) {
|
||||
continue;
|
||||
}
|
||||
// Check for all tags attached to this object if they're builtins
|
||||
// If so: if `attrName` is defined in the builtin, use the attributeType from there (mostly to preserve readOnly aspects)
|
||||
for (const otherTag of obj.tags) {
|
||||
const builtinAttributes = builtins[otherTag];
|
||||
if (builtinAttributes && builtinAttributes[attrName]) {
|
||||
allAttributes.set(
|
||||
`${tag}:${attrName}`,
|
||||
builtinAttributes[attrName],
|
||||
);
|
||||
continue attributeLabel;
|
||||
}
|
||||
}
|
||||
allAttributes.set(`${tag}:${attrName}`, determineType(attrValue));
|
||||
}
|
||||
} else if (tag !== "attribute") {
|
||||
@ -112,12 +125,16 @@ export async function indexObjects<T>(
|
||||
page,
|
||||
[...allAttributes].map(([key, value]) => {
|
||||
const [tag, name] = key.split(":");
|
||||
const attributeType = value.startsWith("!")
|
||||
? value.substring(1)
|
||||
: value;
|
||||
return {
|
||||
ref: key,
|
||||
tags: ["attribute"],
|
||||
tag,
|
||||
name,
|
||||
attributeType: value,
|
||||
attributeType,
|
||||
readOnly: value.startsWith("!"),
|
||||
page,
|
||||
};
|
||||
}),
|
||||
|
@ -1,14 +1,15 @@
|
||||
import type { CompleteEvent } from "$sb/app_event.ts";
|
||||
import { events } from "$sb/syscalls.ts";
|
||||
import { getObjectByRef, queryObjects } from "./api.ts";
|
||||
import { queryObjects } from "./api.ts";
|
||||
import { ObjectValue, QueryExpression } from "$sb/types.ts";
|
||||
import { builtinPseudoPage } from "./builtins.ts";
|
||||
import { determineTags } from "./cheap_yaml.ts";
|
||||
|
||||
export type AttributeObject = ObjectValue<{
|
||||
name: string;
|
||||
attributeType: string;
|
||||
tag: string;
|
||||
page: string;
|
||||
readOnly: boolean;
|
||||
}>;
|
||||
|
||||
export type AttributeCompleteEvent = {
|
||||
@ -20,7 +21,7 @@ export type AttributeCompletion = {
|
||||
name: string;
|
||||
source: string;
|
||||
attributeType: string;
|
||||
builtin?: boolean;
|
||||
readOnly: boolean;
|
||||
};
|
||||
|
||||
export function determineType(v: any): string {
|
||||
@ -33,31 +34,53 @@ export function determineType(v: any): string {
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered by the `attribute:complete:*` event (that is: gimme all attribute completions)
|
||||
* @param attributeCompleteEvent
|
||||
* @returns
|
||||
*/
|
||||
export async function objectAttributeCompleter(
|
||||
attributeCompleteEvent: AttributeCompleteEvent,
|
||||
): Promise<AttributeCompletion[]> {
|
||||
const prefixFilter: QueryExpression = ["call", "startsWith", [[
|
||||
"attr",
|
||||
"name",
|
||||
], ["string", attributeCompleteEvent.prefix]]];
|
||||
const attributeFilter: QueryExpression | undefined =
|
||||
attributeCompleteEvent.source === ""
|
||||
? undefined
|
||||
: ["=", ["attr", "tag"], ["string", attributeCompleteEvent.source]];
|
||||
? prefixFilter
|
||||
: ["and", prefixFilter, ["=", ["attr", "tag"], [
|
||||
"string",
|
||||
attributeCompleteEvent.source,
|
||||
]]];
|
||||
const allAttributes = await queryObjects<AttributeObject>("attribute", {
|
||||
filter: attributeFilter,
|
||||
distinct: true,
|
||||
select: [{ name: "name" }, { name: "attributeType" }, { name: "tag" }, {
|
||||
name: "readOnly",
|
||||
}],
|
||||
});
|
||||
return allAttributes.map((value) => {
|
||||
return {
|
||||
name: value.name,
|
||||
source: value.tag,
|
||||
attributeType: value.attributeType,
|
||||
builtin: value.page === builtinPseudoPage,
|
||||
readOnly: value.readOnly,
|
||||
} as AttributeCompletion;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Offer completions for _setting_ attributes on objects (either in frontmatter or inline)
|
||||
* Triggered by `editor:complete` events from the editor
|
||||
*/
|
||||
export async function attributeComplete(completeEvent: CompleteEvent) {
|
||||
if (/([\-\*]\s+\[)([^\]]+)$/.test(completeEvent.linePrefix)) {
|
||||
// Don't match task states, which look similar
|
||||
return null;
|
||||
}
|
||||
|
||||
// Inline attribute completion (e.g. [myAttr: 10])
|
||||
const inlineAttributeMatch = /([^\[\{}]|^)\[(\w+)$/.exec(
|
||||
completeEvent.linePrefix,
|
||||
);
|
||||
@ -79,22 +102,24 @@ export async function attributeComplete(completeEvent: CompleteEvent) {
|
||||
return {
|
||||
from: completeEvent.pos - inlineAttributeMatch[2].length,
|
||||
options: attributeCompletionsToCMCompletion(
|
||||
completions.filter((completion) => !completion.builtin),
|
||||
// Filter out read-only attributes
|
||||
completions.filter((completion) => !completion.readOnly),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// Frontmatter attribute completion
|
||||
const attributeMatch = /^(\w+)$/.exec(completeEvent.linePrefix);
|
||||
if (attributeMatch) {
|
||||
if (completeEvent.parentNodes.includes("FrontMatter")) {
|
||||
const pageMeta = await getObjectByRef(
|
||||
completeEvent.pageName,
|
||||
const frontmatterParent = completeEvent.parentNodes.find((node) =>
|
||||
node.startsWith("FrontMatter:")
|
||||
);
|
||||
if (frontmatterParent) {
|
||||
const tags = [
|
||||
"page",
|
||||
completeEvent.pageName,
|
||||
);
|
||||
let tags = ["page"];
|
||||
if (pageMeta?.tags) {
|
||||
tags = pageMeta.tags;
|
||||
}
|
||||
...determineTags(frontmatterParent.slice("FrontMatter:".length)),
|
||||
];
|
||||
|
||||
const completions = (await Promise.all(tags.map((tag) =>
|
||||
events.dispatchEvent(
|
||||
`attribute:complete:${tag}`,
|
||||
@ -109,7 +134,7 @@ export async function attributeComplete(completeEvent: CompleteEvent) {
|
||||
from: completeEvent.pos - attributeMatch[1].length,
|
||||
options: attributeCompletionsToCMCompletion(
|
||||
completions.filter((completion) =>
|
||||
!completion.builtin
|
||||
!completion.readOnly
|
||||
),
|
||||
),
|
||||
};
|
||||
|
@ -5,70 +5,76 @@ import { TagObject } from "./tags.ts";
|
||||
|
||||
export const builtinPseudoPage = ":builtin:";
|
||||
|
||||
// Types marked with a ! are read-only, they cannot be set by the user
|
||||
export const builtins: Record<string, Record<string, string>> = {
|
||||
page: {
|
||||
ref: "string",
|
||||
name: "string",
|
||||
lastModified: "date",
|
||||
perm: "rw|ro",
|
||||
contentType: "string",
|
||||
size: "number",
|
||||
ref: "!string",
|
||||
name: "!string",
|
||||
displayName: "string",
|
||||
aliases: "array",
|
||||
created: "!date",
|
||||
lastModified: "!date",
|
||||
perm: "!rw|ro",
|
||||
contentType: "!string",
|
||||
size: "!number",
|
||||
tags: "array",
|
||||
},
|
||||
task: {
|
||||
ref: "string",
|
||||
name: "string",
|
||||
done: "boolean",
|
||||
page: "string",
|
||||
state: "string",
|
||||
ref: "!string",
|
||||
name: "!string",
|
||||
done: "!boolean",
|
||||
page: "!string",
|
||||
state: "!string",
|
||||
deadline: "string",
|
||||
pos: "number",
|
||||
pos: "!number",
|
||||
tags: "array",
|
||||
},
|
||||
taskstate: {
|
||||
ref: "string",
|
||||
tags: "array",
|
||||
state: "string",
|
||||
count: "number",
|
||||
page: "string",
|
||||
ref: "!string",
|
||||
tags: "!array",
|
||||
state: "!string",
|
||||
count: "!number",
|
||||
page: "!string",
|
||||
},
|
||||
tag: {
|
||||
ref: "string",
|
||||
name: "string",
|
||||
page: "string",
|
||||
context: "string",
|
||||
ref: "!string",
|
||||
name: "!string",
|
||||
page: "!string",
|
||||
context: "!string",
|
||||
},
|
||||
attribute: {
|
||||
ref: "string",
|
||||
name: "string",
|
||||
attributeType: "string",
|
||||
type: "string",
|
||||
page: "string",
|
||||
ref: "!string",
|
||||
name: "!string",
|
||||
attributeType: "!string",
|
||||
type: "!string",
|
||||
page: "!string",
|
||||
},
|
||||
anchor: {
|
||||
ref: "string",
|
||||
name: "string",
|
||||
page: "string",
|
||||
pos: "number",
|
||||
ref: "!string",
|
||||
name: "!string",
|
||||
page: "!string",
|
||||
pos: "!number",
|
||||
},
|
||||
link: {
|
||||
ref: "string",
|
||||
name: "string",
|
||||
page: "string",
|
||||
pos: "number",
|
||||
alias: "string",
|
||||
inDirective: "boolean",
|
||||
asTemplate: "boolean",
|
||||
ref: "!string",
|
||||
name: "!string",
|
||||
page: "!string",
|
||||
pos: "!number",
|
||||
alias: "!string",
|
||||
inDirective: "!boolean",
|
||||
asTemplate: "!boolean",
|
||||
},
|
||||
paragraph: {
|
||||
text: "string",
|
||||
page: "string",
|
||||
pos: "number",
|
||||
text: "!string",
|
||||
page: "!string",
|
||||
pos: "!number",
|
||||
},
|
||||
template: {
|
||||
ref: "string",
|
||||
page: "string",
|
||||
pos: "number",
|
||||
ref: "!string",
|
||||
page: "!string",
|
||||
pageName: "string",
|
||||
pos: "!number",
|
||||
type: "string",
|
||||
trigger: "string",
|
||||
},
|
||||
};
|
||||
@ -92,8 +98,10 @@ export async function loadBuiltinsIntoIndex() {
|
||||
tags: ["attribute"],
|
||||
tag,
|
||||
name,
|
||||
attributeType,
|
||||
builtinPseudoPage,
|
||||
attributeType: attributeType.startsWith("!")
|
||||
? attributeType.substring(1)
|
||||
: attributeType,
|
||||
readOnly: attributeType.startsWith("!"),
|
||||
page: builtinPseudoPage,
|
||||
};
|
||||
}),
|
||||
|
10
plugs/index/cheap_yaml.test.ts
Normal file
10
plugs/index/cheap_yaml.test.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { assertEquals } from "../../test_deps.ts";
|
||||
import { determineTags } from "./cheap_yaml.ts";
|
||||
|
||||
Deno.test("cheap yaml", () => {
|
||||
assertEquals([], determineTags(""));
|
||||
assertEquals([], determineTags("hank: bla"));
|
||||
assertEquals(["template"], determineTags("tags: template"));
|
||||
assertEquals(["bla", "template"], determineTags("tags: bla,template"));
|
||||
assertEquals(["bla", "template"], determineTags("tags:\n- bla\n- template"));
|
||||
});
|
34
plugs/index/cheap_yaml.ts
Normal file
34
plugs/index/cheap_yaml.ts
Normal file
@ -0,0 +1,34 @@
|
||||
const yamlKvRegex = /^\s*(\w+):\s*(.*)/;
|
||||
const yamlListItemRegex = /^\s*-\s+(.+)/;
|
||||
|
||||
/**
|
||||
* Cheap YAML parser to determine tags (ugly, regex based but fast)
|
||||
* @param yamlText
|
||||
* @returns
|
||||
*/
|
||||
export function determineTags(yamlText: string): string[] {
|
||||
const lines = yamlText.split("\n");
|
||||
let inTagsSection = false;
|
||||
const tags: string[] = [];
|
||||
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) {
|
||||
tags.push(...value.split(/,\s*/));
|
||||
}
|
||||
} else {
|
||||
inTagsSection = false;
|
||||
}
|
||||
}
|
||||
const yamlListem = yamlListItemRegex.exec(line);
|
||||
if (yamlListem && inTagsSection) {
|
||||
tags.push(yamlListem[1]);
|
||||
}
|
||||
}
|
||||
return tags;
|
||||
}
|
@ -1,19 +1,38 @@
|
||||
import { YAML } from "$sb/syscalls.ts";
|
||||
import { LintDiagnostic } from "$sb/types.ts";
|
||||
import { LintDiagnostic, QueryExpression } from "$sb/types.ts";
|
||||
import {
|
||||
findNodeOfType,
|
||||
renderToText,
|
||||
traverseTreeAsync,
|
||||
} from "$sb/lib/tree.ts";
|
||||
import { LintEvent } from "$sb/app_event.ts";
|
||||
import { queryObjects } from "./api.ts";
|
||||
import { AttributeObject } from "./attributes.ts";
|
||||
import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
|
||||
|
||||
export async function lintYAML({ tree }: LintEvent): Promise<LintDiagnostic[]> {
|
||||
const diagnostics: LintDiagnostic[] = [];
|
||||
const frontmatter = await extractFrontmatter(tree);
|
||||
// Query all readOnly attributes for pages with this tag set
|
||||
const readOnlyAttributes = await queryObjects<AttributeObject>("attribute", {
|
||||
filter: ["and", ["=", ["attr", "tag"], [
|
||||
"array",
|
||||
frontmatter.tags.map((tag): QueryExpression => ["string", tag]),
|
||||
]], [
|
||||
"=",
|
||||
["attr", "readOnly"],
|
||||
["boolean", true],
|
||||
]],
|
||||
distinct: true,
|
||||
select: [{ name: "name" }],
|
||||
});
|
||||
// console.log("All read only attributes", readOnlyAttributes);
|
||||
await traverseTreeAsync(tree, async (node) => {
|
||||
if (node.type === "FrontMatterCode") {
|
||||
const lintResult = await lintYaml(
|
||||
renderToText(node),
|
||||
node.from!,
|
||||
readOnlyAttributes.map((a) => a.name),
|
||||
);
|
||||
if (lintResult) {
|
||||
diagnostics.push(lintResult);
|
||||
@ -56,9 +75,20 @@ const errorRegex = /\((\d+):(\d+)\)/;
|
||||
async function lintYaml(
|
||||
yamlText: string,
|
||||
from: number,
|
||||
disallowedKeys: string[] = [],
|
||||
): Promise<LintDiagnostic | undefined> {
|
||||
try {
|
||||
await YAML.parse(yamlText);
|
||||
const parsed = await YAML.parse(yamlText);
|
||||
for (const key of disallowedKeys) {
|
||||
if (parsed[key]) {
|
||||
return {
|
||||
from,
|
||||
to: from + yamlText.length,
|
||||
severity: "error",
|
||||
message: `Disallowed key "${key}"`,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMatch = errorRegex.exec(e.message);
|
||||
if (errorMatch) {
|
||||
|
@ -26,10 +26,10 @@ export async function indexPage({ name, tree }: IndexTreeEvent) {
|
||||
|
||||
pageMeta.tags = [...new Set(["page", ...pageMeta.tags || []])];
|
||||
|
||||
if (pageMeta.tags.includes("template")) {
|
||||
// If this is a template, we don't want to index it as a page or anything else, just a template
|
||||
pageMeta.tags = ["template"];
|
||||
}
|
||||
// if (pageMeta.tags.includes("template")) {
|
||||
// // If this is a template, we don't want to index it as a page or anything else, just a template
|
||||
// pageMeta.tags = ["template"];
|
||||
// }
|
||||
|
||||
// console.log("Page object", pageObj);
|
||||
await indexObjects<PageMeta>(name, [pageMeta]);
|
||||
|
@ -59,7 +59,7 @@ export async function renderTOC(reload = false) {
|
||||
}
|
||||
cachedTOC = JSON.stringify(headers);
|
||||
if (headers.length < headerThreshold) {
|
||||
console.log("Not enough headers, not showing TOC", headers.length);
|
||||
// console.log("Not enough headers, not showing TOC", headers.length);
|
||||
await editor.hidePanel("top");
|
||||
return;
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ export async function renderTemplate(
|
||||
templateText: string,
|
||||
pageMeta: PageMeta,
|
||||
data: any = {},
|
||||
): Promise<{ frontmatter?: string; text: string }> {
|
||||
): Promise<{ renderedFrontmatter?: string; frontmatter: any; text: string }> {
|
||||
const tree = await markdown.parseMarkdown(templateText);
|
||||
const frontmatter: Partial<TemplateObject> = await extractFrontmatter(tree, {
|
||||
removeFrontmatterSection: true,
|
||||
@ -36,7 +36,8 @@ export async function renderTemplate(
|
||||
});
|
||||
}
|
||||
return {
|
||||
frontmatter: frontmatterText,
|
||||
frontmatter,
|
||||
renderedFrontmatter: frontmatterText,
|
||||
text: await handlebars.renderTemplate(templateText, data, {
|
||||
page: pageMeta,
|
||||
}),
|
||||
|
111
plugs/template/complete.ts
Normal file
111
plugs/template/complete.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { CompleteEvent, SlashCompletion } from "$sb/app_event.ts";
|
||||
import { PageMeta } from "$sb/types.ts";
|
||||
import { editor, events, markdown, space } from "$sb/syscalls.ts";
|
||||
import { buildHandebarOptions } from "../directive/util.ts";
|
||||
import type {
|
||||
AttributeCompleteEvent,
|
||||
AttributeCompletion,
|
||||
} from "../index/attributes.ts";
|
||||
import { queryObjects } from "../index/plug_api.ts";
|
||||
import { TemplateObject } from "./types.ts";
|
||||
import { loadPageObject } from "./template.ts";
|
||||
import { renderTemplate } from "./api.ts";
|
||||
import { prepareFrontmatterDispatch } from "$sb/lib/frontmatter.ts";
|
||||
|
||||
export async function templateVariableComplete(completeEvent: CompleteEvent) {
|
||||
const match = /\{\{([\w@]*)$/.exec(completeEvent.linePrefix);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handlebarOptions = buildHandebarOptions({ name: "" } as PageMeta);
|
||||
let allCompletions: any[] = Object.keys(handlebarOptions.helpers).map(
|
||||
(name) => ({ label: name, detail: "helper" }),
|
||||
);
|
||||
allCompletions = allCompletions.concat(
|
||||
Object.keys(handlebarOptions.data).map((key) => ({
|
||||
label: `@${key}`,
|
||||
detail: "global variable",
|
||||
})),
|
||||
);
|
||||
|
||||
const completions = (await events.dispatchEvent(
|
||||
`attribute:complete:_`,
|
||||
{
|
||||
source: "",
|
||||
prefix: match[1],
|
||||
} as AttributeCompleteEvent,
|
||||
)).flat() as AttributeCompletion[];
|
||||
|
||||
allCompletions = allCompletions.concat(
|
||||
attributeCompletionsToCMCompletion(completions),
|
||||
);
|
||||
|
||||
return {
|
||||
from: completeEvent.pos - match[1].length,
|
||||
options: allCompletions,
|
||||
};
|
||||
}
|
||||
|
||||
export async function templateSlashComplete(
|
||||
completeEvent: CompleteEvent,
|
||||
): Promise<SlashCompletion[]> {
|
||||
const allTemplates = await queryObjects<TemplateObject>("template", {
|
||||
// Only return templates that have a trigger
|
||||
filter: ["!=", ["attr", "trigger"], ["null"]],
|
||||
});
|
||||
return allTemplates.map((template) => ({
|
||||
label: template.trigger!,
|
||||
detail: "template",
|
||||
templatePage: template.ref,
|
||||
pageName: completeEvent.pageName,
|
||||
invoke: "template.insertSlashTemplate",
|
||||
}));
|
||||
}
|
||||
|
||||
export async function insertSlashTemplate(slashCompletion: SlashCompletion) {
|
||||
const pageObject = await loadPageObject(slashCompletion.pageName);
|
||||
|
||||
const templateText = await space.readPage(slashCompletion.templatePage);
|
||||
let { renderedFrontmatter, text } = await renderTemplate(
|
||||
templateText,
|
||||
pageObject,
|
||||
);
|
||||
|
||||
let cursorPos = await editor.getCursor();
|
||||
|
||||
if (renderedFrontmatter) {
|
||||
renderedFrontmatter = renderedFrontmatter.trim();
|
||||
const pageText = await editor.getText();
|
||||
const tree = await markdown.parseMarkdown(pageText);
|
||||
|
||||
const dispatch = await prepareFrontmatterDispatch(
|
||||
tree,
|
||||
renderedFrontmatter,
|
||||
);
|
||||
if (cursorPos === 0) {
|
||||
dispatch.selection = { anchor: renderedFrontmatter.length + 9 };
|
||||
}
|
||||
await editor.dispatch(dispatch);
|
||||
}
|
||||
|
||||
cursorPos = await editor.getCursor();
|
||||
const carretPos = text.indexOf("|^|");
|
||||
text = text.replace("|^|", "");
|
||||
await editor.insertAtCursor(text);
|
||||
if (carretPos !== -1) {
|
||||
await editor.moveCursor(cursorPos + carretPos);
|
||||
}
|
||||
}
|
||||
|
||||
export function attributeCompletionsToCMCompletion(
|
||||
completions: AttributeCompletion[],
|
||||
) {
|
||||
return completions.map(
|
||||
(completion) => ({
|
||||
label: completion.name,
|
||||
detail: `${completion.attributeType} (${completion.source})`,
|
||||
type: "attribute",
|
||||
}),
|
||||
);
|
||||
}
|
@ -6,24 +6,30 @@ functions:
|
||||
cleanTemplate:
|
||||
path: api.ts:cleanTemplate
|
||||
|
||||
# Used by various slash commands
|
||||
insertTemplateText:
|
||||
path: template.ts:insertTemplateText
|
||||
|
||||
|
||||
indexTemplate:
|
||||
path: ./index.ts:indexTemplate
|
||||
events:
|
||||
# Special event only triggered for template pages
|
||||
- page:indexTemplate
|
||||
|
||||
# Completion
|
||||
templateSlashCommand:
|
||||
path: ./template.ts:templateSlashComplete
|
||||
path: ./complete.ts:templateSlashComplete
|
||||
events:
|
||||
- slash:complete
|
||||
|
||||
insertSlashTemplate:
|
||||
path: ./template.ts:insertSlashTemplate
|
||||
path: ./complete.ts:insertSlashTemplate
|
||||
|
||||
handlebarHelperComplete:
|
||||
path: ./complete.ts:templateVariableComplete
|
||||
events:
|
||||
- editor:complete
|
||||
|
||||
# Template commands
|
||||
applyLineReplace:
|
||||
path: ./template.ts:applyLineReplace
|
||||
insertFrontMatter:
|
||||
@ -79,6 +85,7 @@ functions:
|
||||
name: hr
|
||||
description: Insert a horizontal rule
|
||||
value: "---"
|
||||
|
||||
insertTable:
|
||||
redirect: insertTemplateText
|
||||
slashCommand:
|
||||
@ -89,44 +96,38 @@ functions:
|
||||
| Header A | Header B |
|
||||
|----------|----------|
|
||||
| Cell A|^| | Cell B |
|
||||
|
||||
quickNoteCommand:
|
||||
path: ./template.ts:quickNoteCommand
|
||||
command:
|
||||
name: "Quick Note"
|
||||
key: "Alt-Shift-n"
|
||||
|
||||
dailyNoteCommand:
|
||||
path: ./template.ts:dailyNoteCommand
|
||||
command:
|
||||
name: "Open Daily Note"
|
||||
key: "Alt-Shift-d"
|
||||
|
||||
weeklyNoteCommand:
|
||||
path: ./template.ts:weeklyNoteCommand
|
||||
command:
|
||||
name: "Open Weekly Note"
|
||||
key: "Alt-Shift-w"
|
||||
|
||||
instantiateTemplateCommand:
|
||||
path: ./template.ts:instantiateTemplateCommand
|
||||
newPageCommand:
|
||||
path: ./template.ts:newPageCommand
|
||||
command:
|
||||
name: "Template: Instantiate Page"
|
||||
insertSnippet:
|
||||
path: ./template.ts:insertSnippet
|
||||
command:
|
||||
name: "Template: Insert Snippet"
|
||||
slashCommand:
|
||||
name: snippet
|
||||
description: Insert a snippet
|
||||
applyPageTemplateCommand:
|
||||
path: ./template.ts:applyPageTemplateCommand
|
||||
slashCommand:
|
||||
name: page-template
|
||||
description: Apply a page template
|
||||
name: "Page: From Template"
|
||||
key: "Alt-Shift-t"
|
||||
|
||||
insertTodayCommand:
|
||||
path: "./template.ts:insertTemplateText"
|
||||
slashCommand:
|
||||
name: today
|
||||
description: Insert today's date
|
||||
value: "{{today}}"
|
||||
|
||||
insertTomorrowCommand:
|
||||
path: "./template.ts:insertTemplateText"
|
||||
slashCommand:
|
||||
|
@ -1,96 +1,40 @@
|
||||
import { editor, handlebars, markdown, space, YAML } from "$sb/syscalls.ts";
|
||||
import {
|
||||
extractFrontmatter,
|
||||
prepareFrontmatterDispatch,
|
||||
} from "$sb/lib/frontmatter.ts";
|
||||
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 { PageMeta } from "$sb/types.ts";
|
||||
import { CompleteEvent, SlashCompletion } from "$sb/app_event.ts";
|
||||
import { getObjectByRef, queryObjects } from "../index/plug_api.ts";
|
||||
import { TemplateObject } from "./types.ts";
|
||||
import { renderTemplate } from "./api.ts";
|
||||
|
||||
export async function templateSlashComplete(
|
||||
completeEvent: CompleteEvent,
|
||||
): Promise<SlashCompletion[]> {
|
||||
const allTemplates = await queryObjects<TemplateObject>("template", {
|
||||
// Only return templates that have a trigger
|
||||
filter: ["!=", ["attr", "trigger"], ["null"]],
|
||||
});
|
||||
return allTemplates.map((template) => ({
|
||||
label: template.trigger!,
|
||||
detail: "template",
|
||||
templatePage: template.ref,
|
||||
pageName: completeEvent.pageName,
|
||||
invoke: "template.insertSlashTemplate",
|
||||
}));
|
||||
}
|
||||
export async function newPageCommand(
|
||||
_cmdDef: any,
|
||||
templateName?: string,
|
||||
askName = true,
|
||||
) {
|
||||
if (!templateName) {
|
||||
const allPageTemplates = await queryObjects<TemplateObject>("template", {
|
||||
// Only return templates that have a trigger
|
||||
filter: ["=", ["attr", "type"], ["string", "page"]],
|
||||
});
|
||||
const selectedTemplate = await editor.filterBox(
|
||||
"Page template",
|
||||
allPageTemplates
|
||||
.map((pageMeta) => ({
|
||||
...pageMeta,
|
||||
name: pageMeta.displayName || pageMeta.ref,
|
||||
})),
|
||||
`Select the template to create a new page from (listing any page tagged with <tt>#template</tt> and 'page' set as 'type')`,
|
||||
);
|
||||
|
||||
export async function insertSlashTemplate(slashCompletion: SlashCompletion) {
|
||||
const pageObject = await loadPageObject(slashCompletion.pageName);
|
||||
|
||||
const templateText = await space.readPage(slashCompletion.templatePage);
|
||||
let { frontmatter, text } = await renderTemplate(templateText, pageObject);
|
||||
|
||||
let cursorPos = await editor.getCursor();
|
||||
|
||||
if (frontmatter) {
|
||||
frontmatter = frontmatter.trim();
|
||||
const pageText = await editor.getText();
|
||||
const tree = await markdown.parseMarkdown(pageText);
|
||||
|
||||
const dispatch = await prepareFrontmatterDispatch(tree, frontmatter);
|
||||
if (cursorPos === 0) {
|
||||
dispatch.selection = { anchor: frontmatter.length + 9 };
|
||||
if (!selectedTemplate) {
|
||||
return;
|
||||
}
|
||||
await editor.dispatch(dispatch);
|
||||
templateName = selectedTemplate.ref;
|
||||
}
|
||||
console.log("Selected template", templateName);
|
||||
|
||||
cursorPos = await editor.getCursor();
|
||||
const carretPos = text.indexOf("|^|");
|
||||
text = text.replace("|^|", "");
|
||||
await editor.insertAtCursor(text);
|
||||
if (carretPos !== -1) {
|
||||
await editor.moveCursor(cursorPos + carretPos);
|
||||
}
|
||||
}
|
||||
|
||||
export async function instantiateTemplateCommand() {
|
||||
const allPages = await space.listPages();
|
||||
const { pageTemplatePrefix } = await readSettings({
|
||||
pageTemplatePrefix: "template/page/",
|
||||
});
|
||||
|
||||
const selectedTemplate = await editor.filterBox(
|
||||
"Template",
|
||||
allPages
|
||||
.filter((pageMeta) => pageMeta.name.startsWith(pageTemplatePrefix))
|
||||
.map((pageMeta) => ({
|
||||
...pageMeta,
|
||||
name: pageMeta.name.slice(pageTemplatePrefix.length),
|
||||
})),
|
||||
`Select the template to create a new page from (listing any page starting with <tt>${pageTemplatePrefix}</tt>)`,
|
||||
);
|
||||
|
||||
if (!selectedTemplate) {
|
||||
return;
|
||||
}
|
||||
console.log("Selected template", selectedTemplate);
|
||||
|
||||
const text = await space.readPage(
|
||||
`${pageTemplatePrefix}${selectedTemplate.name}`,
|
||||
);
|
||||
|
||||
const parseTree = await markdown.parseMarkdown(text);
|
||||
const additionalPageMeta = await extractFrontmatter(parseTree, {
|
||||
removeKeys: [
|
||||
"$name",
|
||||
"$disableDirectives",
|
||||
],
|
||||
});
|
||||
const templateText = await space.readPage(templateName!);
|
||||
|
||||
const tempPageMeta: PageMeta = {
|
||||
tags: ["page"],
|
||||
@ -100,20 +44,25 @@ export async function instantiateTemplateCommand() {
|
||||
lastModified: "",
|
||||
perm: "rw",
|
||||
};
|
||||
|
||||
if (additionalPageMeta.$name) {
|
||||
additionalPageMeta.$name = await replaceTemplateVars(
|
||||
additionalPageMeta.$name,
|
||||
tempPageMeta,
|
||||
);
|
||||
}
|
||||
|
||||
const pageName = await editor.prompt(
|
||||
"Name of new page",
|
||||
additionalPageMeta.$name,
|
||||
// Just used to extract the frontmatter
|
||||
const { frontmatter } = await renderTemplate(
|
||||
templateText,
|
||||
tempPageMeta,
|
||||
);
|
||||
if (!pageName) {
|
||||
return;
|
||||
|
||||
let pageName: string | undefined = await replaceTemplateVars(
|
||||
frontmatter?.pageName || "",
|
||||
tempPageMeta,
|
||||
);
|
||||
|
||||
if (askName) {
|
||||
pageName = await editor.prompt(
|
||||
"Name of new page",
|
||||
await replaceTemplateVars(frontmatter?.pageName || "", tempPageMeta),
|
||||
);
|
||||
if (!pageName) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
tempPageMeta.name = pageName;
|
||||
|
||||
@ -127,92 +76,27 @@ export async function instantiateTemplateCommand() {
|
||||
`Page ${pageName} already exists, are you sure you want to override it?`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
// Just navigate there without instantiating
|
||||
return editor.navigate(pageName);
|
||||
}
|
||||
} catch {
|
||||
// The preferred scenario, let's keep going
|
||||
}
|
||||
|
||||
const pageText = await replaceTemplateVars(
|
||||
renderToText(parseTree),
|
||||
const { text: pageText, renderedFrontmatter } = await renderTemplate(
|
||||
templateText,
|
||||
tempPageMeta,
|
||||
);
|
||||
await space.writePage(pageName, pageText);
|
||||
await editor.navigate(pageName);
|
||||
}
|
||||
|
||||
export async function insertSnippet() {
|
||||
const allPages = await space.listPages();
|
||||
const { snippetPrefix } = await readSettings({
|
||||
snippetPrefix: "snippet/",
|
||||
});
|
||||
const cursorPos = await editor.getCursor();
|
||||
const page = await editor.getCurrentPage();
|
||||
const pageMeta = await space.getPageMeta(page);
|
||||
const allSnippets = allPages
|
||||
.filter((pageMeta) => pageMeta.name.startsWith(snippetPrefix))
|
||||
.map((pageMeta) => ({
|
||||
...pageMeta,
|
||||
name: pageMeta.name.slice(snippetPrefix.length),
|
||||
}));
|
||||
|
||||
const selectedSnippet = await editor.filterBox(
|
||||
"Snippet",
|
||||
allSnippets,
|
||||
`Select the snippet to insert (listing any page starting with <tt>${snippetPrefix}</tt>)`,
|
||||
let fullPageText = renderedFrontmatter
|
||||
? "---\n" + renderedFrontmatter + "---\n" + pageText
|
||||
: pageText;
|
||||
const carretPos = fullPageText.indexOf("|^|");
|
||||
fullPageText = fullPageText.replace("|^|", "");
|
||||
await space.writePage(
|
||||
pageName,
|
||||
fullPageText,
|
||||
);
|
||||
|
||||
if (!selectedSnippet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = await space.readPage(`${snippetPrefix}${selectedSnippet.name}`);
|
||||
let templateText = await replaceTemplateVars(text, pageMeta);
|
||||
const carretPos = templateText.indexOf("|^|");
|
||||
templateText = templateText.replace("|^|", "");
|
||||
templateText = await replaceTemplateVars(templateText, pageMeta);
|
||||
await editor.insertAtCursor(templateText);
|
||||
if (carretPos !== -1) {
|
||||
await editor.moveCursor(cursorPos + carretPos);
|
||||
}
|
||||
}
|
||||
|
||||
export async function applyPageTemplateCommand() {
|
||||
const allPages = await space.listPages();
|
||||
const { pageTemplatePrefix } = await readSettings({
|
||||
pageTemplatePrefix: "template/page/",
|
||||
});
|
||||
const cursorPos = await editor.getCursor();
|
||||
const page = await editor.getCurrentPage();
|
||||
const pageMeta = await space.getPageMeta(page);
|
||||
const allSnippets = allPages
|
||||
.filter((pageMeta) => pageMeta.name.startsWith(pageTemplatePrefix))
|
||||
.map((pageMeta) => ({
|
||||
...pageMeta,
|
||||
name: pageMeta.name.slice(pageTemplatePrefix.length),
|
||||
}));
|
||||
|
||||
const selectedPage = await editor.filterBox(
|
||||
"Page template",
|
||||
allSnippets,
|
||||
`Select the page template to apply (listing any page starting with <tt>${pageTemplatePrefix}</tt>)`,
|
||||
);
|
||||
|
||||
if (!selectedPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = await space.readPage(
|
||||
`${pageTemplatePrefix}${selectedPage.name}`,
|
||||
);
|
||||
let templateText = await replaceTemplateVars(text, pageMeta);
|
||||
const carretPos = templateText.indexOf("|^|");
|
||||
templateText = templateText.replace("|^|", "");
|
||||
templateText = await replaceTemplateVars(templateText, pageMeta);
|
||||
await editor.insertAtCursor(templateText);
|
||||
if (carretPos !== -1) {
|
||||
await editor.moveCursor(cursorPos + carretPos);
|
||||
}
|
||||
await editor.navigate(pageName, carretPos !== -1 ? carretPos : undefined);
|
||||
}
|
||||
|
||||
export async function loadPageObject(pageName?: string): Promise<PageMeta> {
|
||||
|
@ -2,7 +2,8 @@ import { ObjectValue } from "$sb/types.ts";
|
||||
|
||||
export type TemplateFrontmatter = {
|
||||
trigger?: string; // slash command name
|
||||
scope?: string;
|
||||
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;
|
||||
};
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
SyntaxNode,
|
||||
syntaxTree,
|
||||
} from "../common/deps.ts";
|
||||
import { fileMetaToPageMeta, Space } from "./space.ts";
|
||||
import { Space } from "./space.ts";
|
||||
import { FilterOption } from "./types.ts";
|
||||
import { ensureSettingsAndIndex } from "../common/util.ts";
|
||||
import { EventHook } from "../plugos/hooks/event.ts";
|
||||
@ -44,7 +44,6 @@ import { IndexedDBKvPrimitives } from "../plugos/lib/indexeddb_kv_primitives.ts"
|
||||
import { DataStoreMQ } from "../plugos/lib/mq.datastore.ts";
|
||||
import { DataStoreSpacePrimitives } from "../common/spaces/datastore_space_primitives.ts";
|
||||
import {
|
||||
encryptedFileExt,
|
||||
EncryptedSpacePrimitives,
|
||||
} from "../common/spaces/encrypted_space_primitives.ts";
|
||||
const frontMatterRegex = /^---\n(([^\n]|\n)*?)---\n/;
|
||||
@ -63,7 +62,6 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Oh my god, need to refactor this
|
||||
export class Client {
|
||||
system!: ClientSystem;
|
||||
editorView!: EditorView;
|
||||
@ -501,14 +499,14 @@ export class Client {
|
||||
},
|
||||
);
|
||||
|
||||
this.eventHook.addLocalListener("file:listed", (fileList: FileMeta[]) => {
|
||||
this.ui.viewDispatch({
|
||||
type: "pages-listed",
|
||||
pages: fileList.filter(this.space.isListedPage).map(
|
||||
fileMetaToPageMeta,
|
||||
),
|
||||
});
|
||||
});
|
||||
// this.eventHook.addLocalListener("file:listed", (fileList: FileMeta[]) => {
|
||||
// this.ui.viewDispatch({
|
||||
// type: "update-all-pages",
|
||||
// pages: fileList.filter(this.space.isListedPage).map(
|
||||
// fileMetaToPageMeta,
|
||||
// ),
|
||||
// });
|
||||
// });
|
||||
|
||||
this.space.watch();
|
||||
|
||||
@ -593,6 +591,13 @@ export class Client {
|
||||
);
|
||||
}
|
||||
|
||||
async startPageNavigate() {
|
||||
// Fetch all pages from the index
|
||||
const pages = await this.system.queryObjects<PageMeta>("page", {});
|
||||
// Then show the page navigator
|
||||
this.ui.viewDispatch({ type: "start-navigate", pages });
|
||||
}
|
||||
|
||||
private progressTimeout?: number;
|
||||
showProgress(progressPerc: number) {
|
||||
this.ui.viewDispatch({
|
||||
@ -719,9 +724,9 @@ export class Client {
|
||||
if (currentNode) {
|
||||
let node: SyntaxNode | null = currentNode;
|
||||
do {
|
||||
if (node.name === "FencedCode") {
|
||||
if (node.name === "FencedCode" || node.name === "FrontMatter") {
|
||||
const body = editorState.sliceDoc(node.from + 3, node.to - 3);
|
||||
parentNodes.push(`FencedCode:${body}`);
|
||||
parentNodes.push(`${node.name}:${body}`);
|
||||
} else {
|
||||
parentNodes.push(node.name);
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ import { codeWidgetSyscalls } from "./syscalls/code_widget.ts";
|
||||
import { clientCodeWidgetSyscalls } from "./syscalls/client_code_widget.ts";
|
||||
import { KVPrimitivesManifestCache } from "../plugos/manifest_cache.ts";
|
||||
import { deepObjectMerge } from "$sb/lib/json.ts";
|
||||
import { Query } from "$sb/types.ts";
|
||||
|
||||
const plugNameExtractRegex = /\/(.+)\.plug\.js$/;
|
||||
|
||||
@ -238,6 +239,14 @@ export class ClientSystem {
|
||||
}
|
||||
|
||||
localSyscall(name: string, args: any[]) {
|
||||
return this.system.localSyscall("[local]", name, args);
|
||||
return this.system.localSyscall("editor", name, args);
|
||||
}
|
||||
|
||||
queryObjects<T>(tag: string, query: Query): Promise<T[]> {
|
||||
return this.system.localSyscall(
|
||||
"index",
|
||||
"system.invokeFunction",
|
||||
["queryObjects", tag, query],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -186,15 +186,11 @@ export function FilterList({
|
||||
<Icon width={16} height={16} />
|
||||
</span>
|
||||
)}
|
||||
<span className="sb-name" // dangerouslySetInnerHTML={{
|
||||
// __html: option?.result?.indexes
|
||||
// ? fuzzysort.highlight(option.result, "<b>", "</b>")!
|
||||
// : escapeHtml(option.name),
|
||||
// }}
|
||||
>
|
||||
<span className="sb-name">
|
||||
{option.name}
|
||||
</span>
|
||||
{option.hint && <span className="sb-hint">{option.hint}</span>}
|
||||
<div className="sb-description">{option.description}</div>
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
|
@ -14,7 +14,12 @@ export const fuzzySearchAndSort = (
|
||||
return arr.sort((a, b) => (a.orderId || 0) - (b.orderId || 0));
|
||||
}
|
||||
const enrichedArr: FuseOption[] = arr.map((item) => {
|
||||
return { ...item, baseName: item.name.split("/").pop()! };
|
||||
return {
|
||||
...item,
|
||||
baseName: item.name.split("/").pop()!,
|
||||
tags: item.tags?.join(" "),
|
||||
aliases: item.aliases?.join(" "),
|
||||
};
|
||||
});
|
||||
const fuse = new Fuse(enrichedArr, {
|
||||
keys: [{
|
||||
@ -23,13 +28,21 @@ export const fuzzySearchAndSort = (
|
||||
}, {
|
||||
name: "baseName",
|
||||
weight: 0.7,
|
||||
}, {
|
||||
name: "displayName",
|
||||
weight: 0.3,
|
||||
}, {
|
||||
name: "tags",
|
||||
weight: 0.1,
|
||||
}, {
|
||||
name: "aliases",
|
||||
weight: 0.7,
|
||||
}],
|
||||
includeScore: true,
|
||||
shouldSort: true,
|
||||
isCaseSensitive: false,
|
||||
threshold: 0.6,
|
||||
sortFn: (a, b): number => {
|
||||
// console.log(a, b);
|
||||
if (a.score === b.score) {
|
||||
const aOrder = enrichedArr[a.idx].orderId || 0;
|
||||
const bOrder = enrichedArr[b.idx].orderId || 0;
|
||||
|
@ -36,8 +36,26 @@ export function PageNavigator({
|
||||
if (isFederationPath(pageMeta.name)) {
|
||||
orderId = Math.round(orderId / 10); // Just 10x lower the timestamp to push them down, should work
|
||||
}
|
||||
let description: string | undefined;
|
||||
let aliases: string[] = [];
|
||||
if (pageMeta.displayName) {
|
||||
aliases.push(pageMeta.displayName);
|
||||
}
|
||||
if (Array.isArray(pageMeta.aliases)) {
|
||||
aliases = aliases.concat(pageMeta.aliases);
|
||||
}
|
||||
if (aliases.length > 0) {
|
||||
description = "(a.k.a. " + aliases.join(", ") + ") ";
|
||||
}
|
||||
if (pageMeta.tags.length > 1) {
|
||||
// Every page has the "page" tag, so it only gets interesting beyond that
|
||||
const interestingTags = pageMeta.tags.filter((tag) => tag !== "page");
|
||||
description = (description || "") +
|
||||
interestingTags.map((tag) => `#${tag}`).join(" ");
|
||||
}
|
||||
options.push({
|
||||
...pageMeta,
|
||||
description,
|
||||
orderId: orderId,
|
||||
});
|
||||
}
|
||||
|
@ -178,9 +178,7 @@ export function createEditorState(
|
||||
key: "Ctrl-k",
|
||||
mac: "Cmd-k",
|
||||
run: (): boolean => {
|
||||
client.ui.viewDispatch({ type: "start-navigate" });
|
||||
client.space.updatePageList();
|
||||
|
||||
client.startPageNavigate().catch(console.error);
|
||||
return true;
|
||||
},
|
||||
},
|
||||
|
@ -44,7 +44,7 @@ export class MainUI {
|
||||
if (ev.touches.length === 2) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.viewDispatch({ type: "start-navigate" });
|
||||
client.startPageNavigate().catch(console.error);
|
||||
}
|
||||
// Launch the command palette using a three-finger tap
|
||||
if (ev.touches.length === 3) {
|
||||
@ -63,7 +63,7 @@ export class MainUI {
|
||||
this.viewState = viewState;
|
||||
this.viewDispatch = dispatch;
|
||||
|
||||
const editor = this.client;
|
||||
const client = this.client;
|
||||
|
||||
useEffect(() => {
|
||||
if (viewState.currentPage) {
|
||||
@ -72,8 +72,8 @@ export class MainUI {
|
||||
}, [viewState.currentPage]);
|
||||
|
||||
useEffect(() => {
|
||||
editor.tweakEditorDOM(
|
||||
editor.editorView.contentDOM,
|
||||
client.tweakEditorDOM(
|
||||
client.editorView.contentDOM,
|
||||
);
|
||||
}, [viewState.uiOptions.forcedROMode]);
|
||||
|
||||
@ -98,18 +98,18 @@ export class MainUI {
|
||||
{viewState.showPageNavigator && (
|
||||
<PageNavigator
|
||||
allPages={viewState.allPages}
|
||||
currentPage={editor.currentPage}
|
||||
completer={editor.miniEditorComplete.bind(editor)}
|
||||
currentPage={client.currentPage}
|
||||
completer={client.miniEditorComplete.bind(client)}
|
||||
vimMode={viewState.uiOptions.vimMode}
|
||||
darkMode={viewState.uiOptions.darkMode}
|
||||
onNavigate={(page) => {
|
||||
dispatch({ type: "stop-navigate" });
|
||||
setTimeout(() => {
|
||||
editor.focus();
|
||||
client.focus();
|
||||
});
|
||||
if (page) {
|
||||
safeRun(async () => {
|
||||
await editor.navigate(page);
|
||||
await client.navigate(page);
|
||||
});
|
||||
}
|
||||
}}
|
||||
@ -120,7 +120,7 @@ export class MainUI {
|
||||
onTrigger={(cmd) => {
|
||||
dispatch({ type: "hide-palette" });
|
||||
setTimeout(() => {
|
||||
editor.focus();
|
||||
client.focus();
|
||||
});
|
||||
if (cmd) {
|
||||
dispatch({ type: "command-run", command: cmd.command.name });
|
||||
@ -131,14 +131,14 @@ export class MainUI {
|
||||
})
|
||||
.then(() => {
|
||||
// Always be focusing the editor after running a command
|
||||
editor.focus();
|
||||
client.focus();
|
||||
});
|
||||
}
|
||||
}}
|
||||
commands={editor.getCommandsByContext(viewState)}
|
||||
commands={client.getCommandsByContext(viewState)}
|
||||
vimMode={viewState.uiOptions.vimMode}
|
||||
darkMode={viewState.uiOptions.darkMode}
|
||||
completer={editor.miniEditorComplete.bind(editor)}
|
||||
completer={client.miniEditorComplete.bind(client)}
|
||||
recentCommands={viewState.recentCommands}
|
||||
/>
|
||||
)}
|
||||
@ -150,7 +150,7 @@ export class MainUI {
|
||||
vimMode={viewState.uiOptions.vimMode}
|
||||
darkMode={viewState.uiOptions.darkMode}
|
||||
allowNew={false}
|
||||
completer={editor.miniEditorComplete.bind(editor)}
|
||||
completer={client.miniEditorComplete.bind(client)}
|
||||
helpText={viewState.filterBoxHelpText}
|
||||
onSelect={viewState.filterBoxOnSelect}
|
||||
/>
|
||||
@ -161,7 +161,7 @@ export class MainUI {
|
||||
defaultValue={viewState.promptDefaultValue}
|
||||
vimMode={viewState.uiOptions.vimMode}
|
||||
darkMode={viewState.uiOptions.darkMode}
|
||||
completer={editor.miniEditorComplete.bind(editor)}
|
||||
completer={client.miniEditorComplete.bind(client)}
|
||||
callback={(value) => {
|
||||
dispatch({ type: "hide-prompt" });
|
||||
viewState.promptCallback!(value);
|
||||
@ -186,25 +186,25 @@ export class MainUI {
|
||||
vimMode={viewState.uiOptions.vimMode}
|
||||
darkMode={viewState.uiOptions.darkMode}
|
||||
progressPerc={viewState.progressPerc}
|
||||
completer={editor.miniEditorComplete.bind(editor)}
|
||||
completer={client.miniEditorComplete.bind(client)}
|
||||
onClick={() => {
|
||||
editor.editorView.scrollDOM.scrollTop = 0;
|
||||
client.editorView.scrollDOM.scrollTop = 0;
|
||||
}}
|
||||
onRename={async (newName) => {
|
||||
if (!newName) {
|
||||
// Always move cursor to the start of the page
|
||||
editor.editorView.dispatch({
|
||||
client.editorView.dispatch({
|
||||
selection: { anchor: 0 },
|
||||
});
|
||||
editor.focus();
|
||||
client.focus();
|
||||
return;
|
||||
}
|
||||
console.log("Now renaming page to...", newName);
|
||||
await editor.system.system.loadedPlugs.get("index")!.invoke(
|
||||
await client.system.system.loadedPlugs.get("index")!.invoke(
|
||||
"renamePageCommand",
|
||||
[{ page: newName }],
|
||||
);
|
||||
editor.focus();
|
||||
client.focus();
|
||||
}}
|
||||
actionButtons={[
|
||||
...!window.silverBulletConfig.syncOnly
|
||||
@ -242,7 +242,7 @@ export class MainUI {
|
||||
icon: HomeIcon,
|
||||
description: `Go to the index page (Alt-h)`,
|
||||
callback: () => {
|
||||
editor.navigate("");
|
||||
client.navigate("");
|
||||
},
|
||||
href: "",
|
||||
},
|
||||
@ -250,8 +250,7 @@ export class MainUI {
|
||||
icon: BookIcon,
|
||||
description: `Open page (${isMacLike() ? "Cmd-k" : "Ctrl-k"})`,
|
||||
callback: () => {
|
||||
dispatch({ type: "start-navigate" });
|
||||
editor.space.updatePageList();
|
||||
client.startPageNavigate().catch(console.error);
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -260,7 +259,7 @@ export class MainUI {
|
||||
callback: () => {
|
||||
dispatch({
|
||||
type: "show-palette",
|
||||
context: editor.getContext(),
|
||||
context: client.getContext(),
|
||||
});
|
||||
},
|
||||
},
|
||||
@ -280,11 +279,11 @@ export class MainUI {
|
||||
/>
|
||||
<div id="sb-main">
|
||||
{!!viewState.panels.lhs.mode && (
|
||||
<Panel config={viewState.panels.lhs} editor={editor} />
|
||||
<Panel config={viewState.panels.lhs} editor={client} />
|
||||
)}
|
||||
<div id="sb-editor" />
|
||||
{!!viewState.panels.rhs.mode && (
|
||||
<Panel config={viewState.panels.rhs} editor={editor} />
|
||||
<Panel config={viewState.panels.rhs} editor={client} />
|
||||
)}
|
||||
</div>
|
||||
{!!viewState.panels.modal.mode && (
|
||||
@ -292,12 +291,12 @@ export class MainUI {
|
||||
className="sb-modal"
|
||||
style={{ inset: `${viewState.panels.modal.mode}px` }}
|
||||
>
|
||||
<Panel config={viewState.panels.modal} editor={editor} />
|
||||
<Panel config={viewState.panels.modal} editor={client} />
|
||||
</div>
|
||||
)}
|
||||
{!!viewState.panels.bhs.mode && (
|
||||
<div className="sb-bhs">
|
||||
<Panel config={viewState.panels.bhs} editor={editor} />
|
||||
<Panel config={viewState.panels.bhs} editor={client} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
@ -11,7 +11,7 @@ export default function reducer(
|
||||
...state,
|
||||
isLoading: true,
|
||||
currentPage: action.name,
|
||||
panels: {
|
||||
panels: state.currentPage === action.name ? state.panels : {
|
||||
...state.panels,
|
||||
// Hide these by default to avoid flickering
|
||||
top: {},
|
||||
@ -45,19 +45,7 @@ export default function reducer(
|
||||
...state,
|
||||
syncFailures: action.syncSuccess ? 0 : state.syncFailures + 1,
|
||||
};
|
||||
case "start-navigate":
|
||||
return {
|
||||
...state,
|
||||
showPageNavigator: true,
|
||||
showCommandPalette: false,
|
||||
showFilterBox: false,
|
||||
};
|
||||
case "stop-navigate":
|
||||
return {
|
||||
...state,
|
||||
showPageNavigator: false,
|
||||
};
|
||||
case "pages-listed": {
|
||||
case "start-navigate": {
|
||||
// Let's move over any "lastOpened" times to the "allPages" list
|
||||
const oldPageMeta = new Map(
|
||||
[...state.allPages].map((pm) => [pm.name, pm]),
|
||||
@ -71,8 +59,17 @@ export default function reducer(
|
||||
return {
|
||||
...state,
|
||||
allPages: action.pages,
|
||||
showPageNavigator: true,
|
||||
showCommandPalette: false,
|
||||
showFilterBox: false,
|
||||
};
|
||||
}
|
||||
case "stop-navigate":
|
||||
return {
|
||||
...state,
|
||||
showPageNavigator: false,
|
||||
};
|
||||
|
||||
case "show-palette": {
|
||||
return {
|
||||
...state,
|
||||
|
@ -114,9 +114,15 @@
|
||||
color: var(--modal-selected-option-color);
|
||||
}
|
||||
|
||||
.sb-result-list .sb-hint {
|
||||
color: var(--modal-hint-color);
|
||||
background-color: var(--modal-hint-background-color);
|
||||
.sb-result-list {
|
||||
.sb-hint {
|
||||
color: var(--modal-hint-color);
|
||||
background-color: var(--modal-hint-background-color);
|
||||
}
|
||||
|
||||
.sb-description {
|
||||
color: var(--modal-description-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,6 +61,10 @@
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
.sb-description {
|
||||
font-size: 75%;
|
||||
}
|
||||
}
|
||||
|
||||
.sb-option,
|
||||
@ -81,4 +85,4 @@
|
||||
padding-bottom: 3px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
}
|
@ -36,6 +36,7 @@ html {
|
||||
--modal-selected-option-color: #eee;
|
||||
--modal-hint-background-color: #212476;
|
||||
--modal-hint-color: #eee;
|
||||
--modal-description-color: #aaa;
|
||||
|
||||
--notifications-background-color: inherit;
|
||||
--notifications-border-color: rgb(41, 41, 41);
|
||||
@ -153,6 +154,7 @@ html[data-theme="dark"] {
|
||||
--modal-selected-option-color: #eee;
|
||||
--modal-hint-background-color: #212476;
|
||||
--modal-hint-color: #eee;
|
||||
--modal-description-color: #aaa;
|
||||
|
||||
--notifications-background-color: #333;
|
||||
--notifications-border-color: rgb(197, 197, 197);
|
||||
|
@ -5,6 +5,7 @@ import { AppCommand } from "./hooks/command.ts";
|
||||
// Used by FilterBox
|
||||
export type FilterOption = {
|
||||
name: string;
|
||||
description?: string;
|
||||
orderId?: number;
|
||||
hint?: string;
|
||||
} & Record<string, any>;
|
||||
@ -111,11 +112,10 @@ export const initialViewState: AppViewState = {
|
||||
export type Action =
|
||||
| { type: "page-loaded"; meta: PageMeta }
|
||||
| { type: "page-loading"; name: string }
|
||||
| { type: "pages-listed"; pages: PageMeta[] }
|
||||
| { type: "page-changed" }
|
||||
| { type: "page-saved" }
|
||||
| { type: "sync-change"; syncSuccess: boolean }
|
||||
| { type: "start-navigate" }
|
||||
| { type: "start-navigate"; pages: PageMeta[] }
|
||||
| { type: "stop-navigate" }
|
||||
| {
|
||||
type: "update-commands";
|
||||
|
@ -15,9 +15,12 @@ Here is an example:
|
||||
## This is a section
|
||||
This is content
|
||||
|
||||
SilverBullet allows arbitrary metadata to be added to pages this way, with two exceptions:
|
||||
# Special attributes
|
||||
While SilverBullet allows arbitrary metadata to be added to pages, there are a few attributes with special meaning:
|
||||
|
||||
* `name` is an attribute used for page names, so don’t attempt to override it in frontmatter
|
||||
* `tags` can be specified (as in the example) and are, in effect, another way of adding tags to your page. You can achieve the same result by simply adding hashtags in the body of your document, e.g. `#tag1 #tag2`.
|
||||
* `name` (==DISALLOWED==): is an attribute used for page names, _you should not set it_.
|
||||
* `displayName` (`string`): very similar in effect as `aliases` but will use this name for the page in certain contexts.
|
||||
* `aliases` (`array of strings`): allow you to specify a list of alternative names for this page, which can be used to navigate or link to this page
|
||||
* `tags` (`array of strings` or `string`): an alternative (and perhaps preferred) way to assign [[Tags]] to a page. In principle you specify them as a list of strings, but for convenience you can also specify them as (possibly comma-separated) string, e.g. `tags: tag1, tag2, tag3`
|
||||
|
||||
SilverBullet also has the _convention_ of using attributes starting with a `$` for internal use. For instance, the sharing capability uses the `$share` attribute, and `$disableDirectives: true` has the special meaning of disabling [[🔌 Directive]] processing on a page.
|
||||
In addition, in the context of [[Templates]] frontmatter has a very specific interpretation.
|
@ -3,29 +3,28 @@ Live templates rendering [[Templates]] inline in a page.
|
||||
## 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.
|
||||
|
||||
Generally you’d use it in one of two ways, either using a `page` template reference, or an inline `template`:
|
||||
Generally you’d use it in one of two ways, either using a `page` [[Templates|template]] reference, or an inline `template`:
|
||||
|
||||
Here’s an example using `page`:
|
||||
|
||||
```template
|
||||
page: "[[template/today]]"
|
||||
```
|
||||
And here’s an example using `template`:
|
||||
|
||||
And here’s an example using `template`:
|
||||
```template
|
||||
template: |
|
||||
Today is {{today}}!
|
||||
```
|
||||
To pass in a value to the template, you can specify the optional `value` attribute:
|
||||
|
||||
To pass in a value to the template, you can specify the optional `value` attribute:
|
||||
```template
|
||||
template: |
|
||||
Hello, {{name}}! Today is _{{today}}_
|
||||
value:
|
||||
name: Pete
|
||||
```
|
||||
If you just want to render the raw markdown without handling it as a handlebars template, set `raw` to true:
|
||||
|
||||
If you just want to render the raw markdown without handling it as a handlebars template, set `raw` to true:
|
||||
```template
|
||||
template: |
|
||||
This is not going to be {{processed}} by Handlebars
|
||||
|
26
website/Page Templates.md
Normal file
26
website/Page Templates.md
Normal file
@ -0,0 +1,26 @@
|
||||
The {[Page: From Template]} command enables you to create a new page based on a page template. A page template is a [[Templates|template]] with the `type` attribute (in [[Frontmatter]]) set to `page`.
|
||||
|
||||
An example:
|
||||
|
||||
---
|
||||
tags: template
|
||||
type: page
|
||||
pageName: "📕 "
|
||||
---
|
||||
# {{@page.name}}
|
||||
As recorded on {{today}}.
|
||||
|
||||
## Introduction
|
||||
## Notes
|
||||
## Conclusions
|
||||
|
||||
Will prompt you to pick a page name (defaulting to “📕 “), and then create the following page (on 2023-08-08) when you pick “📕 Harry Potter” as a page name:
|
||||
|
||||
# 📕 Harry Potter
|
||||
As recorded on 2022-08-08.
|
||||
|
||||
## Introduction
|
||||
## Notes
|
||||
## Conclusions
|
||||
|
||||
As with any [[Templates|template]], the `frontmatter` can be used to define [[Frontmatter]] for the new page.
|
@ -2,62 +2,6 @@
|
||||
|
||||
The [[Plugs/Template]] plug implements a few templating mechanisms.
|
||||
|
||||
### Page Templates
|
||||
> **Warning** Deprecated
|
||||
> Use [[Slash Templates]] instead
|
||||
|
||||
The {[Template: Instantiate Page]} command enables you to create a new page based on a page template.
|
||||
|
||||
Page templates, by default, are looked for in the `template/page/` prefix. So creating e.g. a `template/page/Meeting Notes` page will create a “Meeting Notes” template. You can override this prefix by setting the `pageTemplatePrefix` in `SETTINGS`.
|
||||
|
||||
Page templates have one “magic” type of page metadata that is used during
|
||||
instantiation:
|
||||
|
||||
* `$name` is used as the default value for a new page based on this template
|
||||
|
||||
In addition, any standard template placeholders are available (see below)
|
||||
|
||||
For instance:
|
||||
|
||||
---
|
||||
$name: "📕 "
|
||||
---
|
||||
|
||||
# {{@page.name}}
|
||||
As recorded on {{today}}.
|
||||
|
||||
## Introduction
|
||||
## Notes
|
||||
## Conclusions
|
||||
|
||||
Will prompt you to pick a page name (defaulting to “📕 “), and then create the following page (on 2022-08-08) when you pick “📕 Harry Potter” as a page name:
|
||||
|
||||
# 📕 Harry Potter
|
||||
As recorded on 2022-08-08.
|
||||
|
||||
## Introduction
|
||||
## Notes
|
||||
## Conclusions
|
||||
|
||||
### Snippets
|
||||
$snippets
|
||||
> **Warning** Deprecated
|
||||
> Use [[Slash Templates]] instead
|
||||
|
||||
Snippets are similar to page templates, except you insert them into an existing page with the `/snippet` slash command. The default prefix is `snippet/` which is configurable via the `snippetPrefix` setting in `SETTINGS`.
|
||||
|
||||
Snippet templates do not support the `$name` page meta, because it doesn’t apply.
|
||||
|
||||
However, snippets do support the special `|^|` placeholder for placing the cursor caret after injecting the snippet. If you leave it out, the cursor will simply be placed at the end, but if you like to insert the cursor elsewhere, that position can be set with the `|^|` placeholder.
|
||||
|
||||
For instance to replicate the `/query` slash command as a snippet:
|
||||
|
||||
<!-- #query |^| -->
|
||||
|
||||
<!-- /query -->
|
||||
|
||||
Which would insert the cursor right after `#query`.
|
||||
|
||||
### 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).
|
||||
|
@ -7,10 +7,6 @@ indexPage: "[[SilverBullet]]"
|
||||
# Load custom CSS styles from the following page, can also be an array
|
||||
customStyles: "[[STYLES]]"
|
||||
|
||||
# Template related settings
|
||||
pageTemplatePrefix: "template/page/"
|
||||
snippetPrefix: "snippet/"
|
||||
|
||||
quickNotePrefix: "📥 "
|
||||
|
||||
dailyNotePrefix: "📅 "
|
||||
|
@ -33,6 +33,7 @@ Some highlights:
|
||||
* SilverBullet runs in any modern browser (including mobile ones) as a [[PWA]] in two [[Client Modes]] ([[Client Modes$online|online]] and [[Client Modes$sync|synced]] mode), where the _synced mode_ enables **100% offline operation**, keeping a copy of content in the browser’s local ([IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API)) database, syncing back to the server when a network connection is available.
|
||||
* SilverBullet provides an enjoyable [[Markdown]] writing experience with a clean UI, rendering text using [[Live Preview|live preview]], further **reducing visual noise** while still providing direct access to the underlying markdown syntax.
|
||||
* SilverBullet supports wiki-style **page linking** using the `[[page link]]` syntax. Incoming links are indexed and appear as [[Linked Mentions]] at the bottom of the pages linked to thereby providing _bi-directional linking_.
|
||||
* SilverBullet allows you to be extra productive using its [[Templates]] mechanism.
|
||||
* SilverBullet is optimized for **keyboard-based operation**:
|
||||
* Quickly navigate between pages using the **page switcher** (triggered with `Cmd-k` on Mac or `Ctrl-k` on Linux and Windows).
|
||||
* Run commands via their keyboard shortcuts or the **command palette** (triggered with `Cmd-/` or `Ctrl-/` on Linux and Windows).
|
||||
|
@ -5,7 +5,6 @@ The [[Plugs/Editor]] plug provides a few helpful ones:
|
||||
* `/h1` through `/h4` to turn the current line into a header
|
||||
* `/hr` to insert a horizontal rule (`---`)
|
||||
* `/table` to insert a markdown table (whoever can remember this syntax without it)
|
||||
* `/snippet` see [[Plugs/Template@snippets]]
|
||||
* `/today` to insert today’s date
|
||||
* `/tomorrow` to insert tomorrow’s date
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
Slash templates allow you to define custom [[Slash Commands]] that expand “snippet style” templates inline. They’re like [[🔌 Template$snippets]], but appear immediately as slash commands.
|
||||
Slash templates allow you to define custom [[Slash Commands]] that expand “snippet style” templates inline.
|
||||
|
||||
## Definition
|
||||
# Definition
|
||||
You can define a slash template by creating a [[Templates|template page]] with a template tag and `trigger` attribute.
|
||||
|
||||
Example:
|
||||
@ -13,11 +13,8 @@ Example:
|
||||
|
||||
|^|
|
||||
|
||||
## Use
|
||||
You can _trigger_ the slash template by typing `/meeting-notes` in any page. That’s it.
|
||||
|
||||
## Frontmatter
|
||||
A template’s [[Frontmatter]] is interpreted by SilverBullet’s template engine and removed when instantiated. However, to still include frontmatter after instantiation, you can use the `frontmatter` attribute.
|
||||
A template’s [[Frontmatter]] is interpreted by SilverBullet’s [[Templates|template]] engine and removed when instantiated. However, to still include frontmatter after instantiation, you can use the `frontmatter` attribute.
|
||||
|
||||
Example:
|
||||
|
||||
@ -40,3 +37,7 @@ Which will expand into e.g.
|
||||
|
||||
.
|
||||
|
||||
When the page already contains frontmatter before invoking the slash command, it will be augmented with the additional frontmatter specified by the template.
|
||||
|
||||
# Use
|
||||
You can _trigger_ the slash template by typing `/<trigger>` (e.g. `/meeting-notes`) in any page.
|
||||
|
@ -1,25 +1,55 @@
|
||||
Templates are _reusable_ pieces of markdown content, usually with placeholders that are replaced once instantiated.
|
||||
Templates are reusable pieces of markdown content, usually with placeholders that are replaced once instantiated.
|
||||
|
||||
Templates are used in a few different contexts:
|
||||
There are two general uses for templates:
|
||||
|
||||
1. To render [[Live Queries]]
|
||||
2. To render [[Live Templates]]
|
||||
3. To be included using [[Slash Templates]]
|
||||
4. Some legacy use cases described in [[Plugs/Template]]
|
||||
1. _Live_ uses, where page content is dynamically updated based on templates:
|
||||
* [[Live Queries]]
|
||||
* [[Live Templates]]
|
||||
2. _One-off_ uses, where a template is instantiated once and inserted into an existing or new page:
|
||||
* [[Slash Templates]]
|
||||
* [[Page Templates]]
|
||||
|
||||
## Creating templates
|
||||
Templates are defined as any other page. It’s convenient, although not required, to use a `template/` prefix when naming templates. It is also _recommended_ to tag templates with a `#template` tag. Note that this tag will be removed when the template is instantiated.
|
||||
# Creating templates
|
||||
Templates are regular pages [[Tags|tagged]] with the `#template` tag. Note that, when tagged inline (by putting `#template` at the beginning of the page), the tag will be removed when the template is instantiated.
|
||||
|
||||
Tagging a page with a `#template` tag (either in the [[Frontmatter]] or using a [[Tags]] at the very beginning of the page content) does two things:
|
||||
**Naming**: it’s common, although not required, to use a `template/` prefix when naming templates.
|
||||
|
||||
1. It excludes the page from being indexed for [[Objects]], that is: any tasks, items, paragraphs etc. will not appear in your space’s object database. Which is usually what you want.
|
||||
2. It allows you to register your templates to be used as [[Slash Templates]].
|
||||
Tagging a page with a `#template` tag (either in the [[Frontmatter]] or using a [[Tags]] at the very beginning of the page content) does a few things:
|
||||
|
||||
1. It will make the page appear when completing template names, e.g. in `render` clauses in [[Live Queries]], or after the `page` key in [[Live Templates]].
|
||||
2. It excludes the page from being indexed for [[Objects]], that is: any tasks, items, paragraphs etc. will not appear in your space’s object database. Which is usually what you want.
|
||||
3. It registers your templates to be used as [[Slash Templates]] as well as [[Page Templates]].
|
||||
|
||||
## Frontmatter
|
||||
[[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]]
|
||||
* `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.
|
||||
* `frontmatter` (optional): defines [[Frontmatter]] to be added/used in the rendered template. This can either be specified as a string or as an object.
|
||||
|
||||
An example:
|
||||
|
||||
---
|
||||
tags: template
|
||||
type: page
|
||||
trigger: one-on-one
|
||||
displayName: "1:1 template"
|
||||
pageName: "1-1s/"
|
||||
frontmatter:
|
||||
dateCreated: "{{today}}"
|
||||
---
|
||||
# {{today}}
|
||||
* |^|
|
||||
|
||||
# Template content
|
||||
Templates consist of markdown, but can also include [Handlebars syntax](https://handlebarsjs.com/), such as `{{today}}`, and `{{#each .}}`.
|
||||
|
||||
In addition the special `|^|` marker can be used to specify the desired cursor position after the template is included (relevant mostly to [[Slash Templates]]).
|
||||
The special `|^|` marker can be used to specify the desired cursor position after the template is included.
|
||||
|
||||
### Template helpers
|
||||
## Handlebar helpers
|
||||
There are a number of built-in handlebars helpers you can use:
|
||||
|
||||
- `{{today}}`: Today’s date in the usual YYYY-MM-DD format
|
||||
|
9
website/template/page/slash-template.md
Normal file
9
website/template/page/slash-template.md
Normal file
@ -0,0 +1,9 @@
|
||||
---
|
||||
tags: template
|
||||
type: page
|
||||
displayName: Slash Template
|
||||
pageName: "template/slash/"
|
||||
frontmatter:
|
||||
tags: template
|
||||
trigger: "|^|"
|
||||
---
|
10
website/template/page/template.md
Normal file
10
website/template/page/template.md
Normal file
@ -0,0 +1,10 @@
|
||||
---
|
||||
tags: template
|
||||
type: page
|
||||
displayName: Page Template
|
||||
pageName: "template/page/"
|
||||
frontmatter:
|
||||
tags: template
|
||||
type: page
|
||||
---
|
||||
|^|
|
@ -1 +1 @@
|
||||
Today is {{today}}!
|
||||
#template Today is {{today}}!
|
Loading…
Reference in New Issue
Block a user