* Fixes #529 by removing directives * Load builtin tags on space reindex
This commit is contained in:
parent
5f4e584e46
commit
8a2e081672
@ -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,
|
||||
}),
|
||||
};
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
});
|
@ -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(" "),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -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]]"
|
||||
\`\`\`
|
||||
`,
|
||||
);
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -5,7 +5,6 @@ export const builtinPlugNames = [
|
||||
"sync",
|
||||
"template",
|
||||
"plug-manager",
|
||||
"directive",
|
||||
"emoji",
|
||||
"query",
|
||||
"markdown",
|
||||
|
@ -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;
|
||||
}
|
@ -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",
|
||||
}),
|
||||
);
|
||||
}
|
@ -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"
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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}`);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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}`;
|
||||
});
|
||||
}
|
@ -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 },
|
||||
};
|
||||
}
|
@ -43,11 +43,6 @@ export async function brokenLinksCommand() {
|
||||
}
|
||||
}
|
||||
|
||||
if (tree.type === "DirectiveBody") {
|
||||
// Don't look inside directive bodies
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
@ -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) => {
|
||||
|
@ -62,7 +62,6 @@ export const builtins: Record<string, Record<string, string>> = {
|
||||
page: "!string",
|
||||
pos: "!number",
|
||||
alias: "!string",
|
||||
inDirective: "!boolean",
|
||||
asTemplate: "!boolean",
|
||||
},
|
||||
paragraph: {
|
||||
|
@ -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
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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>();
|
||||
|
@ -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);
|
||||
|
@ -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 });
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
@ -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?)
|
||||
|
@ -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 };
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
@ -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
|
||||
|
@ -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]]
|
||||
|
Loading…
Reference in New Issue
Block a user