1
0

Fixes #529 by removing directives (#613)

* Fixes #529 by removing directives
* Load builtin tags on space reindex
This commit is contained in:
Zef Hemel 2024-01-02 14:47:02 +01:00 committed by GitHub
parent 5f4e584e46
commit 8a2e081672
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 159 additions and 1445 deletions

View File

@ -25,7 +25,7 @@ import {
xmlLanguage,
yamlLanguage,
} from "./deps.ts";
import { highlightingDirectiveParser } from "./markdown_parser/parser.ts";
import { highlightingQueryParser } from "./markdown_parser/parser.ts";
export const builtinLanguages: Record<string, Language> = {
"meta": StreamLanguage.define(yamlLanguage),
@ -79,7 +79,7 @@ export const builtinLanguages: Record<string, Language> = {
"dart": StreamLanguage.define(dartLanguage),
"query": LRLanguage.define({
name: "query",
parser: highlightingDirectiveParser,
parser: highlightingQueryParser,
}),
};

View File

@ -13,11 +13,6 @@ export const OrderedList = Tag.define();
export const Highlight = Tag.define();
export const HorizontalRuleTag = Tag.define();
export const DirectiveTag = Tag.define();
export const DirectiveStartTag = Tag.define();
export const DirectiveEndTag = Tag.define();
export const DirectiveProgramTag = Tag.define();
export const AttributeTag = Tag.define();
export const AttributeNameTag = Tag.define();
export const AttributeValueTag = Tag.define();

View File

@ -51,28 +51,6 @@ Deno.test("Test parser", () => {
assertEquals(node, undefined);
});
const directiveSample = `
Before
<!-- #query page -->
Body line 1
Body line 2
<!-- /query -->
End
`;
const nestedDirectiveExample = `
Before
<!-- #query page -->
1
<!-- #eval 10 * 10 -->
100
<!-- /eval -->
3
<!-- /query -->
End
`;
const inlineAttributeSample = `
Hello there [a link](http://zef.plus)
[age: 100]
@ -152,9 +130,9 @@ Deno.test("Test command link arguments", () => {
const commands = collectNodesOfType(tree, "CommandLink");
assertEquals(commands.length, 2);
const args1 = findNodeOfType(commands[0], "CommandLinkArgs")
const args1 = findNodeOfType(commands[0], "CommandLinkArgs");
assertEquals(args1!.children![0].text, '"with", "args"');
const args2 = findNodeOfType(commands[1], "CommandLinkArgs")
const args2 = findNodeOfType(commands[1], "CommandLinkArgs");
assertEquals(args2!.children![0].text, '"other", "args", 123');
});
});

View File

@ -160,6 +160,21 @@ export const Highlight: MarkdownConfig = {
],
};
import { parser as queryParser } from "./parse-query.js";
export const highlightingQueryParser = queryParser.configure({
props: [
styleTags({
"Name": t.variableName,
"String": t.string,
"Number": t.number,
"PageRef": ct.WikiLinkTag,
"where limit select render Order OrderKW and or as InKW each all":
t.keyword,
}),
],
});
export const attributeStartRegex = /^\[([\w\$]+)(::?\s*)/;
export const Attribute: MarkdownConfig = {
@ -264,121 +279,8 @@ export const Comment: MarkdownConfig = {
],
};
// Directive parser
const directiveStart = /^\s*<!--\s*#([a-z]+)\s*(.*?)-->\s*/;
const directiveEnd = /^\s*<!--\s*\/(.*?)-->\s*/;
import { parser as directiveParser } from "./parse-query.js";
import { parser as expressionParser } from "./parse-expression.js";
import { Table } from "./table_parser.ts";
import { foldNodeProp } from "@codemirror/language";
import { lezerToParseTree } from "./parse_tree.ts";
export const highlightingDirectiveParser = directiveParser.configure({
props: [
styleTags({
"Name": t.variableName,
"String": t.string,
"Number": t.number,
"PageRef": ct.WikiLinkTag,
"where limit select render Order OrderKW and or as InKW each all":
t.keyword,
}),
],
});
export const Directive: MarkdownConfig = {
defineNodes: [
{ name: "Directive", block: true, style: ct.DirectiveTag },
{ name: "DirectiveStart", style: ct.DirectiveStartTag, block: true },
{ name: "DirectiveEnd", style: ct.DirectiveEndTag },
{ name: "DirectiveBody", block: true },
],
parseBlock: [{
name: "Directive",
parse: (cx, line: Line) => {
const match = directiveStart.exec(line.text);
if (!match) {
return false;
}
// console.log("Parsing directive", line.text);
const frontStart = cx.parsedPos;
const [fullMatch, directive, arg] = match;
const elts = [];
if (directive === "query") {
const queryParseTree = highlightingDirectiveParser.parse(arg);
elts.push(cx.elt(
"DirectiveStart",
cx.parsedPos,
cx.parsedPos + line.text.length + 1,
[cx.elt(queryParseTree, frontStart + fullMatch.indexOf(arg))],
));
} else if (directive === "eval") {
const expressionParseTree = expressionParser.parse(arg);
elts.push(cx.elt(
"DirectiveStart",
cx.parsedPos,
cx.parsedPos + line.text.length + 1,
[cx.elt(expressionParseTree, frontStart + fullMatch.indexOf(arg))],
));
} else {
elts.push(cx.elt(
"DirectiveStart",
cx.parsedPos,
cx.parsedPos + line.text.length + 1,
));
}
// console.log("Query parse tree", queryParseTree.topNode);
cx.nextLine();
const startPos = cx.parsedPos;
let endPos = startPos;
let text = "";
let lastPos = cx.parsedPos;
let nesting = 0;
while (true) {
if (directiveEnd.exec(line.text) && nesting === 0) {
break;
}
text += line.text + "\n";
endPos += line.text.length + 1;
if (directiveStart.exec(line.text)) {
nesting++;
}
if (directiveEnd.exec(line.text)) {
nesting--;
}
cx.nextLine();
if (cx.parsedPos === lastPos) {
// End of file, no progress made, there may be a better way to do this but :shrug:
return false;
}
lastPos = cx.parsedPos;
}
const directiveBodyTree = cx.parser.parse(text);
elts.push(
cx.elt("DirectiveBody", startPos, endPos, [
cx.elt(directiveBodyTree, startPos),
]),
);
endPos = cx.parsedPos + line.text.length;
elts.push(cx.elt(
"DirectiveEnd",
cx.parsedPos,
cx.parsedPos + line.text.length,
));
cx.nextLine();
cx.addElement(cx.elt("Directive", frontStart, endPos, elts));
return true;
},
before: "HTMLBlock",
}],
};
// FrontMatter parser
@ -450,7 +352,6 @@ export default function buildMarkdown(mdExtensions: MDExt[]): Language {
CommandLink,
Attribute,
FrontMatter,
Directive,
TaskList,
Comment,
Highlight,

View File

@ -1,4 +1,4 @@
import { removeDirectiveBody, SpaceSync, SyncStatusItem } from "./sync.ts";
import { SpaceSync, SyncStatusItem } from "./sync.ts";
import { DiskSpacePrimitives } from "./disk_space_primitives.ts";
import { assertEquals } from "../../test_deps.ts";
@ -117,23 +117,11 @@ Deno.test("Test store", async () => {
await primary.writeFile("index", stringToBytes("Hello 1"));
await secondary.writeFile("index", stringToBytes("Hello 1"));
// And two more files with different bodies, but only within a query directive — shouldn't conflict
await primary.writeFile(
"index.md",
stringToBytes(
"Hello\n<!-- #query page -->\nHello 1\n<!-- /query -->",
),
);
await secondary.writeFile(
"index.md",
stringToBytes("Hello\n<!-- #query page -->\nHello 2\n<!-- /query -->"),
);
await doSync();
await doSync();
// test + index + index.md + previous index.conflicting copy but nothing more
assertEquals((await primary.fetchFileList()).length, 4);
assertEquals((await primary.fetchFileList()).length, 3);
console.log("Bringing a third device in the mix");
@ -191,26 +179,6 @@ function sleep(ms = 10): Promise<void> {
});
}
Deno.test("Remove directive bodies", () => {
assertEquals(
removeDirectiveBody(`<!-- #query page -->
This is a body
bla bla bla
<!-- /query -->
Hello
<!-- #include [[test]] -->
This is a body
<!-- /include -->
`),
`<!-- #query page -->
<!-- /query -->
Hello
<!-- #include [[test]] -->
<!-- /include -->
`,
);
});
function stringToBytes(s: string): Uint8Array {
return new TextEncoder().encode(s);
}

View File

@ -1,6 +1,3 @@
import { renderToText, replaceNodesMatching } from "../../plug-api/lib/tree.ts";
import buildMarkdown from "../markdown_parser/parser.ts";
import { parse } from "../markdown_parser/parse_tree.ts";
import { SpacePrimitives } from "./space_primitives.ts";
import { EventEmitter } from "../../plugos/event.ts";
import { FileMeta } from "$sb/types.ts";
@ -305,56 +302,32 @@ export class SpaceSync extends EventEmitter<SyncEvents> {
const pageData1 = await primary.readFile(name);
const pageData2 = await secondary.readFile(name);
if (name.endsWith(".md")) {
console.log(
"[sync]",
"File is markdown, using smart conflict resolution",
);
// Let's use a smartert check for markdown files, ignoring directive bodies
const pageText1 = removeDirectiveBody(
new TextDecoder().decode(pageData1.data),
);
const pageText2 = removeDirectiveBody(
new TextDecoder().decode(pageData2.data),
);
if (pageText1 === pageText2) {
console.log(
"[sync]",
"Files are the same (eliminating the directive bodies), no conflict",
);
let byteWiseMatch = true;
const arrayBuffer1 = pageData1.data;
const arrayBuffer2 = pageData2.data;
if (arrayBuffer1.byteLength !== arrayBuffer2.byteLength) {
byteWiseMatch = false;
}
if (byteWiseMatch) {
// Byte-wise comparison
for (let i = 0; i < arrayBuffer1.byteLength; i++) {
if (arrayBuffer1[i] !== arrayBuffer2[i]) {
byteWiseMatch = false;
break;
}
}
// Byte wise they're still the same, so no confict
if (byteWiseMatch) {
console.log("[sync]", "Files are the same, no conflict");
snapshot.set(name, [
pageData1.meta.lastModified,
pageData2.meta.lastModified,
]);
return 0;
}
} else {
let byteWiseMatch = true;
const arrayBuffer1 = pageData1.data;
const arrayBuffer2 = pageData2.data;
if (arrayBuffer1.byteLength !== arrayBuffer2.byteLength) {
byteWiseMatch = false;
}
if (byteWiseMatch) {
// Byte-wise comparison
for (let i = 0; i < arrayBuffer1.byteLength; i++) {
if (arrayBuffer1[i] !== arrayBuffer2[i]) {
byteWiseMatch = false;
break;
}
}
// Byte wise they're still the same, so no confict
if (byteWiseMatch) {
console.log("[sync]", "Files are the same, no conflict");
snapshot.set(name, [
pageData1.meta.lastModified,
pageData2.meta.lastModified,
]);
return 0;
}
}
}
let operations = 0;
const revisionFileName = filePieces.length === 1
? `${name}.conflicted.${pageData2.meta.lastModified}`
@ -403,18 +376,3 @@ export class SpaceSync extends EventEmitter<SyncEvents> {
}
}
}
const markdownLanguage = buildMarkdown([]);
export function removeDirectiveBody(text: string): string {
// Parse
const tree = parse(markdownLanguage, text);
// Remove bodies
replaceNodesMatching(tree, (node) => {
if (node.type === "DirectiveBody") {
return null;
}
});
// Turn back into text
return renderToText(tree);
}

View File

@ -1,17 +1,16 @@
import { parse } from "../../common/markdown_parser/parse_tree.ts";
import buildMarkdown from "../../common/markdown_parser/parser.ts";
import { AST, findNodeOfType, parseTreeToAST } from "$sb/lib/tree.ts";
import { AST, parseTreeToAST } from "$sb/lib/tree.ts";
import { assertEquals } from "../../test_deps.ts";
import { astToKvQuery } from "$sb/lib/parse-query.ts";
const lang = buildMarkdown([]);
import { languageFor } from "../../common/languages.ts";
function wrapQueryParse(query: string): AST | null {
const tree = parse(lang, `<!-- #query ${query} -->\n$\n<!-- /query -->`);
return parseTreeToAST(findNodeOfType(tree, "Query")!);
const tree = parse(languageFor("query")!, query);
// console.log("tree", tree);
return parseTreeToAST(tree.children![0]);
}
Deno.test("Test directive parser", () => {
Deno.test("Test query parser", () => {
// const query = ;
// console.log("query", query);
assertEquals(

View File

@ -1,23 +0,0 @@
import { renderToText } from "./tree.ts";
import { assert, assertEquals } from "../../test_deps.ts";
import { removeQueries } from "./query.ts";
import { parseMarkdown } from "$sb/lib/test_utils.ts";
const queryRemovalTest = `
# Heading
Before
<!-- #query page -->
Bla bla remove me
<!-- /query -->
End
`;
Deno.test("White out queries", () => {
const mdTree = parseMarkdown(queryRemovalTest);
removeQueries(mdTree);
const text = renderToText(mdTree);
// Same length? We should be good
assertEquals(text.length, queryRemovalTest.length);
assert(text.indexOf("remove me") === -1);
console.log("Whited out text", text);
});

View File

@ -1,12 +1,6 @@
import { ParseTree, renderToText, replaceNodesMatching } from "$sb/lib/tree.ts";
import { FunctionMap, KV, Query, QueryExpression } from "$sb/types.ts";
export const queryRegex =
/(<!--\s*#query\s+(.+?)-->)(.+?)(<!--\s*\/query\s*-->)/gs;
export const directiveStartRegex = /<!--\s*#([\w\-]+)\s+(.+?)-->/s;
export const directiveEndRegex = /<!--\s*\/([\w\-]+)\s*-->/s;
export function evalQueryExpression(
val: QueryExpression,
obj: any,
@ -246,17 +240,3 @@ export function applyQueryNoFilterKV(
}
return allItems;
}
export function removeQueries(pt: ParseTree) {
replaceNodesMatching(pt, (t) => {
if (t.type !== "Directive") {
return;
}
const renderedText = renderToText(t);
return {
from: t.from,
to: t.to,
text: new Array(renderedText.length + 1).join(" "),
};
});
}

View File

@ -49,16 +49,13 @@ Deno.test("Test rewritePageRefs", () => {
let tree = parseMarkdown(`
This is a [[local link]] and [[local link|with alias]].
<!-- #query page render [[template/page]] -->
<!-- /query -->
\`\`\`query
page render [[template/page]]
\`\`\`
<!-- #use [[template/use-template]] {} -->
<!-- /use -->
<!-- #include [[template/include-template]] {} -->
<!-- /include -->
\`\`\`template
page: "[[template/use-template]]"
\`\`\`
`);
rewritePageRefs(tree, "!silverbullet.md");
let rewrittenText = renderToText(tree);
@ -68,16 +65,13 @@ This is a [[local link]] and [[local link|with alias]].
`
This is a [[!silverbullet.md/local link]] and [[!silverbullet.md/local link|with alias]].
<!-- #query page render [[!silverbullet.md/template/page]] -->
<!-- /query -->
\`\`\`query
page render [[!silverbullet.md/template/page]]
\`\`\`
<!-- #use [[!silverbullet.md/template/use-template]] {} -->
<!-- /use -->
<!-- #include [[!silverbullet.md/template/include-template]] {} -->
<!-- /include -->
\`\`\`template
page: "[[!silverbullet.md/template/use-template]]"
\`\`\`
`,
);

View File

@ -49,29 +49,6 @@ export function isFederationPath(path: string) {
export function rewritePageRefs(tree: ParseTree, containerPageName: string) {
traverseTree(tree, (n): boolean => {
if (n.type === "DirectiveStart") {
const pageRef = findNodeOfType(n, "PageRef")!;
if (pageRef) {
const pageRefName = pageRef.children![0].text!.slice(2, -2);
pageRef.children![0].text = `[[${
resolvePath(containerPageName, pageRefName)
}]]`;
}
const directiveText = n.children![0].text;
// #use or #import
if (directiveText) {
const match = /\[\[(.+)\]\]/.exec(directiveText);
if (match) {
const pageRefName = match[1];
n.children![0].text = directiveText.replace(
match[0],
`[[${resolvePath(containerPageName, pageRefName)}]]`,
);
}
}
return true;
}
if (n.type === "FencedCode") {
const codeInfo = findNodeOfType(n, "CodeInfo");
if (!codeInfo) {

View File

@ -5,7 +5,6 @@ export const builtinPlugNames = [
"sync",
"template",
"plug-manager",
"directive",
"emoji",
"query",
"markdown",

View File

@ -1,317 +0,0 @@
import { editor, markdown, mq, space, sync } from "$sb/syscalls.ts";
import {
addParentPointers,
collectNodesOfType,
findParentMatching,
nodeAtPos,
ParseTree,
removeParentPointers,
renderToText,
traverseTree,
} from "$sb/lib/tree.ts";
import { renderDirectives } from "./directives.ts";
import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
import { isFederationPath } from "$sb/lib/resolve.ts";
import { MQMessage, PageMeta } from "$sb/types.ts";
import { sleep } from "$sb/lib/async.ts";
const directiveUpdateQueueName = "directiveUpdateQueue";
export async function updateDirectivesOnPageCommand() {
// If `arg` is a string, it's triggered automatically via an event, not explicitly via a command
const currentPage = await editor.getCurrentPage();
let pageMeta: PageMeta | undefined;
try {
pageMeta = await space.getPageMeta(currentPage);
} catch {
console.info("Page not found, not updating directives");
return;
}
const text = await editor.getText();
const tree = await markdown.parseMarkdown(text);
const metaData = await extractFrontmatter(tree, {
removeKeys: ["$disableDirectives"],
});
if (isFederationPath(currentPage)) {
console.info("Current page is a federation page, not updating directives.");
return;
}
if (metaData.$disableDirectives) {
console.info("Directives disabled in page meta, not updating them.");
return;
}
if (!(await sync.hasInitialSyncCompleted())) {
console.info(
"Initial sync hasn't completed yet, not updating directives.",
);
return;
}
await editor.save();
const replacements = await findReplacements(tree, text, pageMeta);
// Iterate again and replace the bodies. Iterating again (not using previous positions)
// because text may have changed in the mean time (directive processing may take some time)
// Hypothetically in the mean time directives in text may have been changed/swapped, in which
// case this will break. This would be a rare edge case, however.
for (const replacement of replacements) {
// Fetch the text every time, because dispatch() will have been made changes
const text = await editor.getText();
// Determine the current position
const index = text.indexOf(replacement.fullMatch);
// This may happen if the query itself, or the user is editing inside the directive block (WHY!?)
if (index === -1) {
console.warn(
"Text I got",
text,
);
console.warn(
"Could not find directive in text, skipping",
replacement.fullMatch,
);
continue;
}
const from = index, to = index + replacement.fullMatch.length;
const newText = await replacement.textPromise;
if (text.substring(from, to) === newText) {
// No change, skip
continue;
}
await editor.dispatch({
changes: {
from,
to,
insert: newText,
},
});
}
}
export async function updateDirectivesInSpaceCommand() {
await editor.flashNotification(
"Updating directives in entire space, this can take a while...",
);
await updateDirectivesInSpace();
// And notify the user
await editor.flashNotification("Updating of all directives completed!");
}
export async function processUpdateQueue(messages: MQMessage[]) {
for (const message of messages) {
const pageName: string = message.body;
console.log("Updating directives in page", pageName);
await updateDirectivesForPage(pageName);
await mq.ack(directiveUpdateQueueName, message.id);
}
}
async function findReplacements(
tree: ParseTree,
text: string,
pageMeta: PageMeta,
) {
// Collect all directives and their body replacements
const replacements: { fullMatch: string; textPromise: Promise<string> }[] =
[];
// Convenience array to wait for all promises to resolve
const allPromises: Promise<string>[] = [];
removeParentPointers(tree);
traverseTree(tree, (tree) => {
if (tree.type !== "Directive") {
return false;
}
const fullMatch = text.substring(tree.from!, tree.to!);
try {
const promise = renderDirectives(pageMeta, tree);
replacements.push({
textPromise: promise,
fullMatch,
});
allPromises.push(promise);
} catch (e: any) {
replacements.push({
fullMatch,
textPromise: Promise.resolve(
`${renderToText(tree.children![0])}\n**ERROR:** ${e.message}\n${
renderToText(tree.children![tree.children!.length - 1])
}`,
),
});
}
return true;
});
// Wait for all to have processed
await Promise.all(allPromises);
return replacements;
}
export async function updateDirectivesInSpace() {
const pages = await space.listPages();
await mq.batchSend(directiveUpdateQueueName, pages.map((page) => page.name));
// Now let's wait for the processing to finish
let queueStats = await mq.getQueueStats(directiveUpdateQueueName);
while (queueStats.queued > 0 || queueStats.processing > 0) {
sleep(1000);
queueStats = await mq.getQueueStats(directiveUpdateQueueName);
}
console.log("Done updating directives in space!");
}
async function updateDirectivesForPage(
pageName: string,
) {
const pageMeta = await space.getPageMeta(pageName);
const currentText = await space.readPage(pageName);
const tree = await markdown.parseMarkdown(currentText);
const metaData = await extractFrontmatter(tree, {
removeKeys: ["$disableDirectives"],
});
if (isFederationPath(pageName)) {
console.info("Current page is a federation page, not updating directives.");
return;
}
if (metaData.$disableDirectives) {
console.info("Directives disabled in page meta, not updating them.");
return;
}
const newText = await updateDirectives(pageMeta, tree, currentText);
if (newText !== currentText) {
console.info("Content of page changed, saving", pageName);
await space.writePage(pageName, newText);
}
}
export async function updateDirectives(
pageMeta: PageMeta,
tree: ParseTree,
text: string,
) {
const replacements = await findReplacements(tree, text, pageMeta);
// Iterate again and replace the bodies.
for (const replacement of replacements) {
text = text.replace(
replacement.fullMatch,
await replacement.textPromise,
);
}
return text;
}
export async function convertToLive() {
const text = await editor.getText();
const pos = await editor.getCursor();
const tree = await markdown.parseMarkdown(text);
addParentPointers(tree);
const currentNode = nodeAtPos(tree, pos);
const directive = findParentMatching(
currentNode!,
(node) => node.type === "Directive",
);
if (!directive) {
await editor.flashNotification(
"No directive found at cursor position",
"error",
);
return;
}
console.log("Got this directive", directive);
const startNode = directive.children![0];
const startNodeText = renderToText(startNode);
if (startNodeText.includes("#query")) {
const queryText = renderToText(startNode.children![1]);
await editor.dispatch({
changes: {
from: directive.from,
to: directive.to,
insert: "```query\n" + queryText + "\n```",
},
});
} else if (
startNodeText.includes("#use") || startNodeText.includes("#include")
) {
const pageRefMatch = /\[\[([^\]]+)\]\]\s*([^\-]+)?/.exec(startNodeText);
if (!pageRefMatch) {
await editor.flashNotification(
"No page reference found in directive",
"error",
);
return;
}
const val = pageRefMatch[2];
await editor.dispatch({
changes: {
from: directive.from,
to: directive.to,
insert: '```template\npage: "[[' + pageRefMatch[1] + ']]"\n' +
(val ? `val: ${val}\n` : "") + "```",
},
});
}
}
export async function convertSpaceToLive() {
if (
!await editor.confirm(
"This will convert all directives in the space to live queries. Are you sure?",
)
) {
return;
}
const pages = await space.listPages();
for (const page of pages) {
console.log("Now converting", page);
const text = await space.readPage(page.name);
const newText = await convertDirectivesOnPage(text);
if (text !== newText) {
console.log("Changes were made, writing", page.name);
await space.writePage(page.name, newText);
}
}
await editor.flashNotification("All done!");
}
export async function convertDirectivesOnPage(text: string) {
const tree = await markdown.parseMarkdown(text);
collectNodesOfType(tree, "Directive").forEach((directive) => {
const directiveText = renderToText(directive);
console.log("Got this directive", directiveText);
const startNode = directive.children![0];
const startNodeText = renderToText(startNode);
if (startNodeText.includes("#query")) {
const queryText = renderToText(startNode.children![1]);
text = text.replace(directiveText, "```query\n" + queryText + "\n```");
} else if (
startNodeText.includes("#use") || startNodeText.includes("#include")
) {
const pageRefMatch = /\[\[([^\]]+)\]\]\s*([^\-]+)?/.exec(startNodeText);
if (!pageRefMatch) {
return;
}
const val = pageRefMatch[2];
text = text.replace(
directiveText,
'```template\npage: "[[' + pageRefMatch[1] + ']]"\n' +
(val ? `val: ${val}\n` : "") + "```",
);
}
});
// console.log("Converted page", text);
return text;
}

View File

@ -1,77 +0,0 @@
import { events } from "$sb/syscalls.ts";
import { CompleteEvent } from "$sb/app_event.ts";
import { buildHandebarOptions } from "./util.ts";
import type {
AttributeCompleteEvent,
AttributeCompletion,
} from "../index/attributes.ts";
import { PageMeta } from "$sb/types.ts";
export async function queryComplete(completeEvent: CompleteEvent) {
const querySourceMatch = /#query\s+([\w\-_]*)$/.exec(
completeEvent.linePrefix,
);
if (querySourceMatch) {
const allEvents = await events.listEvents();
const completionOptions = allEvents
.filter((eventName) =>
eventName.startsWith("query:") && !eventName.includes("*")
)
.map((source) => ({
label: source.substring("query:".length),
}));
const allObjectTypes: string[] = (await events.dispatchEvent("query_", {}))
.flat();
for (const type of allObjectTypes) {
completionOptions.push({
label: type,
});
}
return {
from: completeEvent.pos - querySourceMatch[1].length,
options: completionOptions,
};
}
if (completeEvent.parentNodes.includes("DirectiveStart")) {
const querySourceMatch = /#query\s+([\w\-_\/]+)/.exec(
completeEvent.linePrefix,
);
const whereMatch =
/(where|order\s+by|and|select(\s+[\w\s,]+)?)\s+([\w\-_]*)$/.exec(
completeEvent.linePrefix,
);
if (querySourceMatch && whereMatch) {
const type = querySourceMatch[1];
const attributePrefix = whereMatch[3];
const completions = (await events.dispatchEvent(
`attribute:complete:${type}`,
{
source: type,
prefix: attributePrefix,
} as AttributeCompleteEvent,
)).flat() as AttributeCompletion[];
return {
from: completeEvent.pos - attributePrefix.length,
options: attributeCompletionsToCMCompletion(completions),
};
}
}
return null;
}
export function attributeCompletionsToCMCompletion(
completions: AttributeCompletion[],
) {
return completions.map(
(completion) => ({
label: completion.name,
detail: `${completion.attributeType} (${completion.source})`,
type: "attribute",
}),
);
}

View File

@ -1,35 +0,0 @@
name: directive
requiredPermissions:
- fetch
functions:
updateDirectivesOnPageCommand:
path: ./command.ts:updateDirectivesOnPageCommand
command:
name: "Directives: Update"
events:
- editor:pageLoaded
updateDirectivesInSpace:
path: ./command.ts:updateDirectivesInSpace
updateDirectivesInSpaceCommand:
path: ./command.ts:updateDirectivesInSpaceCommand
command:
name: "Directives: Update Entire Space"
processUpdateQueue:
path: ./command.ts:processUpdateQueue
mqSubscriptions:
- queue: directiveUpdateQueue
batchSize: 3
queryComplete:
path: ./complete.ts:queryComplete
events:
- editor:complete
# Conversion
convertToLiveQuery:
path: command.ts:convertToLive
command:
name: "Directive: Convert to Live Query/Template"
convertSpaceToLive:
path: command.ts:convertSpaceToLive
command:
name: "Directive: Convert Entire Space to Live/Templates"

View File

@ -1,122 +0,0 @@
import {
addParentPointers,
findParentMatching,
ParseTree,
renderToText,
} from "$sb/lib/tree.ts";
import { PageMeta } from "$sb/types.ts";
import { editor, markdown } from "$sb/syscalls.ts";
import { evalDirectiveRenderer } from "./eval_directive.ts";
import { queryDirectiveRenderer } from "./query_directive.ts";
import {
cleanTemplateInstantiations,
templateDirectiveRenderer,
} from "./template_directive.ts";
/** An error that occurs while a directive is being rendered.
* Mostly annotates the underlying error with page metadata.
*/
export class RenderDirectiveError extends Error {
pageMeta: PageMeta;
directive: string;
cause: Error;
constructor(pageMeta: PageMeta, directive: string, cause: Error) {
super(`In directive "${directive}" from "${pageMeta.name}": ${cause}`, {
cause: cause,
});
this.pageMeta = pageMeta;
this.directive = directive;
this.cause = cause;
}
}
export const directiveStartRegex =
/<!--\s*#(use|use-verbose|include|eval|query)\s+(.*?)-->/i;
export const directiveRegex =
/(<!--\s*#(use|use-verbose|include|eval|query)\s+(.*?)-->)(.+?)(<!--\s*\/\2\s*-->)/gs;
/**
* Looks for directives in the text dispatches them based on name
*/
export async function directiveDispatcher(
pageMeta: PageMeta,
directiveTree: ParseTree,
directiveRenderers: Record<
string,
(
directive: string,
pageMeta: PageMeta,
arg: string | ParseTree,
) => Promise<string>
>,
): Promise<string> {
const directiveStart = directiveTree.children![0]; // <!-- #directive -->
const directiveEnd = directiveTree.children![2]; // <!-- /directive -->
const directiveStartText = renderToText(directiveStart).trim();
const directiveEndText = renderToText(directiveEnd).trim();
const firstPart = directiveStart.children![0].text!;
if (firstPart?.includes("#query")) {
// #query
const newBody = await directiveRenderers["query"](
"query",
pageMeta,
directiveStart.children![1].children![0], // The query ParseTree
);
const result =
`${directiveStartText}\n${newBody.trim()}\n${directiveEndText}`;
return result;
} else if (firstPart?.includes("#eval")) {
console.log("Eval stuff", directiveStart.children![1].children![0]);
const newBody = await directiveRenderers["eval"](
"eval",
pageMeta,
directiveStart.children![1].children![0],
);
const result =
`${directiveStartText}\n${newBody.trim()}\n${directiveEndText}`;
return result;
} else {
// Everything not #query and #eval
const match = directiveStartRegex.exec(directiveStart.children![0].text!);
if (!match) {
throw Error("No match");
}
let [_fullMatch, type, arg] = match;
try {
arg = arg.trim();
const newBody = await directiveRenderers[type](type, pageMeta, arg);
const result =
`${directiveStartText}\n${newBody.trim()}\n${directiveEndText}`;
return result;
} catch (e: any) {
return `${directiveStartText}\n**ERROR:** ${e.message}\n${directiveEndText}`;
}
}
}
export async function renderDirectives(
pageMeta: PageMeta,
directiveTree: ParseTree,
): Promise<string> {
try {
const replacementText = await directiveDispatcher(pageMeta, directiveTree, {
use: templateDirectiveRenderer,
include: templateDirectiveRenderer,
query: queryDirectiveRenderer,
eval: evalDirectiveRenderer,
});
return cleanTemplateInstantiations(replacementText);
} catch (e) {
throw new RenderDirectiveError(
pageMeta,
renderToText(directiveTree.children![0].children![1]).trim(),
e,
);
}
}

View File

@ -1,29 +0,0 @@
import { ParseTree, parseTreeToAST } from "$sb/lib/tree.ts";
import { replaceTemplateVars } from "../template/template.ts";
import { PageMeta } from "$sb/types.ts";
import { expressionToKvQueryExpression } from "$sb/lib/parse-query.ts";
import { evalQueryExpression } from "$sb/lib/query.ts";
import { builtinFunctions } from "$sb/lib/builtin_query_functions.ts";
// This is rather scary and fragile stuff, but it works.
export async function evalDirectiveRenderer(
_directive: string,
pageMeta: PageMeta,
expression: string | ParseTree,
): Promise<string> {
try {
const result = evalQueryExpression(
expressionToKvQueryExpression(parseTreeToAST(
JSON.parse(
await replaceTemplateVars(JSON.stringify(expression), pageMeta),
),
)),
{},
builtinFunctions,
);
return Promise.resolve("" + result);
} catch (e: any) {
return Promise.resolve(`**ERROR:** ${e.message}`);
}
}

View File

@ -1,56 +0,0 @@
import { events } from "$sb/syscalls.ts";
import { replaceTemplateVars } from "../template/template.ts";
import { renderQueryTemplate } from "./util.ts";
import { jsonToMDTable } from "./util.ts";
import { ParseTree, parseTreeToAST } from "$sb/lib/tree.ts";
import { astToKvQuery } from "$sb/lib/parse-query.ts";
import { PageMeta, Query } from "$sb/types.ts";
export async function queryDirectiveRenderer(
_directive: string,
pageMeta: PageMeta,
query: string | ParseTree,
): Promise<string> {
if (typeof query === "string") {
throw new Error("Argument must be a ParseTree");
}
const parsedQuery: Query = astToKvQuery(
parseTreeToAST(
JSON.parse(await replaceTemplateVars(JSON.stringify(query), pageMeta)),
),
);
// console.log("QUERY", parsedQuery);
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: pageMeta.name },
30 * 1000,
);
if (results.length === 0) {
// This means there was no handler for the event which means it's unsupported
return `**Error:** Unsupported query source '${parsedQuery.querySource}'`;
} else {
// console.log("Parsed query", parsedQuery);
const allResults = results.flat();
if (parsedQuery.render) {
const rendered = await renderQueryTemplate(
pageMeta,
parsedQuery.render,
allResults,
parsedQuery.renderAll!,
);
return rendered.trim();
} else {
if (allResults.length === 0) {
return "No results";
} else {
return jsonToMDTable(allResults);
}
}
}
}

View File

@ -1,100 +0,0 @@
import { queryRegex } from "$sb/lib/query.ts";
import { ParseTree, renderToText } from "$sb/lib/tree.ts";
import { handlebars, markdown, space } from "$sb/syscalls.ts";
import { replaceTemplateVars } from "../template/template.ts";
import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
import { directiveRegex } from "./directives.ts";
import { updateDirectives } from "./command.ts";
import { resolvePath, rewritePageRefs } from "$sb/lib/resolve.ts";
import { PageMeta } from "$sb/types.ts";
import { renderTemplate } from "../template/plug_api.ts";
const templateRegex = /\[\[([^\]]+)\]\]\s*(.*)\s*/;
export async function templateDirectiveRenderer(
directive: string,
pageMeta: PageMeta,
arg: string | ParseTree,
): Promise<string> {
if (typeof arg !== "string") {
throw new Error("Template directives must be a string");
}
const match = arg.match(templateRegex);
if (!match) {
throw new Error(`Invalid template directive: ${arg}`);
}
let templatePath = match[1];
const args = match[2];
let parsedArgs = {};
if (args) {
try {
parsedArgs = JSON.parse(await replaceTemplateVars(args, pageMeta));
} catch {
throw new Error(
`Failed to parse template instantiation arg: ${
replaceTemplateVars(args, pageMeta)
}`,
);
}
}
let templateText = "";
if (
templatePath.startsWith("http://") || templatePath.startsWith("https://")
) {
try {
const req = await fetch(templatePath);
templateText = await req.text();
} catch (e: any) {
templateText = `ERROR: ${e.message}`;
}
} else {
templatePath = resolvePath(pageMeta.name, templatePath);
templateText = await space.readPage(templatePath);
}
const tree = await markdown.parseMarkdown(templateText);
await extractFrontmatter(tree, { removeFrontmatterSection: true }); // Remove entire frontmatter section, if any
// Resolve paths in the template
rewritePageRefs(tree, templatePath);
let newBody = renderToText(tree);
// console.log("Rewritten template:", newBody);
// if it's a template injection (not a literal "include")
if (directive === "use") {
newBody = (await renderTemplate(newBody, pageMeta, parsedArgs)).text;
// Recursively render directives
const tree = await markdown.parseMarkdown(newBody);
newBody = await updateDirectives(pageMeta, tree, newBody);
}
return newBody.trim();
}
export function cleanTemplateInstantiations(text: string) {
return text.replaceAll(directiveRegex, (
_fullMatch,
startInst,
type,
_args,
body,
endInst,
): string => {
if (type === "use") {
body = body.replaceAll(
queryRegex,
(
_fullMatch: string,
_startQuery: string,
_query: string,
body: string,
) => {
return body.trim();
},
);
}
return `${startInst}${body}${endInst}`;
});
}

View File

@ -1,77 +0,0 @@
import { handlebars, space } from "$sb/syscalls.ts";
import { handlebarHelpers } from "../../common/syscalls/handlebar_helpers.ts";
import { PageMeta } from "$sb/types.ts";
import { cleanTemplate } from "../template/plug_api.ts";
export function defaultJsonTransformer(_k: string, v: any) {
if (v === undefined) {
return "";
}
if (typeof v === "string") {
return v.replaceAll("\n", " ").replaceAll("|", "\\|");
}
return "" + v;
}
// Nicely format an array of JSON objects as a Markdown table
export function jsonToMDTable(
jsonArray: any[],
valueTransformer: (k: string, v: any) => string = defaultJsonTransformer,
): string {
const headers = new Set<string>();
for (const entry of jsonArray) {
for (const k of Object.keys(entry)) {
headers.add(k);
}
}
const headerList = [...headers];
const lines = [];
lines.push(
"|" +
headerList
.map(
(headerName) => headerName,
)
.join("|") +
"|",
);
lines.push(
"|" +
headerList
.map(() => "--")
.join("|") +
"|",
);
for (const val of jsonArray) {
const el = [];
for (const prop of headerList) {
const s = valueTransformer(prop, val[prop]);
el.push(s);
}
lines.push("|" + el.join("|") + "|");
}
return lines.join("\n");
}
export async function renderQueryTemplate(
pageMeta: PageMeta,
templatePage: string,
data: any[],
renderAll: boolean,
): Promise<string> {
let templateText = await space.readPage(templatePage);
templateText = await cleanTemplate(templateText);
if (!renderAll) {
templateText = `{{#each .}}\n${templateText}\n{{/each}}`;
}
return handlebars.renderTemplate(templateText, data, { page: pageMeta });
}
export function buildHandebarOptions(pageMeta: PageMeta) {
return {
helpers: handlebarHelpers(),
data: { page: pageMeta },
};
}

View File

@ -43,11 +43,6 @@ export async function brokenLinksCommand() {
}
}
if (tree.type === "DirectiveBody") {
// Don't look inside directive bodies
return true;
}
return false;
});
}

View File

@ -1,6 +1,5 @@
import { collectNodesOfType } from "$sb/lib/tree.ts";
import type { CompleteEvent, IndexTreeEvent } from "$sb/app_event.ts";
import { removeQueries } from "$sb/lib/query.ts";
import { ObjectValue, QueryExpression } from "$sb/types.ts";
import { indexObjects, queryObjects } from "./api.ts";
@ -11,7 +10,6 @@ type AnchorObject = ObjectValue<{
}>;
export async function indexAnchors({ name: pageName, tree }: IndexTreeEvent) {
removeQueries(tree);
const anchors: ObjectValue<AnchorObject>[] = [];
collectNodesOfType(tree, "NamedAnchor").forEach((n) => {

View File

@ -62,7 +62,6 @@ export const builtins: Record<string, Record<string, string>> = {
page: "!string",
pos: "!number",
alias: "!string",
inDirective: "!boolean",
asTemplate: "!boolean",
},
paragraph: {

View File

@ -14,6 +14,10 @@ export async function reindexSpace() {
console.log("Clearing page index...");
// Executed this way to not have to embed the search plug code here
await system.invokeFunction("index.clearIndex");
// Load builtins
await system.invokeFunction("index.loadBuiltinsIntoIndex");
const pages = await space.listPages();
// Queue all page names to be indexed

View File

@ -1,7 +1,6 @@
import type { IndexTreeEvent } from "$sb/app_event.ts";
import { YAML } from "$sb/syscalls.ts";
import { collectNodesOfType, findNodeOfType } from "$sb/lib/tree.ts";
import { removeQueries } from "$sb/lib/query.ts";
import { ObjectValue } from "$sb/types.ts";
import { indexObjects } from "./api.ts";
import { TagObject } from "./tags.ts";
@ -16,8 +15,6 @@ type DataObject = ObjectValue<
export async function indexData({ name, tree }: IndexTreeEvent) {
const dataObjects: ObjectValue<DataObject>[] = [];
removeQueries(tree);
await Promise.all(
collectNodesOfType(tree, "FencedCode").map(async (t) => {
const codeInfoNode = findNodeOfType(t, "CodeInfo");

View File

@ -1,7 +1,6 @@
import type { IndexTreeEvent } from "$sb/app_event.ts";
import { collectNodesOfType, ParseTree, renderToText } from "$sb/lib/tree.ts";
import { removeQueries } from "$sb/lib/query.ts";
import { extractAttributes } from "$sb/lib/attribute.ts";
import { rewritePageRefs } from "$sb/lib/resolve.ts";
import { ObjectValue } from "$sb/types.ts";
@ -17,7 +16,6 @@ export type ItemObject = ObjectValue<
export async function indexItems({ name, tree }: IndexTreeEvent) {
const items: ObjectValue<ItemObject>[] = [];
removeQueries(tree);
// console.log("Indexing items", name);

View File

@ -19,11 +19,11 @@ export async function renderMentions(): Promise<CodeWidgetContent | null> {
const page = await editor.getCurrentPage();
const linksResult = await queryObjects<LinkObject>("link", {
// Query all links that point to this page, excluding those that are inside directives and self pointers.
filter: ["and", ["!=", ["attr", "page"], ["string", page]], ["and", ["=", [
// Query all links that point to this page
filter: ["and", ["!=", ["attr", "page"], ["string", page]], ["=", [
"attr",
"toPage",
], ["string", page]], ["=", ["attr", "inDirective"], ["boolean", false]]]],
], ["string", page]]],
});
if (linksResult.length === 0) {
// Don't show the panel if there are no links here.

View File

@ -16,7 +16,6 @@ export type LinkObject = {
pos: number;
snippet: string;
alias?: string;
inDirective: boolean;
asTemplate: boolean;
};
@ -51,55 +50,7 @@ export async function indexLinks({ name, tree }: IndexTreeEvent) {
const pageText = renderToText(tree);
let directiveDepth = 0;
traverseTree(tree, (n): boolean => {
if (n.type === "DirectiveStart") {
directiveDepth++;
const pageRef = findNodeOfType(n, "PageRef")!;
if (pageRef) {
const pageRefName = resolvePath(
name,
pageRef.children![0].text!.slice(2, -2),
);
const pos = pageRef.from! + 2;
links.push({
ref: `${name}@${pos}`,
tags: ["link"],
toPage: pageRefName,
pos: pos,
snippet: extractSnippet(pageText, pos),
page: name,
asTemplate: true,
inDirective: false,
});
}
const directiveText = n.children![0].text;
// #use or #import
if (directiveText) {
const match = /\[\[(.+)\]\]/.exec(directiveText);
if (match) {
const pageRefName = resolvePath(name, match[1]);
const pos = n.from! + match.index! + 2;
links.push({
ref: `${name}@${pos}`,
tags: ["link"],
toPage: pageRefName,
page: name,
snippet: extractSnippet(pageText, pos),
pos: pos,
asTemplate: true,
inDirective: false,
});
}
}
return true;
}
if (n.type === "DirectiveEnd") {
directiveDepth--;
return true;
}
if (n.type === "WikiLink") {
const wikiLinkPage = findNodeOfType(n, "WikiLinkPage")!;
const wikiLinkAlias = findNodeOfType(n, "WikiLinkAlias");
@ -113,12 +64,8 @@ export async function indexLinks({ name, tree }: IndexTreeEvent) {
snippet: extractSnippet(pageText, pos),
pos,
page: name,
inDirective: false,
asTemplate: false,
};
if (directiveDepth > 0) {
link.inDirective = true;
}
if (wikiLinkAlias) {
link.alias = wikiLinkAlias.children![0].text!;
}
@ -151,7 +98,6 @@ export async function indexLinks({ name, tree }: IndexTreeEvent) {
snippet: extractSnippet(pageText, pos),
pos: pos,
asTemplate: true,
inDirective: false,
});
}
}

View File

@ -1,5 +1,4 @@
import type { CompleteEvent, IndexTreeEvent } from "$sb/app_event.ts";
import { removeQueries } from "$sb/lib/query.ts";
import { extractFrontmatter } from "$sb/lib/frontmatter.ts";
import { indexObjects, queryObjects } from "./api.ts";
import {
@ -16,7 +15,6 @@ export type TagObject = ObjectValue<{
}>;
export async function indexTags({ name, tree }: IndexTreeEvent) {
removeQueries(tree);
const tags = new Set<string>(); // name:parent
addParentPointers(tree);
const pageTags: string[] = (await extractFrontmatter(tree)).tags;

View File

@ -391,10 +391,6 @@ function render(
body: cleanTags(mapRender(newChildren)),
};
}
case "Directive": {
const body = findNodeOfType(t, "DirectiveBody")!;
return posPreservingRender(body.children![0], options);
}
case "Attribute":
if (options.preserveAttributes) {
return {

View File

@ -45,15 +45,6 @@ functions:
```query
|^|
```
insertInclude:
redirect: template.insertTemplateText
slashCommand:
name: include
description: Include another page
value: |
<!-- #include [[|^|]] -->
<!-- /include -->
insertUseTemplate:
redirect: template.insertTemplateText
slashCommand:

View File

@ -1,4 +1,4 @@
import type { LintEvent, WidgetContent } from "$sb/app_event.ts";
import type { LintEvent } from "$sb/app_event.ts";
import { events, language, space } from "$sb/syscalls.ts";
import {
findNodeOfType,
@ -6,10 +6,10 @@ import {
traverseTreeAsync,
} from "$sb/lib/tree.ts";
import { astToKvQuery } from "$sb/lib/parse-query.ts";
import { jsonToMDTable, renderQueryTemplate } from "../directive/util.ts";
import { loadPageObject, replaceTemplateVars } from "../template/template.ts";
import { cleanPageRef, resolvePath } from "$sb/lib/resolve.ts";
import { CodeWidgetContent, LintDiagnostic } from "$sb/types.ts";
import { jsonToMDTable, renderQueryTemplate } from "../template/util.ts";
export async function widget(
bodyText: string,

View File

@ -13,7 +13,6 @@ import {
replaceNodesMatching,
traverseTreeAsync,
} from "$sb/lib/tree.ts";
import { removeQueries } from "$sb/lib/query.ts";
import { niceDate } from "$sb/lib/dates.ts";
import { extractAttributes } from "$sb/lib/attribute.ts";
import { rewritePageRefs } from "$sb/lib/resolve.ts";
@ -47,7 +46,6 @@ const incompleteStates = [" "];
export async function indexTasks({ name, tree }: IndexTreeEvent) {
const tasks: ObjectValue<TaskObject>[] = [];
const taskStates = new Map<string, { count: number; firstPos: number }>();
removeQueries(tree);
addParentPointers(tree);
// const allAttributes: AttributeObject[] = [];
// const allTags = new Set<string>();

View File

@ -1,7 +1,6 @@
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,
@ -11,6 +10,7 @@ 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);

View File

@ -1,4 +1,8 @@
import { determineTags } from "$sb/lib/cheap_yaml.ts";
import { handlebarHelpers } from "../../common/syscalls/handlebar_helpers.ts";
import { PageMeta } from "$sb/types.ts";
import { handlebars, space } from "$sb/syscalls.ts";
import { cleanTemplate } from "./plug_api.ts";
const frontMatterRegex = /^---\n(([^\n]|\n)*?)---\n/;
@ -24,3 +28,76 @@ export function isTemplate(pageText: string): boolean {
}
return false;
}
export function buildHandebarOptions(pageMeta: PageMeta) {
return {
helpers: handlebarHelpers(),
data: { page: pageMeta },
};
}
export function defaultJsonTransformer(_k: string, v: any) {
if (v === undefined) {
return "";
}
if (typeof v === "string") {
return v.replaceAll("\n", " ").replaceAll("|", "\\|");
}
return "" + v;
}
// Nicely format an array of JSON objects as a Markdown table
export function jsonToMDTable(
jsonArray: any[],
valueTransformer: (k: string, v: any) => string = defaultJsonTransformer,
): string {
const headers = new Set<string>();
for (const entry of jsonArray) {
for (const k of Object.keys(entry)) {
headers.add(k);
}
}
const headerList = [...headers];
const lines = [];
lines.push(
"|" +
headerList
.map(
(headerName) => headerName,
)
.join("|") +
"|",
);
lines.push(
"|" +
headerList
.map(() => "--")
.join("|") +
"|",
);
for (const val of jsonArray) {
const el = [];
for (const prop of headerList) {
const s = valueTransformer(prop, val[prop]);
el.push(s);
}
lines.push("|" + el.join("|") + "|");
}
return lines.join("\n");
}
export async function renderQueryTemplate(
pageMeta: PageMeta,
templatePage: string,
data: any[],
renderAll: boolean,
): Promise<string> {
let templateText = await space.readPage(templatePage);
templateText = await cleanTemplate(templateText);
if (!renderAll) {
templateText = `{{#each .}}\n${templateText}\n{{/each}}`;
}
return handlebars.renderTemplate(templateText, data, { page: pageMeta });
}

View File

@ -29,7 +29,7 @@ cp -r dist_plug_bundle/_plug/* website_build/_plug/
#echo "And additional ones"
curl https://raw.githubusercontent.com/silverbulletmd/silverbullet-mermaid/main/mermaid.plug.js > website_build/_plug/mermaid.plug.js
echo "But remove some plugs"
rm -rf website_build/_plug/{plugmd,directive}.plug.js
rm -rf website_build/_plug/{plugmd}.plug.js
# Generate random modified date, and replace in _headers too

View File

@ -3,7 +3,6 @@ import type { Extension } from "../deps.ts";
import type { Client } from "../client.ts";
import { blockquotePlugin } from "./block_quote.ts";
import { admonitionPlugin } from "./admonition.ts";
import { directivePlugin } from "./directive.ts";
import { hideHeaderMarkPlugin, hideMarksPlugin } from "./hide_mark.ts";
import { cleanBlockPlugin } from "./block.ts";
import { linkPlugin } from "./link.ts";
@ -17,7 +16,6 @@ import { fencedCodePlugin } from "./fenced_code.ts";
export function cleanModePlugins(editor: Client) {
return [
linkPlugin(editor),
directivePlugin(),
blockquotePlugin(),
admonitionPlugin(editor),
hideMarksPlugin(),

View File

@ -1,113 +0,0 @@
import {
directiveEndRegex,
directiveStartRegex,
} from "../../plug-api/lib/query.ts";
import { Decoration, EditorState, syntaxTree } from "../deps.ts";
import { decoratorStateField, HtmlWidget, isCursorInRange } from "./util.ts";
// Does a few things: hides the directives when the cursor is not placed inside
// Adds a class to the start and end of the directive when the cursor is placed inside
export function directivePlugin() {
return decoratorStateField((state: EditorState) => {
const widgets: any[] = [];
syntaxTree(state).iterate({
enter: ({ type, from, to, node }) => {
const parent = node.parent;
if (!parent) {
return;
}
const cursorInRange = isCursorInRange(state, [parent.from, parent.to]);
if (type.name === "DirectiveStart") {
if (cursorInRange) {
// Cursor inside this directive
widgets.push(
Decoration.line({ class: "sb-directive-start" }).range(from),
);
} else {
const text = state.sliceDoc(from, to);
const match = directiveStartRegex.exec(text);
if (!match) {
console.error("Something went wrong with this directive");
return;
}
const [_fullMatch, directiveName] = match;
widgets.push(
Decoration.widget({
widget: new HtmlWidget(
`#${directiveName}`,
"sb-directive-placeholder",
),
}).range(from),
);
widgets.push(
Decoration.line({
class: "sb-directive-start sb-directive-start-outside",
attributes: {
spellcheck: "false",
},
}).range(
from,
),
);
}
return true;
}
if (type.name === "DirectiveEnd") {
// Cursor outside this directive
if (cursorInRange) {
widgets.push(
Decoration.line({ class: "sb-directive-end" }).range(from),
);
} else {
const text = state.sliceDoc(from, to);
const match = directiveEndRegex.exec(text);
if (!match) {
console.error("Something went wrong with this directive");
return;
}
const [_fullMatch, directiveName] = match;
widgets.push(
Decoration.widget({
widget: new HtmlWidget(
`/${directiveName}`,
"sb-directive-placeholder",
),
}).range(from),
);
widgets.push(
Decoration.line({
class: "sb-directive-end sb-directive-end-outside",
}).range(
from,
),
);
}
return true;
}
if (type.name === "DirectiveBody") {
const lines = state.sliceDoc(from, to).split("\n");
let pos = from;
for (const line of lines) {
if (pos !== to) {
widgets.push(
Decoration.line({
class: "sb-directive-body",
}).range(pos),
);
}
pos += line.length + 1;
}
return true;
}
},
});
return Decoration.set(widgets, true);
});
}

View File

@ -6,9 +6,8 @@ const straightQuoteContexts = [
"FencedCode",
"InlineCode",
"FrontMatterCode",
"DirectiveStart",
"Attribute",
"CommandLink"
"CommandLink",
];
// TODO: Add support for selection (put quotes around or create blockquote block?)

View File

@ -48,7 +48,6 @@ export default function highlightStyles(mdExtension: MDExt[]) {
{ tag: t.invalid, class: "sb-invalid" },
{ tag: t.processingInstruction, class: "sb-meta" },
{ tag: t.punctuation, class: "sb-punctuation" },
{ tag: ct.DirectiveTag, class: "sb-directive" },
{ tag: ct.HorizontalRuleTag, class: "sb-hr" },
...mdExtension.map((mdExt) => {
return { tag: mdExt.tag, ...mdExt.styles, class: mdExt.className };

View File

@ -317,27 +317,6 @@
color: var(--editor-frontmatter-marker-color);
}
// Directives
.sb-directive-body {
border-left: 1px solid var(--editor-directive-border-color);
border-right: 1px solid var(--editor-directive-border-color);
}
.cm-line.sb-directive-start,
.cm-line.sb-directive-end {
color: var(--editor-directive-color);
border-color: var(--editor-directive-border-color);
background-color: var(--editor-directive-background-color);
}
.sb-directive-start-outside,
.sb-directive-end-outside {
&>span.sb-directive-placeholder {
color: var(--editor-directive-info-color);
}
}
.sb-emphasis {
font-style: italic;
}

View File

@ -316,47 +316,6 @@
font-size: 91%;
}
.sb-directive-start {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
border-style: solid;
border-width: 1px 1px 0 1px;
padding: 3px;
}
.sb-directive-end {
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
border-style: solid;
border-width: 0 1px 1px 1px;
padding: 3px;
}
.sb-directive-start .sb-comment,
.sb-directive-end .sb-comment {
position: relative;
left: -12px;
}
.sb-directive-start-outside,
.sb-directive-end-outside {
color: transparent;
pointer-events: none;
height: 1.3em;
.sb-directive-placeholder {
padding-right: 7px;
float: right;
font-size: 80%;
}
&>span,
&.sb-directive-start,
&.sb-directive-end {
color: transparent;
}
}
.sb-line-frontmatter-outside,
.sb-line-code-outside {
.sb-meta {
@ -458,7 +417,7 @@
border-top-left-radius: 5px;
margin: 0 0 5px 0;
padding: 10px !important;
background-color: var(--editor-directive-background-color);
background-color: var(--editor-widget-background-color);
font-size: 1.2em;
}
@ -474,7 +433,7 @@
.sb-markdown-top-widget:has(*),
.sb-markdown-bottom-widget:has(*) {
overflow-y: auto;
border: 1px solid var(--editor-directive-background-color);
border: 1px solid var(--editor-widget-background-color);
border-radius: 5px;
white-space: normal;
position: relative;
@ -542,7 +501,7 @@
right: 0;
top: 0;
display: none;
background: var(--editor-directive-background-color);
background: var(--editor-widget-background-color);
padding-inline: 3px;
padding: 4px 0;
// border-radius: 0 5px;
@ -573,7 +532,7 @@
max-width: 100%;
padding: 0;
margin: 0 0 -2ch 0;
border: 1px solid var(--editor-directive-background-color);
border: 1px solid var(--editor-widget-background-color);
border-radius: 5px;
}
}
@ -608,15 +567,4 @@
div:not(.cm-focused).cm-fat-cursor {
outline: none !important;
}
// @media only screen and (max-width: 600px) {
// #sb-editor {
// .sb-directive-start-outside,
// .sb-directive-end-outside {
// height: 22px;
// }
// }
// }
}

View File

@ -150,14 +150,14 @@ body {
.sb-bottom-iframe {
width: 100%;
margin-top: 10px;
border: 1px solid var(--editor-directive-background-color);
border: 1px solid var(--editor-widget-background-color);
border-radius: 5px;
}
.sb-top-iframe {
width: 100%;
margin-top: 10px;
border: 1px solid var(--editor-directive-background-color);
border: 1px solid var(--editor-widget-background-color);
border-radius: 5px;
}

View File

@ -101,10 +101,7 @@ html {
--editor-frontmatter-background-color: rgba(255, 246, 189, 0.5);
--editor-frontmatter-color: var(--subtle-color);
--editor-frontmatter-marker-color: #89000080;
--editor-directive-background-color: rgb(238, 238, 238);
--editor-directive-border-color: #0000000f;
--editor-directive-color: #5b5b5b;
--editor-directive-info-color: var(--subtle-color);
--editor-widget-background-color: rgb(238, 238, 238);
--editor-task-marker-color: var(--subtle-color);
--editor-task-state-color: var(--subtle-color);
@ -219,9 +216,6 @@ html[data-theme="dark"] {
--editor-frontmatter-background-color: rgb(41, 40, 35, 0.5);
--editor-frontmatter-color: var(--subtle-color);
--editor-frontmatter-marker-color: #fff;
--editor-directive-background-color: rgba(72, 72, 72, 0.5);
--editor-directive-border-color: #0000000f;
--editor-directive-color: #5b5b5b;
--editor-directive-info-color: var(--subtle-color);
--editor-widget-background-color: rgba(72, 72, 72, 0.5);
--editor-task-marker-color: var(--subtle-color);
}

View File

@ -128,7 +128,7 @@ _Note_: this is the data source used for the {[Mentions: Toggle]} feature as wel
Here is a query that shows all links that appear in this particular page:
```query
link where page = "{{@page.name}}" and inDirective = false
link where page = "{{@page.name}}"
```
## anchor

View File

@ -44,7 +44,7 @@ task where page = "{{@page.name}}"
## Rendering
There is a [[!silverbullet.md/template/tasks/task]] template you can use to render tasks nicely rather than using the default table (as demonstrated above). When you use this template, you can even cycle through the states of the task by click on its state _inside_ the rendered query, and it will update the state of the _original_ task automatically (although not yet in reverse) — this works across pages.
Try it (by clicking on the checkbox inside of the directive):
Try it (by clicking on the checkbox inside of the query):
```query
task where page = "{{@page.name}}" and name = "Remote toggle me" render [[template/task]]