Templates 2.0 (#636)
Templates 2.0 and a whole bunch of other refactoring
This commit is contained in:
@@ -17,8 +17,14 @@ export async function pageComplete(completeEvent: CompleteEvent) {
|
||||
/render\s+\[\[|page:\s*["']\[\[/.test(
|
||||
completeEvent.linePrefix,
|
||||
);
|
||||
const tagToQuery = isInTemplateContext ? "template" : "page";
|
||||
let allPages: PageMeta[] = await queryObjects<PageMeta>(tagToQuery, {}, 5);
|
||||
|
||||
// When in a template context, we only want to complete template pages
|
||||
// When outside of a template context, we want to complete all pages except template pages
|
||||
let allPages: PageMeta[] = isInTemplateContext
|
||||
? await queryObjects<PageMeta>("template", {}, 5)
|
||||
: await queryObjects<PageMeta>("page", {
|
||||
filter: ["!=", ["attr", "tags"], ["string", "template"]],
|
||||
}, 5);
|
||||
const prefix = match[1];
|
||||
if (prefix.startsWith("!")) {
|
||||
// Federation prefix, let's first see if we're matching anything from federation that is locally synced
|
||||
|
||||
@@ -15,6 +15,27 @@ functions:
|
||||
command:
|
||||
name: "Editor: Toggle Dark Mode"
|
||||
|
||||
openCommandPalette:
|
||||
path: editor.ts:openCommandPalette
|
||||
command:
|
||||
name: "Open Command Palette"
|
||||
key: "Ctrl-/"
|
||||
mac: "Cmd-/"
|
||||
|
||||
openPageNavigator:
|
||||
path: editor.ts:openPageNavigator
|
||||
command:
|
||||
name: "Open Page Navigator"
|
||||
key: "Ctrl-k"
|
||||
mac: "Cmd-k"
|
||||
|
||||
openTemplateNavigator:
|
||||
path: editor.ts:openTemplateNavigator
|
||||
command:
|
||||
name: "Open Template Navigator"
|
||||
key: "Ctrl-Shift-t"
|
||||
mac: "Cmd-Shift-t"
|
||||
|
||||
# Page operations
|
||||
deletePage:
|
||||
path: "./page.ts:deletePage"
|
||||
@@ -35,6 +56,11 @@ functions:
|
||||
events:
|
||||
- editor:complete
|
||||
|
||||
reloadSettingsAndCommands:
|
||||
path: editor.ts:reloadSettingsAndCommands
|
||||
command:
|
||||
name: "System: Reload Settings and Commands"
|
||||
|
||||
# Navigation
|
||||
linkNavigate:
|
||||
path: "./navigate.ts:linkNavigate"
|
||||
|
||||
@@ -10,6 +10,18 @@ export async function setEditorMode() {
|
||||
}
|
||||
}
|
||||
|
||||
export function openCommandPalette() {
|
||||
return editor.openCommandPalette();
|
||||
}
|
||||
|
||||
export async function openPageNavigator() {
|
||||
await editor.openPageNavigator("page");
|
||||
}
|
||||
|
||||
export async function openTemplateNavigator() {
|
||||
await editor.openPageNavigator("template");
|
||||
}
|
||||
|
||||
export async function toggleDarkMode() {
|
||||
let darkMode = await clientStore.get("darkMode");
|
||||
darkMode = !darkMode;
|
||||
@@ -34,3 +46,8 @@ export async function moveToPosCommand() {
|
||||
export async function customFlashMessage(_def: any, message: string) {
|
||||
await editor.flashNotification(message);
|
||||
}
|
||||
|
||||
export async function reloadSettingsAndCommands() {
|
||||
await editor.reloadSettingsAndCommands();
|
||||
await editor.flashNotification("Reloaded settings and commands");
|
||||
}
|
||||
|
||||
+20
-9
@@ -14,15 +14,20 @@ export async function deletePage() {
|
||||
await space.deletePage(pageName);
|
||||
}
|
||||
|
||||
export async function copyPage(_def: any, predefinedNewName: string) {
|
||||
const oldName = await editor.getCurrentPage();
|
||||
let suggestedName = predefinedNewName || oldName;
|
||||
export async function copyPage(
|
||||
_def: any,
|
||||
sourcePage?: string,
|
||||
toName?: string,
|
||||
) {
|
||||
const currentPage = await editor.getCurrentPage();
|
||||
const fromName = sourcePage || currentPage;
|
||||
let suggestedName = toName || fromName;
|
||||
|
||||
if (isFederationPath(oldName)) {
|
||||
const pieces = oldName.split("/");
|
||||
if (isFederationPath(fromName)) {
|
||||
const pieces = fromName.split("/");
|
||||
suggestedName = pieces.slice(1).join("/");
|
||||
}
|
||||
const newName = await editor.prompt(`Copy to new page:`, suggestedName);
|
||||
const newName = await editor.prompt(`Copy to page:`, suggestedName);
|
||||
|
||||
if (!newName) {
|
||||
return;
|
||||
@@ -44,11 +49,17 @@ export async function copyPage(_def: any, predefinedNewName: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const text = await editor.getText();
|
||||
const text = await space.readPage(fromName);
|
||||
|
||||
console.log("Writing new page to space");
|
||||
await space.writePage(newName, text);
|
||||
|
||||
console.log("Navigating to new page");
|
||||
await editor.navigate(newName);
|
||||
if (currentPage === fromName) {
|
||||
// If we're copying the current page, navigate there
|
||||
console.log("Navigating to new page");
|
||||
await editor.navigate(newName);
|
||||
} else {
|
||||
// Otherwise just notify of success
|
||||
await editor.flashNotification("Page copied successfully");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,3 +28,9 @@ functions:
|
||||
pageNamespace:
|
||||
pattern: "!.+"
|
||||
operation: getFileMeta
|
||||
|
||||
# Library management
|
||||
importLibraryCommand:
|
||||
path: library.ts:importLibraryCommand
|
||||
command:
|
||||
name: "Library: Import"
|
||||
@@ -117,7 +117,7 @@ export async function cacheFileListing(uri: string): Promise<FileMeta[]> {
|
||||
|
||||
export async function readFile(
|
||||
name: string,
|
||||
): Promise<{ data: Uint8Array; meta: FileMeta } | undefined> {
|
||||
): Promise<{ data: Uint8Array; meta: FileMeta }> {
|
||||
const url = federatedPathToUrl(name);
|
||||
console.log("Fetching federated file", url);
|
||||
const r = await nativeFetch(url, {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { editor, space } from "$sb/syscalls.ts";
|
||||
import { cacheFileListing, readFile } from "./federation.ts";
|
||||
|
||||
export async function importLibraryCommand(_def: any, uri?: string) {
|
||||
if (!uri) {
|
||||
uri = await editor.prompt("Import library (federation URL):");
|
||||
}
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
uri = uri.trim();
|
||||
if (!uri.startsWith("!")) {
|
||||
uri = `!${uri}`;
|
||||
}
|
||||
const allTemplates = (await cacheFileListing(uri)).filter((f) =>
|
||||
f.name.endsWith(".md")
|
||||
);
|
||||
if (
|
||||
!await editor.confirm(
|
||||
`You are about to import ${allTemplates.length} templates, want to do this?`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
for (const template of allTemplates) {
|
||||
// Clean up file path
|
||||
let pageName = template.name.replace(/\.md$/, "");
|
||||
// Remove the federation part
|
||||
const pieces = pageName.split("/");
|
||||
pageName = pieces.slice(1).join("/");
|
||||
|
||||
// Fetch the file
|
||||
const buf = (await readFile(template.name)).data;
|
||||
|
||||
try {
|
||||
// Check if it already exists
|
||||
await space.getPageMeta(pageName);
|
||||
|
||||
if (
|
||||
!await editor.confirm(
|
||||
`Page ${pageName} already exists, are you sure you want to override it?`,
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
} catch {
|
||||
// Expected
|
||||
}
|
||||
|
||||
// Write to local space
|
||||
await space.writePage(pageName, new TextDecoder().decode(buf));
|
||||
}
|
||||
await editor.reloadSettingsAndCommands();
|
||||
await editor.flashNotification("Import complete!");
|
||||
}
|
||||
+1
-1
@@ -60,7 +60,7 @@ export async function clearIndex(): Promise<void> {
|
||||
console.log("Deleted", allKeys.length, "keys from the index");
|
||||
}
|
||||
|
||||
// ENTITIES API
|
||||
// OBJECTS API
|
||||
|
||||
/**
|
||||
* Indexes entities in the data store
|
||||
|
||||
@@ -80,11 +80,7 @@ export const builtins: Record<string, Record<string, string>> = {
|
||||
page: "!string",
|
||||
pageName: "string",
|
||||
pos: "!number",
|
||||
type: "string",
|
||||
trigger: "string",
|
||||
where: "string",
|
||||
priority: "number",
|
||||
enabled: "boolean",
|
||||
hooks: "hooksSpec",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -162,17 +162,17 @@ functions:
|
||||
|
||||
# Template Widgets
|
||||
renderTemplateWidgetsTop:
|
||||
path: template_widget.ts:renderTemplateWidgets
|
||||
path: widget.ts:renderTemplateWidgets
|
||||
env: client
|
||||
panelWidget: top
|
||||
|
||||
renderTemplateWidgetsBottom:
|
||||
path: template_widget.ts:renderTemplateWidgets
|
||||
path: widget.ts:renderTemplateWidgets
|
||||
env: client
|
||||
panelWidget: bottom
|
||||
|
||||
refreshWidgets:
|
||||
path: template_widget.ts:refreshWidgets
|
||||
path: widget.ts:refreshWidgets
|
||||
|
||||
lintYAML:
|
||||
path: lint.ts:lintYAML
|
||||
|
||||
@@ -41,6 +41,10 @@ export async function widget(
|
||||
return false;
|
||||
});
|
||||
|
||||
if (headers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (config.minHeaders && headers.length < config.minHeaders) {
|
||||
// Not enough headers, not showing TOC
|
||||
return null;
|
||||
|
||||
@@ -7,9 +7,9 @@ import {
|
||||
} from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { parseTreeToAST, renderToText } from "$sb/lib/tree.ts";
|
||||
import { CodeWidgetContent } from "$sb/types.ts";
|
||||
import { loadPageObject } from "../template/template.ts";
|
||||
import { loadPageObject } from "../template/page.ts";
|
||||
import { queryObjects } from "./api.ts";
|
||||
import { TemplateObject } from "../template/types.ts";
|
||||
import { TemplateObject, WidgetConfig } from "../template/types.ts";
|
||||
import { expressionToKvQueryExpression } from "$sb/lib/parse-query.ts";
|
||||
import { evalQueryExpression } from "$sb/lib/query.ts";
|
||||
import { renderTemplate } from "../template/plug_api.ts";
|
||||
@@ -24,35 +24,39 @@ export async function renderTemplateWidgets(side: "top" | "bottom"): Promise<
|
||||
CodeWidgetContent | null
|
||||
> {
|
||||
const text = await editor.getText();
|
||||
const pageMeta = await loadPageObject(await editor.getCurrentPage());
|
||||
let pageMeta = await loadPageObject(await editor.getCurrentPage());
|
||||
const parsedMd = await markdown.parseMarkdown(text);
|
||||
const frontmatter = await extractFrontmatter(parsedMd);
|
||||
|
||||
const allFrontMatterTemplates = await queryObjects<TemplateObject>(
|
||||
pageMeta = { ...pageMeta, ...frontmatter };
|
||||
|
||||
const blockTemplates = await queryObjects<TemplateObject>(
|
||||
"template",
|
||||
{
|
||||
// where type = "widget:X" and enabled != false
|
||||
filter: ["and", ["=", ["attr", "type"], ["string", `widget:${side}`]], [
|
||||
"!=",
|
||||
["attr", "enabled"],
|
||||
["boolean", false],
|
||||
]],
|
||||
orderBy: [{ expr: ["attr", "priority"], desc: false }],
|
||||
// where hooks.top/bottom exists
|
||||
filter: ["attr", ["attr", "hooks"], side],
|
||||
orderBy: [{
|
||||
// order by hooks.top/bottom.order asc
|
||||
expr: ["attr", ["attr", ["attr", "hooks"], side], "order"],
|
||||
desc: false,
|
||||
}],
|
||||
},
|
||||
);
|
||||
// console.log(`Found the following ${side} templates`, blockTemplates);
|
||||
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) {
|
||||
if (!template.where) {
|
||||
for (const template of blockTemplates) {
|
||||
if (!template.hooks) {
|
||||
console.warn(
|
||||
"Skipping template",
|
||||
"No hooks specified for template",
|
||||
template.ref,
|
||||
"because it has no 'where' expression",
|
||||
"this should never happen",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const blockDef = WidgetConfig.parse(template.hooks[side]!);
|
||||
const exprAST = parseTreeToAST(
|
||||
await language.parseLanguage("expression", template.where!),
|
||||
await language.parseLanguage("expression", blockDef.where!),
|
||||
);
|
||||
const parsedExpression = expressionToKvQueryExpression(exprAST[1]);
|
||||
if (evalQueryExpression(parsedExpression, pageMeta)) {
|
||||
@@ -68,11 +72,11 @@ export async function renderTemplateWidgets(side: "top" | "bottom"): Promise<
|
||||
rewritePageRefs(parsedMarkdown, template.ref);
|
||||
renderedTemplate = renderToText(parsedMarkdown);
|
||||
|
||||
// console.log("Rendering template", template.ref, renderedTemplate);
|
||||
templateBits.push(renderedTemplate.trim());
|
||||
}
|
||||
}
|
||||
const summaryText = templateBits.join("\n");
|
||||
// console.log("Rendered", summaryText);
|
||||
return {
|
||||
markdown: summaryText,
|
||||
buttons: [
|
||||
@@ -54,6 +54,7 @@ export async function expandCodeWidgets(
|
||||
// 'not found' is to be expected (no code widget configured for this language)
|
||||
// Every other error should probably be reported
|
||||
if (!e.message.includes("not found")) {
|
||||
console.trace();
|
||||
console.error("Error rendering code", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,6 +330,13 @@ function render(
|
||||
const command = t.children![1].children![0].text!;
|
||||
let commandText = command;
|
||||
const aliasNode = findNodeOfType(t, "CommandLinkAlias");
|
||||
const argsNode = findNodeOfType(t, "CommandLinkArgs");
|
||||
let args: any = [];
|
||||
|
||||
if (argsNode) {
|
||||
args = JSON.parse(`[${argsNode.children![0].text!}]`);
|
||||
}
|
||||
|
||||
if (aliasNode) {
|
||||
commandText = aliasNode.children![0].text!;
|
||||
}
|
||||
@@ -337,7 +344,7 @@ function render(
|
||||
return {
|
||||
name: "button",
|
||||
attrs: {
|
||||
"data-onclick": JSON.stringify(["command", command]),
|
||||
"data-onclick": JSON.stringify(["command", command, args]),
|
||||
},
|
||||
body: commandText,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { LintEvent } from "$sb/app_event.ts";
|
||||
import { parseQuery } from "$sb/lib/parse-query.ts";
|
||||
import { cleanPageRef, resolvePath } from "$sb/lib/resolve.ts";
|
||||
import { findNodeOfType, traverseTreeAsync } from "$sb/lib/tree.ts";
|
||||
import { events, space } from "$sb/syscalls.ts";
|
||||
import { LintDiagnostic } from "$sb/types.ts";
|
||||
import { loadPageObject, replaceTemplateVars } from "../template/page.ts";
|
||||
|
||||
export async function lintQuery(
|
||||
{ name, tree }: LintEvent,
|
||||
): Promise<LintDiagnostic[]> {
|
||||
const diagnostics: LintDiagnostic[] = [];
|
||||
await traverseTreeAsync(tree, async (node) => {
|
||||
if (node.type === "FencedCode") {
|
||||
const codeInfo = findNodeOfType(node, "CodeInfo")!;
|
||||
if (!codeInfo) {
|
||||
return true;
|
||||
}
|
||||
const codeLang = codeInfo.children![0].text!;
|
||||
if (
|
||||
codeLang !== "query"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const codeText = findNodeOfType(node, "CodeText");
|
||||
if (!codeText) {
|
||||
return true;
|
||||
}
|
||||
const bodyText = codeText.children![0].text!;
|
||||
try {
|
||||
const pageObject = await loadPageObject(name);
|
||||
const parsedQuery = await parseQuery(
|
||||
await replaceTemplateVars(bodyText, pageObject),
|
||||
);
|
||||
|
||||
const allSources = await allQuerySources();
|
||||
if (
|
||||
parsedQuery.querySource &&
|
||||
!allSources.includes(parsedQuery.querySource)
|
||||
) {
|
||||
diagnostics.push({
|
||||
from: codeText.from!,
|
||||
to: codeText.to!,
|
||||
message: `Unknown query source '${parsedQuery.querySource}'`,
|
||||
severity: "error",
|
||||
});
|
||||
}
|
||||
if (parsedQuery.render) {
|
||||
const templatePage = resolvePath(
|
||||
name,
|
||||
cleanPageRef(parsedQuery.render),
|
||||
);
|
||||
try {
|
||||
await space.getPageMeta(templatePage);
|
||||
} catch {
|
||||
diagnostics.push({
|
||||
from: codeText.from!,
|
||||
to: codeText.to!,
|
||||
message: `Could not resolve template ${templatePage}`,
|
||||
severity: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
diagnostics.push({
|
||||
from: codeText.from!,
|
||||
to: codeText.to!,
|
||||
message: e.message,
|
||||
severity: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
async function allQuerySources(): Promise<string[]> {
|
||||
const allEvents = await events.listEvents();
|
||||
|
||||
const allSources = allEvents
|
||||
.filter((eventName) =>
|
||||
eventName.startsWith("query:") && !eventName.includes("*")
|
||||
)
|
||||
.map((source) => source.substring("query:".length));
|
||||
|
||||
const allObjectTypes: string[] = (await events.dispatchEvent("query_", {}))
|
||||
.flat();
|
||||
|
||||
return [...allSources, ...allObjectTypes];
|
||||
}
|
||||
@@ -1,20 +1,19 @@
|
||||
name: query
|
||||
functions:
|
||||
queryWidget:
|
||||
path: query.ts:widget
|
||||
path: widget.ts:widget
|
||||
codeWidget: query
|
||||
renderMode: markdown
|
||||
|
||||
# Query widget buttons
|
||||
editButton:
|
||||
path: widget.ts:editButton
|
||||
|
||||
lintQuery:
|
||||
path: query.ts:lintQuery
|
||||
path: lint.ts:lintQuery
|
||||
events:
|
||||
- editor:lint
|
||||
|
||||
templateWidget:
|
||||
path: template.ts:widget
|
||||
codeWidget: template
|
||||
renderMode: markdown
|
||||
|
||||
queryComplete:
|
||||
path: complete.ts:queryComplete
|
||||
events:
|
||||
@@ -31,26 +30,3 @@ functions:
|
||||
name: "Live Queries and Templates: Refresh All"
|
||||
key: "Alt-q"
|
||||
|
||||
# Query widget buttons
|
||||
editButton:
|
||||
path: widget.ts:editButton
|
||||
|
||||
# Slash commands
|
||||
insertQuery:
|
||||
redirect: template.insertTemplateText
|
||||
slashCommand:
|
||||
name: query
|
||||
description: Insert a query
|
||||
value: |
|
||||
```query
|
||||
|^|
|
||||
```
|
||||
insertUseTemplate:
|
||||
redirect: template.insertTemplateText
|
||||
slashCommand:
|
||||
name: template
|
||||
description: Use a template
|
||||
value: |
|
||||
```template
|
||||
page: "[[|^|]]"
|
||||
```
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
import type { LintEvent } from "$sb/app_event.ts";
|
||||
import { events, space } from "$sb/syscalls.ts";
|
||||
import { findNodeOfType, traverseTreeAsync } from "$sb/lib/tree.ts";
|
||||
import { parseQuery } from "$sb/lib/parse-query.ts";
|
||||
import { loadPageObject, replaceTemplateVars } from "../template/template.ts";
|
||||
import { cleanPageRef, resolvePath } from "$sb/lib/resolve.ts";
|
||||
import {
|
||||
CodeWidgetContent,
|
||||
LintDiagnostic,
|
||||
PageMeta,
|
||||
Query,
|
||||
} from "$sb/types.ts";
|
||||
import { jsonToMDTable, renderQueryTemplate } from "../template/util.ts";
|
||||
|
||||
export async function widget(
|
||||
bodyText: string,
|
||||
pageName: string,
|
||||
): Promise<CodeWidgetContent> {
|
||||
const pageObject = await loadPageObject(pageName);
|
||||
try {
|
||||
let resultMarkdown = "";
|
||||
const parsedQuery = await parseQuery(
|
||||
await replaceTemplateVars(bodyText, pageObject),
|
||||
);
|
||||
|
||||
const results = await performQuery(
|
||||
parsedQuery,
|
||||
pageObject,
|
||||
);
|
||||
if (results.length === 0 && !parsedQuery.renderAll) {
|
||||
resultMarkdown = "No results";
|
||||
} 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,
|
||||
results,
|
||||
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(results);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
markdown: resultMarkdown,
|
||||
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: "query.refreshAllWidgets",
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (e: any) {
|
||||
return { markdown: `**Error:** ${e.message}` };
|
||||
}
|
||||
}
|
||||
|
||||
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[]> {
|
||||
const diagnostics: LintDiagnostic[] = [];
|
||||
await traverseTreeAsync(tree, async (node) => {
|
||||
if (node.type === "FencedCode") {
|
||||
const codeInfo = findNodeOfType(node, "CodeInfo")!;
|
||||
if (!codeInfo) {
|
||||
return true;
|
||||
}
|
||||
const codeLang = codeInfo.children![0].text!;
|
||||
if (
|
||||
codeLang !== "query"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const codeText = findNodeOfType(node, "CodeText");
|
||||
if (!codeText) {
|
||||
return true;
|
||||
}
|
||||
const bodyText = codeText.children![0].text!;
|
||||
try {
|
||||
const pageObject = await loadPageObject(name);
|
||||
const parsedQuery = await parseQuery(
|
||||
await replaceTemplateVars(bodyText, pageObject),
|
||||
);
|
||||
|
||||
const allSources = await allQuerySources();
|
||||
if (
|
||||
parsedQuery.querySource &&
|
||||
!allSources.includes(parsedQuery.querySource)
|
||||
) {
|
||||
diagnostics.push({
|
||||
from: codeText.from!,
|
||||
to: codeText.to!,
|
||||
message: `Unknown query source '${parsedQuery.querySource}'`,
|
||||
severity: "error",
|
||||
});
|
||||
}
|
||||
if (parsedQuery.render) {
|
||||
const templatePage = resolvePath(
|
||||
name,
|
||||
cleanPageRef(parsedQuery.render),
|
||||
);
|
||||
try {
|
||||
await space.getPageMeta(templatePage);
|
||||
} catch {
|
||||
diagnostics.push({
|
||||
from: codeText.from!,
|
||||
to: codeText.to!,
|
||||
message: `Could not resolve template ${templatePage}`,
|
||||
severity: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
diagnostics.push({
|
||||
from: codeText.from!,
|
||||
to: codeText.to!,
|
||||
message: e.message,
|
||||
severity: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
async function allQuerySources(): Promise<string[]> {
|
||||
const allEvents = await events.listEvents();
|
||||
|
||||
const allSources = allEvents
|
||||
.filter((eventName) =>
|
||||
eventName.startsWith("query:") && !eventName.includes("*")
|
||||
)
|
||||
.map((source) => source.substring("query:".length));
|
||||
|
||||
const allObjectTypes: string[] = (await events.dispatchEvent("query_", {}))
|
||||
.flat();
|
||||
|
||||
return [...allSources, ...allObjectTypes];
|
||||
}
|
||||
+82
-1
@@ -1,4 +1,85 @@
|
||||
import { codeWidget, editor } from "$sb/syscalls.ts";
|
||||
import { codeWidget, editor, events } from "$sb/syscalls.ts";
|
||||
import { parseQuery } from "$sb/lib/parse-query.ts";
|
||||
import { loadPageObject, replaceTemplateVars } from "../template/page.ts";
|
||||
import { resolvePath } from "$sb/lib/resolve.ts";
|
||||
import { CodeWidgetContent, PageMeta, Query } from "$sb/types.ts";
|
||||
import { jsonToMDTable, renderQueryTemplate } from "../template/util.ts";
|
||||
|
||||
export async function widget(
|
||||
bodyText: string,
|
||||
pageName: string,
|
||||
): Promise<CodeWidgetContent> {
|
||||
const pageObject = await loadPageObject(pageName);
|
||||
try {
|
||||
let resultMarkdown = "";
|
||||
const parsedQuery = await parseQuery(
|
||||
await replaceTemplateVars(bodyText, pageObject),
|
||||
);
|
||||
|
||||
const results = await performQuery(
|
||||
parsedQuery,
|
||||
pageObject,
|
||||
);
|
||||
if (results.length === 0 && !parsedQuery.renderAll) {
|
||||
resultMarkdown = "No results";
|
||||
} 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,
|
||||
results,
|
||||
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(results);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
markdown: resultMarkdown,
|
||||
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: "query.refreshAllWidgets",
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (e: any) {
|
||||
return { markdown: `**Error:** ${e.message}` };
|
||||
}
|
||||
}
|
||||
|
||||
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 function refreshAllWidgets() {
|
||||
codeWidget.refreshAll();
|
||||
|
||||
@@ -23,15 +23,6 @@ functions:
|
||||
updateTaskState:
|
||||
path: task.ts:updateTaskState
|
||||
|
||||
|
||||
turnIntoTask:
|
||||
redirect: template.applyLineReplace
|
||||
slashCommand:
|
||||
name: task
|
||||
description: Turn into task
|
||||
match: "^(\\s*)[\\-\\*]?\\s*(\\[[ xX]\\])?\\s*"
|
||||
replace: "$1* [ ] "
|
||||
|
||||
indexTasks:
|
||||
path: "./task.ts:indexTasks"
|
||||
events:
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
import { CompleteEvent, SlashCompletion } from "$sb/app_event.ts";
|
||||
import { PageMeta } from "$sb/types.ts";
|
||||
import { editor, events, markdown, space } from "$sb/syscalls.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";
|
||||
import { buildHandebarOptions } from "./util.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 and are not expliclty disabled
|
||||
filter: ["and", ["attr", "trigger"], ["!=", ["attr", "enabled"], [
|
||||
"boolean",
|
||||
false,
|
||||
]]],
|
||||
}, 5);
|
||||
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",
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { LintEvent } from "$sb/app_event.ts";
|
||||
import { LintDiagnostic } from "$sb/types.ts";
|
||||
import { findNodeOfType } from "$sb/lib/tree.ts";
|
||||
import { FrontmatterConfig } from "./types.ts";
|
||||
import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
|
||||
|
||||
export async function lintTemplateFrontmatter(
|
||||
{ tree }: LintEvent,
|
||||
): Promise<LintDiagnostic[]> {
|
||||
const diagnostics: LintDiagnostic[] = [];
|
||||
const frontmatter = await extractFrontmatter(tree);
|
||||
|
||||
// Just looking this up again for the purposes of error reporting
|
||||
const frontmatterNode = findNodeOfType(tree, "FrontMatterCode")!;
|
||||
if (!frontmatter.tags?.includes("template")) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
// Just parse to make sure it's valid
|
||||
FrontmatterConfig.parse(frontmatter);
|
||||
} catch (e: any) {
|
||||
if (e.message.startsWith("[")) { // We got a zod error
|
||||
const zodErrors = JSON.parse(e.message);
|
||||
for (const zodError of zodErrors) {
|
||||
console.log("Zod validation error", zodError);
|
||||
diagnostics.push({
|
||||
from: frontmatterNode.from!,
|
||||
to: frontmatterNode.to!,
|
||||
message: `Attribute ${zodError.path.join(".")}: ${zodError.message}`,
|
||||
severity: "error",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
diagnostics.push({
|
||||
from: frontmatterNode.from!,
|
||||
to: frontmatterNode.to!,
|
||||
message: e.message,
|
||||
severity: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
return diagnostics;
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
import { editor, handlebars, space } from "$sb/syscalls.ts";
|
||||
import { PageMeta } from "$sb/types.ts";
|
||||
import { getObjectByRef, queryObjects } from "../index/plug_api.ts";
|
||||
import { FrontmatterConfig, TemplateObject } from "./types.ts";
|
||||
import { renderTemplate } from "./api.ts";
|
||||
|
||||
export async function newPageCommand(
|
||||
_cmdDef: any,
|
||||
templateName?: string,
|
||||
askName = true,
|
||||
) {
|
||||
if (!templateName) {
|
||||
const allPageTemplates = await listPageTemplates();
|
||||
// console.log("All page templates", allPageTemplates);
|
||||
const selectedTemplate = await selectPageTemplate(allPageTemplates);
|
||||
|
||||
if (!selectedTemplate) {
|
||||
return;
|
||||
}
|
||||
templateName = selectedTemplate.ref;
|
||||
}
|
||||
console.log("Selected template", templateName);
|
||||
|
||||
await instantiatePageTemplate(templateName!, undefined, askName);
|
||||
}
|
||||
|
||||
function listPageTemplates() {
|
||||
return queryObjects<TemplateObject>("template", {
|
||||
// where hooks.newPage exists
|
||||
filter: ["attr", ["attr", "hooks"], "newPage"],
|
||||
});
|
||||
}
|
||||
|
||||
// Invoked when a new page is created
|
||||
export async function newPage(pageName: string) {
|
||||
console.log("Asked to setup a new page for", pageName);
|
||||
const allPageTemplatesMatchingPrefix = (await listPageTemplates()).filter(
|
||||
(templateObject) => {
|
||||
const forPrefix = templateObject.hooks?.newPage?.forPrefix;
|
||||
return forPrefix && pageName.startsWith(forPrefix);
|
||||
},
|
||||
);
|
||||
// console.log("Matching templates", allPageTemplatesMatchingPrefix);
|
||||
if (allPageTemplatesMatchingPrefix.length === 0) {
|
||||
// No matching templates, that's ok, we'll just start with an empty page, so let's just return
|
||||
return;
|
||||
}
|
||||
if (allPageTemplatesMatchingPrefix.length === 1) {
|
||||
// Only one matching template, let's use it
|
||||
await instantiatePageTemplate(
|
||||
allPageTemplatesMatchingPrefix[0].ref,
|
||||
pageName,
|
||||
false,
|
||||
);
|
||||
} else {
|
||||
// Let's offer a choice
|
||||
const selectedTemplate = await selectPageTemplate(
|
||||
allPageTemplatesMatchingPrefix,
|
||||
);
|
||||
|
||||
if (!selectedTemplate) {
|
||||
// No choice made? We'll start out empty
|
||||
return;
|
||||
}
|
||||
|
||||
await instantiatePageTemplate(
|
||||
selectedTemplate.ref,
|
||||
pageName,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function selectPageTemplate(options: TemplateObject[]) {
|
||||
return editor.filterBox(
|
||||
"Page template",
|
||||
options.map((templateObj) => {
|
||||
const niceName = templateObj.ref.split("/").pop()!;
|
||||
return {
|
||||
...templateObj,
|
||||
description: templateObj.description || templateObj.ref,
|
||||
name: templateObj.displayName || niceName,
|
||||
};
|
||||
}),
|
||||
`Select the template to create a new page from`,
|
||||
);
|
||||
}
|
||||
|
||||
async function instantiatePageTemplate(
|
||||
templateName: string,
|
||||
intoCurrentPage: string | undefined,
|
||||
askName: boolean,
|
||||
) {
|
||||
const templateText = await space.readPage(templateName!);
|
||||
|
||||
console.log(
|
||||
"Instantiating page template",
|
||||
templateName,
|
||||
intoCurrentPage,
|
||||
askName,
|
||||
);
|
||||
|
||||
const tempPageMeta: PageMeta = {
|
||||
tag: "page",
|
||||
ref: "",
|
||||
name: "",
|
||||
created: "",
|
||||
lastModified: "",
|
||||
perm: "rw",
|
||||
};
|
||||
// Just used to extract the frontmatter
|
||||
const { frontmatter } = await renderTemplate(
|
||||
templateText,
|
||||
tempPageMeta,
|
||||
);
|
||||
|
||||
let frontmatterConfig: FrontmatterConfig;
|
||||
try {
|
||||
frontmatterConfig = FrontmatterConfig.parse(frontmatter!);
|
||||
} catch (e: any) {
|
||||
await editor.flashNotification(
|
||||
`Error parsing template frontmatter for ${templateName}: ${e.message}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
const newPageConfig = frontmatterConfig.hooks!.newPage!;
|
||||
|
||||
let pageName: string | undefined = intoCurrentPage ||
|
||||
await replaceTemplateVars(
|
||||
newPageConfig.suggestedName || "",
|
||||
tempPageMeta,
|
||||
);
|
||||
|
||||
if (!intoCurrentPage && askName && newPageConfig.confirmName !== false) {
|
||||
pageName = await editor.prompt(
|
||||
"Name of new page",
|
||||
await replaceTemplateVars(
|
||||
newPageConfig.suggestedName || "",
|
||||
tempPageMeta,
|
||||
),
|
||||
);
|
||||
if (!pageName) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
tempPageMeta.name = pageName;
|
||||
|
||||
if (!intoCurrentPage) {
|
||||
// Check if page exists, but only if we're not forcing the name (which only happens when we know that we're creating a new page already)
|
||||
try {
|
||||
// Fails if doesn't exist
|
||||
await space.getPageMeta(pageName);
|
||||
|
||||
// So, page exists
|
||||
if (newPageConfig.openIfExists) {
|
||||
console.log("Page already exists, navigating there");
|
||||
await editor.navigate(pageName);
|
||||
return;
|
||||
}
|
||||
|
||||
// let's warn
|
||||
if (
|
||||
!await editor.confirm(
|
||||
`Page ${pageName} already exists, are you sure you want to override it?`,
|
||||
)
|
||||
) {
|
||||
// Just navigate there without instantiating
|
||||
return editor.navigate(pageName);
|
||||
}
|
||||
} catch {
|
||||
// The preferred scenario, let's keep going
|
||||
}
|
||||
}
|
||||
|
||||
const { text: pageText, renderedFrontmatter } = await renderTemplate(
|
||||
templateText,
|
||||
tempPageMeta,
|
||||
);
|
||||
let fullPageText = renderedFrontmatter
|
||||
? "---\n" + renderedFrontmatter + "---\n" + pageText
|
||||
: pageText;
|
||||
const carretPos = fullPageText.indexOf("|^|");
|
||||
fullPageText = fullPageText.replace("|^|", "");
|
||||
if (intoCurrentPage) {
|
||||
await editor.insertAtCursor(fullPageText);
|
||||
if (carretPos !== -1) {
|
||||
await editor.moveCursor(carretPos);
|
||||
}
|
||||
} else {
|
||||
await space.writePage(
|
||||
pageName,
|
||||
fullPageText,
|
||||
);
|
||||
await editor.navigate(pageName, carretPos !== -1 ? carretPos : undefined);
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadPageObject(pageName?: string): Promise<PageMeta> {
|
||||
if (!pageName) {
|
||||
return {
|
||||
ref: "",
|
||||
name: "",
|
||||
tags: ["page"],
|
||||
lastModified: "",
|
||||
created: "",
|
||||
} as PageMeta;
|
||||
}
|
||||
return (await getObjectByRef<PageMeta>(
|
||||
pageName,
|
||||
"page",
|
||||
pageName,
|
||||
)) || {
|
||||
ref: pageName,
|
||||
name: pageName,
|
||||
tags: ["page"],
|
||||
lastModified: "",
|
||||
created: "",
|
||||
} as PageMeta;
|
||||
}
|
||||
|
||||
export function replaceTemplateVars(
|
||||
s: string,
|
||||
pageMeta: PageMeta,
|
||||
): Promise<string> {
|
||||
return handlebars.renderTemplate(s, {}, { page: pageMeta });
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import { CompleteEvent, SlashCompletion } from "$sb/app_event.ts";
|
||||
import { editor, markdown, space } from "$sb/syscalls.ts";
|
||||
import type { AttributeCompletion } from "../index/attributes.ts";
|
||||
import { queryObjects } from "../index/plug_api.ts";
|
||||
import { TemplateObject } from "./types.ts";
|
||||
import { loadPageObject } from "./page.ts";
|
||||
import { renderTemplate } from "./api.ts";
|
||||
import { prepareFrontmatterDispatch } from "$sb/lib/frontmatter.ts";
|
||||
import { SnippetConfig } from "./types.ts";
|
||||
import { snippet } from "@codemirror/autocomplete";
|
||||
|
||||
export async function snippetSlashComplete(
|
||||
completeEvent: CompleteEvent,
|
||||
): Promise<SlashCompletion[]> {
|
||||
const allTemplates = await queryObjects<TemplateObject>("template", {
|
||||
// where hooks.snippet.slashCommand exists
|
||||
filter: ["attr", ["attr", ["attr", "hooks"], "snippet"], "slashCommand"],
|
||||
}, 5);
|
||||
return allTemplates.map((template) => {
|
||||
const snippetTemplate = template.hooks!.snippet!;
|
||||
|
||||
return {
|
||||
label: snippetTemplate.slashCommand,
|
||||
detail: template.description,
|
||||
templatePage: template.ref,
|
||||
pageName: completeEvent.pageName,
|
||||
invoke: "template.insertSnippetTemplate",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function insertSnippetTemplate(slashCompletion: SlashCompletion) {
|
||||
const pageObject = await loadPageObject(
|
||||
slashCompletion.pageName,
|
||||
);
|
||||
|
||||
const templateText = await space.readPage(slashCompletion.templatePage);
|
||||
let { renderedFrontmatter, text: replacementText, frontmatter } =
|
||||
await renderTemplate(
|
||||
templateText,
|
||||
pageObject,
|
||||
);
|
||||
let snippetTemplate: SnippetConfig;
|
||||
try {
|
||||
snippetTemplate = SnippetConfig.parse(frontmatter.hooks!.snippet!);
|
||||
} catch (e: any) {
|
||||
console.error(
|
||||
`Invalid template configuration for ${slashCompletion.templatePage}:`,
|
||||
e.message,
|
||||
);
|
||||
await editor.flashNotification(
|
||||
`Invalid template configuration for ${slashCompletion.templatePage}, won't insert snippet`,
|
||||
"error",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
// update cursor position
|
||||
cursorPos = await editor.getCursor();
|
||||
}
|
||||
|
||||
if (snippetTemplate.insertAt) {
|
||||
switch (snippetTemplate.insertAt) {
|
||||
case "page-start":
|
||||
await editor.moveCursor(0);
|
||||
break;
|
||||
case "page-end":
|
||||
await editor.moveCursor((await editor.getText()).length);
|
||||
break;
|
||||
case "line-start": {
|
||||
const pageText = await editor.getText();
|
||||
let startOfLine = cursorPos;
|
||||
while (startOfLine > 0 && pageText[startOfLine - 1] !== "\n") {
|
||||
startOfLine--;
|
||||
}
|
||||
await editor.moveCursor(startOfLine);
|
||||
break;
|
||||
}
|
||||
case "line-end": {
|
||||
const pageText = await editor.getText();
|
||||
let endOfLine = cursorPos;
|
||||
while (endOfLine < pageText.length && pageText[endOfLine] !== "\n") {
|
||||
endOfLine++;
|
||||
}
|
||||
await editor.moveCursor(endOfLine);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// Deliberate no-op
|
||||
}
|
||||
}
|
||||
|
||||
cursorPos = await editor.getCursor();
|
||||
|
||||
if (snippetTemplate.matchRegex) {
|
||||
const pageText = await editor.getText();
|
||||
// Regex matching mode
|
||||
const matchRegex = new RegExp(snippetTemplate.matchRegex);
|
||||
|
||||
let startOfLine = cursorPos;
|
||||
while (startOfLine > 0 && pageText[startOfLine - 1] !== "\n") {
|
||||
startOfLine--;
|
||||
}
|
||||
let currentLine = pageText.slice(startOfLine, cursorPos);
|
||||
const emptyLine = !currentLine;
|
||||
currentLine = currentLine.replace(matchRegex, replacementText);
|
||||
|
||||
await editor.dispatch({
|
||||
changes: {
|
||||
from: startOfLine,
|
||||
to: cursorPos,
|
||||
insert: currentLine,
|
||||
},
|
||||
selection: emptyLine
|
||||
? {
|
||||
anchor: startOfLine + currentLine.length,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
} else {
|
||||
const carretPos = replacementText.indexOf("|^|");
|
||||
replacementText = replacementText.replace("|^|", "");
|
||||
await editor.insertAtCursor(replacementText);
|
||||
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",
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -3,135 +3,51 @@ functions:
|
||||
# API
|
||||
renderTemplate:
|
||||
path: api.ts:renderTemplate
|
||||
|
||||
cleanTemplate:
|
||||
path: api.ts:cleanTemplate
|
||||
|
||||
# Used by various slash commands
|
||||
insertTemplateText:
|
||||
path: template.ts:insertTemplateText
|
||||
|
||||
# Indexing
|
||||
indexTemplate:
|
||||
path: ./index.ts:indexTemplate
|
||||
path: index.ts:indexTemplate
|
||||
events:
|
||||
# Special event only triggered for template pages
|
||||
- page:indexTemplate
|
||||
|
||||
# Completion
|
||||
templateSlashCommand:
|
||||
path: ./complete.ts:templateSlashComplete
|
||||
path: snippet.ts:snippetSlashComplete
|
||||
events:
|
||||
- slash:complete
|
||||
|
||||
insertSlashTemplate:
|
||||
path: ./complete.ts:insertSlashTemplate
|
||||
insertSnippetTemplate:
|
||||
path: snippet.ts:insertSnippetTemplate
|
||||
|
||||
handlebarHelperComplete:
|
||||
path: ./complete.ts:templateVariableComplete
|
||||
path: var.ts:templateVariableComplete
|
||||
events:
|
||||
- editor:complete
|
||||
|
||||
applyLineReplace:
|
||||
path: ./template.ts:applyLineReplace
|
||||
insertFrontMatter:
|
||||
redirect: insertTemplateText
|
||||
slashCommand:
|
||||
name: frontmatter
|
||||
description: Insert page frontmatter
|
||||
value: |
|
||||
---
|
||||
|^|
|
||||
---
|
||||
makeH1:
|
||||
redirect: applyLineReplace
|
||||
slashCommand:
|
||||
name: h1
|
||||
description: Turn line into h1 header
|
||||
match: "^#*\\s*"
|
||||
replace: "# "
|
||||
makeH2:
|
||||
redirect: applyLineReplace
|
||||
slashCommand:
|
||||
name: h2
|
||||
description: Turn line into h2 header
|
||||
match: "^#*\\s*"
|
||||
replace: "## "
|
||||
makeH3:
|
||||
redirect: applyLineReplace
|
||||
slashCommand:
|
||||
name: h3
|
||||
description: Turn line into h3 header
|
||||
match: "^#*\\s*"
|
||||
replace: "### "
|
||||
makeH4:
|
||||
redirect: applyLineReplace
|
||||
slashCommand:
|
||||
name: h4
|
||||
description: Turn line into h4 header
|
||||
match: "^#*\\s*"
|
||||
replace: "#### "
|
||||
insertCodeBlock:
|
||||
redirect: insertTemplateText
|
||||
slashCommand:
|
||||
name: code
|
||||
description: Insert fenced code block
|
||||
value: |
|
||||
```|^|
|
||||
|
||||
```
|
||||
# Widget
|
||||
templateWidget: # Legacy
|
||||
path: template_block.ts:widget
|
||||
codeWidget: template
|
||||
renderMode: markdown
|
||||
|
||||
insertHRTemplate:
|
||||
redirect: insertTemplateText
|
||||
slashCommand:
|
||||
name: hr
|
||||
description: Insert a horizontal rule
|
||||
value: "---"
|
||||
|
||||
insertTable:
|
||||
redirect: insertTemplateText
|
||||
slashCommand:
|
||||
name: table
|
||||
description: Insert a table
|
||||
boost: -1 # Low boost because it's likely not very commonly used
|
||||
value: |
|
||||
| Header A | Header B |
|
||||
|----------|----------|
|
||||
| Cell A|^| | Cell B |
|
||||
|
||||
quickNoteCommand:
|
||||
path: ./template.ts:quickNoteCommand
|
||||
command:
|
||||
name: "Quick Note"
|
||||
key: "Alt-Shift-n"
|
||||
priority: 3
|
||||
|
||||
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"
|
||||
# API invoked when a new page is created
|
||||
newPage:
|
||||
path: page.ts:newPage
|
||||
|
||||
# Commands
|
||||
newPageCommand:
|
||||
path: ./template.ts:newPageCommand
|
||||
path: page.ts:newPageCommand
|
||||
command:
|
||||
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:
|
||||
name: tomorrow
|
||||
description: Insert tomorrow's date
|
||||
value: "{{tomorrow}}"
|
||||
# Lint
|
||||
lintTemplateFrontmatter:
|
||||
path: lint.ts:lintTemplateFrontmatter
|
||||
events:
|
||||
- editor:lint
|
||||
|
||||
@@ -1,274 +0,0 @@
|
||||
import { editor, handlebars, space } from "$sb/syscalls.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 { getObjectByRef, queryObjects } from "../index/plug_api.ts";
|
||||
import { TemplateObject } from "./types.ts";
|
||||
import { renderTemplate } from "./api.ts";
|
||||
|
||||
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')`,
|
||||
);
|
||||
|
||||
if (!selectedTemplate) {
|
||||
return;
|
||||
}
|
||||
templateName = selectedTemplate.ref;
|
||||
}
|
||||
console.log("Selected template", templateName);
|
||||
|
||||
const templateText = await space.readPage(templateName!);
|
||||
|
||||
const tempPageMeta: PageMeta = {
|
||||
tag: "page",
|
||||
ref: "",
|
||||
name: "",
|
||||
created: "",
|
||||
lastModified: "",
|
||||
perm: "rw",
|
||||
};
|
||||
// Just used to extract the frontmatter
|
||||
const { frontmatter } = await renderTemplate(
|
||||
templateText,
|
||||
tempPageMeta,
|
||||
);
|
||||
|
||||
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;
|
||||
|
||||
try {
|
||||
// Fails if doesn't exist
|
||||
await space.getPageMeta(pageName);
|
||||
|
||||
// So, page exists, let's warn
|
||||
if (
|
||||
!await editor.confirm(
|
||||
`Page ${pageName} already exists, are you sure you want to override it?`,
|
||||
)
|
||||
) {
|
||||
// Just navigate there without instantiating
|
||||
return editor.navigate(pageName);
|
||||
}
|
||||
} catch {
|
||||
// The preferred scenario, let's keep going
|
||||
}
|
||||
|
||||
const { text: pageText, renderedFrontmatter } = await renderTemplate(
|
||||
templateText,
|
||||
tempPageMeta,
|
||||
);
|
||||
let fullPageText = renderedFrontmatter
|
||||
? "---\n" + renderedFrontmatter + "---\n" + pageText
|
||||
: pageText;
|
||||
const carretPos = fullPageText.indexOf("|^|");
|
||||
fullPageText = fullPageText.replace("|^|", "");
|
||||
await space.writePage(
|
||||
pageName,
|
||||
fullPageText,
|
||||
);
|
||||
await editor.navigate(pageName, carretPos !== -1 ? carretPos : undefined);
|
||||
}
|
||||
|
||||
export async function loadPageObject(pageName?: string): Promise<PageMeta> {
|
||||
if (!pageName) {
|
||||
return {
|
||||
ref: "",
|
||||
name: "",
|
||||
tags: ["page"],
|
||||
lastModified: "",
|
||||
created: "",
|
||||
} as PageMeta;
|
||||
}
|
||||
return (await getObjectByRef<PageMeta>(
|
||||
pageName,
|
||||
"page",
|
||||
pageName,
|
||||
)) || {
|
||||
ref: pageName,
|
||||
name: pageName,
|
||||
tags: ["page"],
|
||||
lastModified: "",
|
||||
created: "",
|
||||
} as PageMeta;
|
||||
}
|
||||
|
||||
export function replaceTemplateVars(
|
||||
s: string,
|
||||
pageMeta: PageMeta,
|
||||
): Promise<string> {
|
||||
return handlebars.renderTemplate(s, {}, { page: pageMeta });
|
||||
}
|
||||
|
||||
export async function quickNoteCommand() {
|
||||
const { quickNotePrefix } = await readSettings({
|
||||
quickNotePrefix: "📥 ",
|
||||
});
|
||||
const date = niceDate(new Date());
|
||||
const time = niceTime(new Date());
|
||||
const pageName = `${quickNotePrefix}${date} ${time}`;
|
||||
await editor.navigate(pageName);
|
||||
}
|
||||
|
||||
export async function dailyNoteCommand() {
|
||||
const { dailyNoteTemplate, dailyNotePrefix } = await readSettings({
|
||||
dailyNoteTemplate: "[[template/page/Daily Note]]",
|
||||
dailyNotePrefix: "📅 ",
|
||||
});
|
||||
const date = niceDate(new Date());
|
||||
const pageName = `${dailyNotePrefix}${date}`;
|
||||
let carretPos = 0;
|
||||
|
||||
try {
|
||||
await space.getPageMeta(pageName);
|
||||
} catch {
|
||||
// Doesn't exist, let's create
|
||||
let dailyNoteTemplateText = "";
|
||||
try {
|
||||
dailyNoteTemplateText = await space.readPage(
|
||||
cleanPageRef(dailyNoteTemplate),
|
||||
);
|
||||
carretPos = dailyNoteTemplateText.indexOf("|^|");
|
||||
if (carretPos === -1) {
|
||||
carretPos = 0;
|
||||
}
|
||||
dailyNoteTemplateText = dailyNoteTemplateText.replace("|^|", "");
|
||||
} catch {
|
||||
console.warn(`No daily note template found at ${dailyNoteTemplate}`);
|
||||
}
|
||||
|
||||
await space.writePage(
|
||||
pageName,
|
||||
await replaceTemplateVars(dailyNoteTemplateText, {
|
||||
tag: "page",
|
||||
ref: pageName,
|
||||
name: pageName,
|
||||
created: "",
|
||||
lastModified: "",
|
||||
perm: "rw",
|
||||
}),
|
||||
);
|
||||
}
|
||||
await editor.navigate(pageName, carretPos);
|
||||
}
|
||||
|
||||
function getWeekStartDate(monday = false) {
|
||||
const d = new Date();
|
||||
const day = d.getDay();
|
||||
let diff = d.getDate() - day;
|
||||
if (monday) {
|
||||
diff += day == 0 ? -6 : 1;
|
||||
}
|
||||
return new Date(d.setDate(diff));
|
||||
}
|
||||
|
||||
export async function weeklyNoteCommand() {
|
||||
const { weeklyNoteTemplate, weeklyNotePrefix, weeklyNoteMonday } =
|
||||
await readSettings({
|
||||
weeklyNoteTemplate: "[[template/page/Weekly Note]]",
|
||||
weeklyNotePrefix: "🗓️ ",
|
||||
weeklyNoteMonday: false,
|
||||
});
|
||||
let weeklyNoteTemplateText = "";
|
||||
try {
|
||||
weeklyNoteTemplateText = await space.readPage(
|
||||
cleanPageRef(weeklyNoteTemplate),
|
||||
);
|
||||
} catch {
|
||||
console.warn(`No weekly note template found at ${weeklyNoteTemplate}`);
|
||||
}
|
||||
const date = niceDate(getWeekStartDate(weeklyNoteMonday));
|
||||
const pageName = `${weeklyNotePrefix}${date}`;
|
||||
if (weeklyNoteTemplateText) {
|
||||
try {
|
||||
await space.getPageMeta(pageName);
|
||||
} catch {
|
||||
// Doesn't exist, let's create
|
||||
await space.writePage(
|
||||
pageName,
|
||||
await replaceTemplateVars(weeklyNoteTemplateText, {
|
||||
name: pageName,
|
||||
ref: pageName,
|
||||
tag: "page",
|
||||
created: "",
|
||||
lastModified: "",
|
||||
perm: "rw",
|
||||
}),
|
||||
);
|
||||
}
|
||||
await editor.navigate(pageName);
|
||||
} else {
|
||||
await editor.navigate(pageName);
|
||||
}
|
||||
}
|
||||
|
||||
export async function insertTemplateText(cmdDef: any) {
|
||||
const cursorPos = await editor.getCursor();
|
||||
const page = await editor.getCurrentPage();
|
||||
const pageMeta = await loadPageObject(page);
|
||||
let templateText: string = cmdDef.value;
|
||||
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 applyLineReplace(cmdDef: any) {
|
||||
const cursorPos = await editor.getCursor();
|
||||
const text = await editor.getText();
|
||||
const matchRegex = new RegExp(cmdDef.match);
|
||||
let startOfLine = cursorPos;
|
||||
while (startOfLine > 0 && text[startOfLine - 1] !== "\n") {
|
||||
startOfLine--;
|
||||
}
|
||||
let currentLine = text.slice(startOfLine, cursorPos);
|
||||
|
||||
const emptyLine = !currentLine;
|
||||
|
||||
currentLine = currentLine.replace(matchRegex, cmdDef.replace);
|
||||
|
||||
await editor.dispatch({
|
||||
changes: {
|
||||
from: startOfLine,
|
||||
to: cursorPos,
|
||||
insert: currentLine,
|
||||
},
|
||||
selection: emptyLine
|
||||
? {
|
||||
anchor: startOfLine + currentLine.length,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { markdown, space, YAML } from "$sb/syscalls.ts";
|
||||
import { loadPageObject, replaceTemplateVars } from "../template/template.ts";
|
||||
import { loadPageObject, replaceTemplateVars } from "./page.ts";
|
||||
import { CodeWidgetContent, PageMeta } from "$sb/types.ts";
|
||||
import { renderTemplate } from "../template/plug_api.ts";
|
||||
import { renderTemplate } from "./plug_api.ts";
|
||||
import { renderToText } from "$sb/lib/tree.ts";
|
||||
import { rewritePageRefs, rewritePageRefsInString } from "$sb/lib/resolve.ts";
|
||||
import { performQuery } from "./query.ts";
|
||||
import { performQuery } from "../query/widget.ts";
|
||||
import { parseQuery } from "$sb/lib/parse-query.ts";
|
||||
|
||||
type TemplateConfig = {
|
||||
type TemplateWidgetConfig = {
|
||||
// Pull the template from a page
|
||||
page?: string;
|
||||
// Or use a string directly
|
||||
@@ -29,7 +29,7 @@ export async function widget(
|
||||
const pageMeta: PageMeta = await loadPageObject(pageName);
|
||||
|
||||
try {
|
||||
const config: TemplateConfig = await YAML.parse(bodyText);
|
||||
const config: TemplateWidgetConfig = await YAML.parse(bodyText);
|
||||
let templateText = config.template || "";
|
||||
let templatePage = config.page;
|
||||
if (templatePage) {
|
||||
@@ -41,7 +41,13 @@ export async function widget(
|
||||
if (!templatePage) {
|
||||
throw new Error("No template page specified");
|
||||
}
|
||||
templateText = await space.readPage(templatePage);
|
||||
try {
|
||||
templateText = await space.readPage(templatePage);
|
||||
} catch (e: any) {
|
||||
if (e.message === "Not found") {
|
||||
throw new Error(`Template page ${templatePage} not found`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let value: any;
|
||||
+94
-11
@@ -1,17 +1,100 @@
|
||||
import { ObjectValue } from "$sb/types.ts";
|
||||
import { z, ZodEffects } from "zod";
|
||||
|
||||
export type TemplateFrontmatter = {
|
||||
displayName?: string;
|
||||
type?: "page";
|
||||
export const CommandConfig = z.object({
|
||||
command: z.string().optional(),
|
||||
key: z.string().optional(),
|
||||
mac: z.string().optional(),
|
||||
});
|
||||
|
||||
export type CommandConfig = z.infer<typeof CommandConfig>;
|
||||
|
||||
/**
|
||||
* Used for creating new pages using {[Page: From Template]} command
|
||||
*/
|
||||
export const NewPageConfig = refineCommand(
|
||||
z.object({
|
||||
// Suggested name for the new page, can use template placeholders
|
||||
suggestedName: z.string().optional(),
|
||||
// Suggest (or auto use) this template for a specific prefix
|
||||
forPrefix: z.string().optional(),
|
||||
// Confirm the name before creating
|
||||
confirmName: z.boolean().optional(),
|
||||
// If the page already exists, open it instead of creating a new one
|
||||
openIfExists: z.boolean().optional(),
|
||||
}).strict().merge(CommandConfig),
|
||||
);
|
||||
|
||||
export type NewPageConfig = z.infer<typeof NewPageConfig>;
|
||||
|
||||
/**
|
||||
* Represents a snippet
|
||||
*/
|
||||
|
||||
export const SnippetConfig = refineCommand(
|
||||
z.object({
|
||||
slashCommand: z.string(), // trigger
|
||||
// Regex match to apply (implicitly makes the body the regex replacement)
|
||||
matchRegex: z.string().optional(),
|
||||
insertAt: z.enum([
|
||||
"cursor",
|
||||
"line-start",
|
||||
"line-end",
|
||||
"page-start",
|
||||
"page-end",
|
||||
]).optional(), // defaults to cursor
|
||||
}).strict().merge(CommandConfig),
|
||||
);
|
||||
|
||||
/**
|
||||
* Ensures that 'command' is present if either 'key' or 'mac' is present for a particular object
|
||||
* @param o object to 'refine' with this constraint
|
||||
* @returns
|
||||
*/
|
||||
function refineCommand<T extends typeof CommandConfig>(o: T): ZodEffects<T> {
|
||||
return o.refine((data) => {
|
||||
// Check if either 'key' or 'mac' is present
|
||||
const hasKeyOrMac = data.key !== undefined || data.mac !== undefined;
|
||||
// Ensure 'command' is present if either 'key' or 'mac' is present
|
||||
return !hasKeyOrMac || data.command !== undefined;
|
||||
}, {
|
||||
message:
|
||||
"Attribute 'command' is required when specifying a key binding via 'key' and/or 'mac'.",
|
||||
});
|
||||
}
|
||||
|
||||
export type SnippetConfig = z.infer<typeof SnippetConfig>;
|
||||
|
||||
export const WidgetConfig = z.object({
|
||||
where: z.string(),
|
||||
priority: z.number().optional(),
|
||||
});
|
||||
|
||||
export type WidgetConfig = z.infer<typeof WidgetConfig>;
|
||||
|
||||
export const HooksConfig = z.object({
|
||||
top: WidgetConfig.optional(),
|
||||
bottom: WidgetConfig.optional(),
|
||||
newPage: NewPageConfig.optional(),
|
||||
snippet: SnippetConfig.optional(),
|
||||
}).strict();
|
||||
|
||||
export type HooksConfig = z.infer<typeof HooksConfig>;
|
||||
|
||||
export const FrontmatterConfig = z.object({
|
||||
// Used for matching in page navigator
|
||||
displayName: z.string().optional(),
|
||||
tags: z.union([z.string(), z.array(z.string())]).optional(),
|
||||
|
||||
// For use in the template selector slash commands and other avenues
|
||||
|
||||
description: z.string().optional(),
|
||||
// Frontmatter can be encoded as an object (in which case we'll serialize it) or as a string
|
||||
frontmatter?: Record<string, any> | string;
|
||||
frontmatter: z.union([z.record(z.unknown()), z.string()]).optional(),
|
||||
|
||||
// Specific for slash templates
|
||||
trigger?: string;
|
||||
hooks: HooksConfig.optional(),
|
||||
});
|
||||
|
||||
// Specific for frontmatter templates
|
||||
where?: string; // expression (SB query style)
|
||||
priority?: number; // When multiple templates match, the one with the highest priority is used
|
||||
};
|
||||
export type FrontmatterConfig = z.infer<typeof FrontmatterConfig>;
|
||||
|
||||
export type TemplateObject = ObjectValue<TemplateFrontmatter>;
|
||||
export type TemplateObject = ObjectValue<FrontmatterConfig>;
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { CompleteEvent } from "$sb/app_event.ts";
|
||||
import { PageMeta } from "$sb/types.ts";
|
||||
import { events } from "$sb/syscalls.ts";
|
||||
import { buildHandebarOptions } from "./util.ts";
|
||||
import {
|
||||
AttributeCompleteEvent,
|
||||
AttributeCompletion,
|
||||
} from "../index/attributes.ts";
|
||||
import { attributeCompletionsToCMCompletion } from "./snippet.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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user