Work on #587: revamped templates
This commit is contained in:
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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:
|
||||
|
||||
+55
-171
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user