Large "query" plug refactor into "directive"
This commit is contained in:
@@ -1,20 +0,0 @@
|
||||
import { events } from "$sb/plugos-syscall/mod.ts";
|
||||
import { editor } from "$sb/silverbullet-syscall/mod.ts";
|
||||
|
||||
export async function queryComplete() {
|
||||
const prefix = await editor.matchBefore("#query [\\w\\-_]*");
|
||||
|
||||
if (prefix) {
|
||||
const allEvents = await events.listEvents();
|
||||
// console.log("All events", allEvents);
|
||||
|
||||
return {
|
||||
from: prefix.from + "#query ".length,
|
||||
options: allEvents
|
||||
.filter((eventName) => eventName.startsWith("query:"))
|
||||
.map((source) => ({
|
||||
label: source.substring("query:".length),
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
// Index key space:
|
||||
// data:page@pos
|
||||
|
||||
import type { IndexTreeEvent, QueryProviderEvent } from "$sb/app_event.ts";
|
||||
import { index } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import {
|
||||
addParentPointers,
|
||||
collectNodesOfType,
|
||||
findNodeOfType,
|
||||
ParseTree,
|
||||
renderToText,
|
||||
replaceNodesMatching,
|
||||
} from "$sb/lib/tree.ts";
|
||||
import { applyQuery, removeQueries } from "$sb/lib/query.ts";
|
||||
import * as YAML from "yaml";
|
||||
import { text } from "https://esm.sh/v96/@fortawesome/fontawesome-svg-core@1.3.0/X-ZS9AZm9ydGF3ZXNvbWUvZm9udGF3ZXNvbWUtY29tbW9uLXR5cGVz/index.d.ts";
|
||||
|
||||
export async function indexData({ name, tree }: IndexTreeEvent) {
|
||||
const dataObjects: { key: string; value: any }[] = [];
|
||||
|
||||
removeQueries(tree);
|
||||
|
||||
collectNodesOfType(tree, "FencedCode").forEach((t) => {
|
||||
const codeInfoNode = findNodeOfType(t, "CodeInfo");
|
||||
if (!codeInfoNode) {
|
||||
return;
|
||||
}
|
||||
if (codeInfoNode.children![0].text !== "data") {
|
||||
return;
|
||||
}
|
||||
const codeTextNode = findNodeOfType(t, "CodeText");
|
||||
if (!codeTextNode) {
|
||||
// Honestly, this shouldn't happen
|
||||
return;
|
||||
}
|
||||
const codeText = codeTextNode.children![0].text!;
|
||||
try {
|
||||
const docs = codeText.split("---").map((d) => YAML.parse(d));
|
||||
// We support multiple YAML documents in one block
|
||||
for (let i = 0; i < docs.length; i++) {
|
||||
const doc = docs[i];
|
||||
if (!doc) {
|
||||
continue;
|
||||
}
|
||||
dataObjects.push({
|
||||
key: `data:${name}@${i}`,
|
||||
value: doc,
|
||||
});
|
||||
}
|
||||
// console.log("Parsed data", parsedData);
|
||||
} catch (e) {
|
||||
console.error("Could not parse data", codeText, "error:", e);
|
||||
return;
|
||||
}
|
||||
});
|
||||
// console.log("Found", dataObjects.length, "data objects");
|
||||
await index.batchSet(name, dataObjects);
|
||||
}
|
||||
|
||||
export function extractMeta(
|
||||
parseTree: ParseTree,
|
||||
removeKeys: string[] = [],
|
||||
): any {
|
||||
let data: any = {};
|
||||
addParentPointers(parseTree);
|
||||
|
||||
replaceNodesMatching(parseTree, (t) => {
|
||||
// Find top-level hash tags
|
||||
if (t.type === "Hashtag") {
|
||||
// Check if if nested directly into a Paragraph
|
||||
if (t.parent && t.parent.type === "Paragraph") {
|
||||
const tagname = t.children![0].text!.substring(1);
|
||||
if (!data.tags) {
|
||||
data.tags = [];
|
||||
}
|
||||
if (!data.tags.includes(tagname)) {
|
||||
data.tags.push(tagname);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Find FrontMatter and parse it
|
||||
if (t.type === "FrontMatter") {
|
||||
const yamlText = renderToText(t.children![1].children![0]);
|
||||
const parsedData: any = YAML.parse(yamlText);
|
||||
const newData = { ...parsedData };
|
||||
data = { ...data, ...parsedData };
|
||||
if (removeKeys.length > 0) {
|
||||
let removedOne = false;
|
||||
|
||||
for (const key of removeKeys) {
|
||||
if (key in newData) {
|
||||
delete newData[key];
|
||||
removedOne = true;
|
||||
}
|
||||
}
|
||||
if (removedOne) {
|
||||
t.children![0].text = YAML.stringify(newData);
|
||||
}
|
||||
}
|
||||
// If nothing is left, let's just delete this whole block
|
||||
if (Object.keys(newData).length === 0) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Find a fenced code block with `meta` as the language type
|
||||
if (t.type !== "FencedCode") {
|
||||
return;
|
||||
}
|
||||
const codeInfoNode = findNodeOfType(t, "CodeInfo");
|
||||
if (!codeInfoNode) {
|
||||
return;
|
||||
}
|
||||
if (codeInfoNode.children![0].text !== "meta") {
|
||||
return;
|
||||
}
|
||||
const codeTextNode = findNodeOfType(t, "CodeText");
|
||||
if (!codeTextNode) {
|
||||
// Honestly, this shouldn't happen
|
||||
return;
|
||||
}
|
||||
const codeText = codeTextNode.children![0].text!;
|
||||
const parsedData: any = YAML.parse(codeText);
|
||||
const newData = { ...parsedData };
|
||||
data = { ...data, ...parsedData };
|
||||
if (removeKeys.length > 0) {
|
||||
let removedOne = false;
|
||||
for (const key of removeKeys) {
|
||||
if (key in newData) {
|
||||
delete newData[key];
|
||||
removedOne = true;
|
||||
}
|
||||
}
|
||||
if (removedOne) {
|
||||
codeTextNode.children![0].text = YAML.stringify(newData).trim();
|
||||
}
|
||||
}
|
||||
// If nothing is left, let's just delete this whole block
|
||||
if (Object.keys(newData).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
|
||||
if (data.name) {
|
||||
data.displayName = data.name;
|
||||
delete data.name;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function queryProvider({
|
||||
query,
|
||||
}: QueryProviderEvent): Promise<any[]> {
|
||||
const allData: any[] = [];
|
||||
for (const { key, page, value } of await index.queryPrefix("data:")) {
|
||||
const [, pos] = key.split("@");
|
||||
allData.push({
|
||||
...value,
|
||||
page: page,
|
||||
pos: +pos,
|
||||
});
|
||||
}
|
||||
return applyQuery(query, allData);
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
import { assertEquals } from "../../test_deps.ts";
|
||||
import { applyQuery } from "$sb/lib/query.ts";
|
||||
import { parseQuery } from "./parser.ts";
|
||||
|
||||
Deno.test("Test parser", () => {
|
||||
const parsedBasicQuery = parseQuery(`page`);
|
||||
assertEquals(parsedBasicQuery.table, "page");
|
||||
|
||||
const parsedQuery1 = parseQuery(
|
||||
`task where completed = false and dueDate <= "{{today}}" order by dueDate desc limit 5`,
|
||||
);
|
||||
assertEquals(parsedQuery1.table, "task");
|
||||
assertEquals(parsedQuery1.orderBy, "dueDate");
|
||||
assertEquals(parsedQuery1.orderDesc, true);
|
||||
assertEquals(parsedQuery1.limit, 5);
|
||||
assertEquals(parsedQuery1.filter.length, 2);
|
||||
assertEquals(parsedQuery1.filter[0], {
|
||||
op: "=",
|
||||
prop: "completed",
|
||||
value: false,
|
||||
});
|
||||
assertEquals(parsedQuery1.filter[1], {
|
||||
op: "<=",
|
||||
prop: "dueDate",
|
||||
value: "{{today}}",
|
||||
});
|
||||
|
||||
const parsedQuery2 = parseQuery(`page where name =~ /interview\\/.*/"`);
|
||||
assertEquals(parsedQuery2.table, "page");
|
||||
assertEquals(parsedQuery2.filter.length, 1);
|
||||
assertEquals(parsedQuery2.filter[0], {
|
||||
op: "=~",
|
||||
prop: "name",
|
||||
value: "interview\\/.*",
|
||||
});
|
||||
|
||||
const parsedQuery3 = parseQuery(`page where something != null`);
|
||||
assertEquals(parsedQuery3.table, "page");
|
||||
assertEquals(parsedQuery3.filter.length, 1);
|
||||
assertEquals(parsedQuery3.filter[0], {
|
||||
op: "!=",
|
||||
prop: "something",
|
||||
value: null,
|
||||
});
|
||||
|
||||
assertEquals(parseQuery(`page select name`).select, ["name"]);
|
||||
assertEquals(parseQuery(`page select name, age`).select, [
|
||||
"name",
|
||||
"age",
|
||||
]);
|
||||
|
||||
assertEquals(
|
||||
parseQuery(`gh-events where type in ["PushEvent", "somethingElse"]`),
|
||||
{
|
||||
table: "gh-events",
|
||||
filter: [
|
||||
{
|
||||
op: "in",
|
||||
prop: "type",
|
||||
value: ["PushEvent", "somethingElse"],
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
assertEquals(parseQuery(`something render [[template/table]]`), {
|
||||
table: "something",
|
||||
filter: [],
|
||||
render: "template/table",
|
||||
});
|
||||
|
||||
assertEquals(parseQuery(`something render "template/table"`), {
|
||||
table: "something",
|
||||
filter: [],
|
||||
render: "template/table",
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("Test applyQuery", () => {
|
||||
const data: any[] = [
|
||||
{ name: "interview/My Interview", lastModified: 1 },
|
||||
{ name: "interview/My Interview 2", lastModified: 2 },
|
||||
{ name: "Pete", age: 38 },
|
||||
{ name: "Angie", age: 28 },
|
||||
];
|
||||
|
||||
assertEquals(
|
||||
applyQuery(parseQuery(`page where name =~ /interview\\/.*/`), data),
|
||||
[
|
||||
{ name: "interview/My Interview", lastModified: 1 },
|
||||
{ name: "interview/My Interview 2", lastModified: 2 },
|
||||
],
|
||||
);
|
||||
assertEquals(
|
||||
applyQuery(
|
||||
parseQuery(`page where name =~ /interview\\/.*/ order by lastModified`),
|
||||
data,
|
||||
),
|
||||
[
|
||||
{ name: "interview/My Interview", lastModified: 1 },
|
||||
{ name: "interview/My Interview 2", lastModified: 2 },
|
||||
],
|
||||
);
|
||||
assertEquals(
|
||||
applyQuery(
|
||||
parseQuery(
|
||||
`page where name =~ /interview\\/.*/ order by lastModified desc`,
|
||||
),
|
||||
data,
|
||||
),
|
||||
[
|
||||
{ name: "interview/My Interview 2", lastModified: 2 },
|
||||
{ name: "interview/My Interview", lastModified: 1 },
|
||||
],
|
||||
);
|
||||
assertEquals(applyQuery(parseQuery(`page where age > 30`), data), [
|
||||
{ name: "Pete", age: 38 },
|
||||
]);
|
||||
assertEquals(
|
||||
applyQuery(parseQuery(`page where age > 28 and age < 38`), data),
|
||||
[],
|
||||
);
|
||||
assertEquals(
|
||||
applyQuery(parseQuery(`page where age > 30 select name`), data),
|
||||
[{ name: "Pete" }],
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
applyQuery(parseQuery(`page where name in ["Pete"] select name`), data),
|
||||
[{ name: "Pete" }],
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("Test applyQuery with multi value", () => {
|
||||
const data: any[] = [
|
||||
{ name: "Pete", children: ["John", "Angie"] },
|
||||
{ name: "Angie", children: ["Angie"] },
|
||||
{ name: "Steve" },
|
||||
];
|
||||
|
||||
assertEquals(
|
||||
applyQuery(parseQuery(`page where children = "Angie"`), data),
|
||||
[
|
||||
{ name: "Pete", children: ["John", "Angie"] },
|
||||
{ name: "Angie", children: ["Angie"] },
|
||||
],
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
applyQuery(parseQuery(`page where children = ["Angie", "John"]`), data),
|
||||
[
|
||||
{ name: "Pete", children: ["John", "Angie"] },
|
||||
{ name: "Angie", children: ["Angie"] },
|
||||
],
|
||||
);
|
||||
});
|
||||
@@ -1,80 +0,0 @@
|
||||
import { collectNodesOfType, ParseTree } from "$sb/lib/tree.ts";
|
||||
import Handlebars from "handlebars";
|
||||
import * as YAML from "yaml";
|
||||
|
||||
import { space } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { niceDate } from "$sb/lib/dates.ts";
|
||||
import { ParsedQuery } from "$sb/lib/query.ts";
|
||||
|
||||
export function valueNodeToVal(valNode: ParseTree): any {
|
||||
switch (valNode.type) {
|
||||
case "Number":
|
||||
return +valNode.children![0].text!;
|
||||
case "Bool":
|
||||
return valNode.children![0].text! === "true";
|
||||
case "Null":
|
||||
return null;
|
||||
case "Name":
|
||||
return valNode.children![0].text!;
|
||||
case "Regex": {
|
||||
const val = valNode.children![0].text!;
|
||||
return val.substring(1, val.length - 1);
|
||||
}
|
||||
case "String": {
|
||||
const stringVal = valNode.children![0].text!;
|
||||
return stringVal.substring(1, stringVal.length - 1);
|
||||
}
|
||||
case "PageRef": {
|
||||
const pageRefVal = valNode.children![0].text!;
|
||||
return pageRefVal.substring(2, pageRefVal.length - 2);
|
||||
}
|
||||
case "List": {
|
||||
return collectNodesOfType(valNode, "Value").map((t) =>
|
||||
valueNodeToVal(t.children![0])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function renderQuery(
|
||||
parsedQuery: ParsedQuery,
|
||||
data: any[],
|
||||
): Promise<string> {
|
||||
if (parsedQuery.render) {
|
||||
Handlebars.registerHelper("json", (v: any) => JSON.stringify(v));
|
||||
Handlebars.registerHelper("niceDate", (ts: any) => niceDate(new Date(ts)));
|
||||
Handlebars.registerHelper("prefixLines", (v: string, prefix: string) =>
|
||||
v
|
||||
.split("\n")
|
||||
.map((l) => prefix + l)
|
||||
.join("\n"));
|
||||
|
||||
Handlebars.registerHelper(
|
||||
"substring",
|
||||
(s: string, from: number, to: number, elipsis = "") =>
|
||||
s.length > to - from ? s.substring(from, to) + elipsis : s,
|
||||
);
|
||||
|
||||
Handlebars.registerHelper("yaml", (v: any, prefix: string) => {
|
||||
if (typeof prefix === "string") {
|
||||
let yaml = YAML.stringify(v)
|
||||
.split("\n")
|
||||
.join("\n" + prefix)
|
||||
.trim();
|
||||
if (Array.isArray(v)) {
|
||||
return "\n" + prefix + yaml;
|
||||
} else {
|
||||
return yaml;
|
||||
}
|
||||
} else {
|
||||
return YAML.stringify(v).trim();
|
||||
}
|
||||
});
|
||||
let templateText = await space.readPage(parsedQuery.render);
|
||||
templateText = `{{#each .}}\n${templateText}\n{{/each}}`;
|
||||
const template = Handlebars.compile(templateText, { noEscape: true });
|
||||
return template(data);
|
||||
}
|
||||
|
||||
return "ERROR";
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
import { editor } from "$sb/silverbullet-syscall/mod.ts";
|
||||
|
||||
import Handlebars from "handlebars";
|
||||
|
||||
import { markdown, space } from "$sb/silverbullet-syscall/mod.ts";
|
||||
import { invokeFunction } from "$sb/silverbullet-syscall/system.ts";
|
||||
import { renderQuery } from "./engine.ts";
|
||||
import { parseQuery } from "./parser.ts";
|
||||
import { replaceTemplateVars } from "../core/template.ts";
|
||||
import { jsonToMDTable } from "./util.ts";
|
||||
import { queryRegex } from "$sb/lib/query.ts";
|
||||
import { events } from "$sb/plugos-syscall/mod.ts";
|
||||
import { replaceAsync } from "$sb/lib/util.ts";
|
||||
import { nodeAtPos, renderToText } from "$sb/lib/tree.ts";
|
||||
import { extractMeta } from "./data.ts";
|
||||
|
||||
export async function updateMaterializedQueriesCommand() {
|
||||
const currentPage = await editor.getCurrentPage();
|
||||
await editor.save();
|
||||
if (
|
||||
await invokeFunction(
|
||||
"server",
|
||||
"updateMaterializedQueriesOnPage",
|
||||
currentPage,
|
||||
)
|
||||
) {
|
||||
await editor.reloadPage();
|
||||
}
|
||||
}
|
||||
|
||||
export const templateInstRegex =
|
||||
/(<!--\s*#(use|use-verbose|include)\s+\[\[([^\]]+)\]\](.*?)-->)(.+?)(<!--\s*\/\2\s*-->)/gs;
|
||||
|
||||
function updateTemplateInstantiations(
|
||||
text: string,
|
||||
pageName: string,
|
||||
): Promise<string> {
|
||||
return replaceAsync(
|
||||
text,
|
||||
templateInstRegex,
|
||||
async (fullMatch, startInst, type, template, args, _body, endInst) => {
|
||||
args = args.trim();
|
||||
let parsedArgs = {};
|
||||
if (args) {
|
||||
try {
|
||||
parsedArgs = JSON.parse(args);
|
||||
} catch {
|
||||
console.error("Failed to parse template instantiation args", args);
|
||||
return fullMatch;
|
||||
}
|
||||
}
|
||||
let templateText = "";
|
||||
if (template.startsWith("http://") || template.startsWith("https://")) {
|
||||
try {
|
||||
const req = await fetch(template);
|
||||
templateText = await req.text();
|
||||
} catch (e: any) {
|
||||
templateText = `ERROR: ${e.message}`;
|
||||
}
|
||||
} else {
|
||||
templateText = await space.readPage(template);
|
||||
}
|
||||
let newBody = templateText;
|
||||
// if it's a template injection (not a literal "include")
|
||||
if (type === "use" || type === "use-verbose") {
|
||||
const tree = await markdown.parseMarkdown(templateText);
|
||||
extractMeta(tree, ["$disableDirectives"]);
|
||||
templateText = renderToText(tree);
|
||||
const templateFn = Handlebars.compile(
|
||||
replaceTemplateVars(templateText, pageName),
|
||||
{ noEscape: true },
|
||||
);
|
||||
newBody = templateFn(parsedArgs);
|
||||
}
|
||||
return `${startInst}\n${newBody.trim()}\n${endInst}`;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function cleanTemplateInstantiations(text: string): Promise<string> {
|
||||
return replaceAsync(
|
||||
text,
|
||||
templateInstRegex,
|
||||
(
|
||||
_fullMatch,
|
||||
startInst,
|
||||
type,
|
||||
_template,
|
||||
_args,
|
||||
body,
|
||||
endInst,
|
||||
): Promise<string> => {
|
||||
if (type === "use") {
|
||||
body = body.replaceAll(
|
||||
queryRegex,
|
||||
(
|
||||
_fullMatch: string,
|
||||
_startQuery: string,
|
||||
_query: string,
|
||||
body: string,
|
||||
) => {
|
||||
return body.trim();
|
||||
},
|
||||
);
|
||||
}
|
||||
return Promise.resolve(`${startInst}${body}${endInst}`);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Called from client, running on server
|
||||
export async function updateMaterializedQueriesOnPage(
|
||||
pageName: string,
|
||||
): Promise<boolean> {
|
||||
// console.log("Updating queries");
|
||||
let text = "";
|
||||
try {
|
||||
text = await space.readPage(pageName);
|
||||
} catch {
|
||||
console.warn(
|
||||
"Could not read page",
|
||||
pageName,
|
||||
"perhaps it doesn't yet exist",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
let newText = await updateTemplateInstantiations(text, pageName);
|
||||
const tree = await markdown.parseMarkdown(newText);
|
||||
const metaData = extractMeta(tree, ["$disableDirectives"]);
|
||||
// console.log("Meta data", pageName, metaData);
|
||||
if (metaData.$disableDirectives) {
|
||||
console.log("Directives disabled, skipping");
|
||||
return false;
|
||||
}
|
||||
newText = renderToText(tree);
|
||||
|
||||
newText = await replaceAsync(
|
||||
newText,
|
||||
queryRegex,
|
||||
async (fullMatch, startQuery, query, _body, endQuery, index) => {
|
||||
const currentNode = nodeAtPos(tree, index + 1);
|
||||
if (currentNode?.type !== "CommentBlock") {
|
||||
// If not a comment block, it's likely a code block, ignore
|
||||
return fullMatch;
|
||||
}
|
||||
|
||||
const parsedQuery = parseQuery(replaceTemplateVars(query, pageName));
|
||||
|
||||
// console.log("Parsed query", parsedQuery);
|
||||
// Let's dispatch an event and see what happens
|
||||
const results = await events.dispatchEvent(
|
||||
`query:${parsedQuery.table}`,
|
||||
{ query: parsedQuery, pageName: pageName },
|
||||
10 * 1000,
|
||||
);
|
||||
if (results.length === 0) {
|
||||
return `${startQuery}\n${endQuery}`;
|
||||
} else if (results.length === 1) {
|
||||
if (parsedQuery.render) {
|
||||
const rendered = await renderQuery(parsedQuery, results[0]);
|
||||
return `${startQuery}\n${rendered.trim()}\n${endQuery}`;
|
||||
} else {
|
||||
return `${startQuery}\n${jsonToMDTable(results[0])}\n${endQuery}`;
|
||||
}
|
||||
} else {
|
||||
console.error("Too many query results", results);
|
||||
return fullMatch;
|
||||
}
|
||||
},
|
||||
);
|
||||
newText = await cleanTemplateInstantiations(newText);
|
||||
if (text !== newText) {
|
||||
await space.writePage(pageName, newText);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import { LRParser } from "@lezer/lr";
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states:
|
||||
"&fOVQPOOOmQQO'#C^QOQPOOOtQPO'#C`OyQQO'#CkO!OQPO'#CmO!TQPO'#CnO!YQPO'#CoOOQO'#Cq'#CqO!bQQO,58xO!iQQO'#CcO#WQQO'#CaOOQO'#Ca'#CaOOQO,58z,58zO#oQPO,59VOOQO,59X,59XO#tQQO'#DaOOQO,59Y,59YOOQO,59Z,59ZOOQO-E6o-E6oO$]QQO,58}OtQPO,58|O$tQQO1G.qO%`QPO'#CsO%eQQO,59{OOQO'#Cg'#CgOOQO'#Ci'#CiO$]QQO'#CjOOQO'#Cd'#CdOOQO1G.i1G.iOOQO1G.h1G.hOOQO'#Cl'#ClOOQO7+$]7+$]OOQO,59_,59_OOQO-E6q-E6qO%|QPO'#C}O&UQPO,59UO$]QQO'#CrO&ZQPO,59iOOQO1G.p1G.pOOQO,59^,59^OOQO-E6p-E6p",
|
||||
stateData:
|
||||
"&c~OjOS~ORPO~OkRO}SO!RTO!SUO!UVO~OhQX~P[ORYO~O!O^O~OX_O~OR`O~OYbOdbO~OhQa~P[OldOtdOudOvdOwdOxdOydOzdO{dO~O|eOhTXkTX}TX!RTX!STX!UTX~ORfO~OrgOh!TXk!TX}!TX!R!TX!S!TX!U!TX~OXlOYlO[lOmiOniOojOpkO~O!PoO!QoOh_ik_i}_i!R_i!S_i!U_i~ORqO~OrgOh!Tak!Ta}!Ta!R!Ta!S!Ta!U!Ta~OruOsqX~OswO~OruOsqa~O",
|
||||
goto:
|
||||
"#e!UPP!VP!Y!^!a!d!jPP!sP!s!s!Y!x!Y!Y!YP!{#R#XPPPPPPPPP#_PPPPPPPPPPPPPPPPP#bRQOTWPXR]RR[RQZRRneQmdQskRxuVldkuRpfQXPRcXQvsRyvQh`RrhRtkRaU",
|
||||
nodeNames:
|
||||
"⚠ Program Query Name WhereClause LogicalExpr AndExpr FilterExpr Value Number String Bool Regex Null List OrderClause Order LimitClause SelectClause RenderClause PageRef",
|
||||
maxTerm: 52,
|
||||
skippedNodes: [0],
|
||||
repeatNodeCount: 3,
|
||||
tokenData:
|
||||
"B[~R}X^$Opq$Oqr$srs%W|}%r}!O%w!P!Q&Y!Q!['P!^!_'X!_!`'f!`!a's!c!}%w!}#O(Q#P#Q(q#R#S%w#T#U(v#U#V+]#V#W%w#W#X,X#X#Y%w#Y#Z.T#Z#]%w#]#^0e#^#`%w#`#a1a#a#b%w#b#c3t#c#d5p#d#f%w#f#g8T#g#h;P#h#i={#i#k%w#k#l?w#l#o%w#y#z$O$f$g$O#BY#BZ$O$IS$I_$O$Ip$Iq%W$Iq$Ir%W$I|$JO$O$JT$JU$O$KV$KW$O&FU&FV$O~$TYj~X^$Opq$O#y#z$O$f$g$O#BY#BZ$O$IS$I_$O$I|$JO$O$JT$JU$O$KV$KW$O&FU&FV$O~$vP!_!`$y~%OPv~#r#s%R~%WOz~~%ZUOr%Wrs%ms$Ip%W$Ip$Iq%m$Iq$Ir%m$Ir~%W~%rOY~~%wOr~P%|SRP}!O%w!c!}%w#R#S%w#T#o%w~&_V[~OY&YZ]&Y^!P&Y!P!Q&t!Q#O&Y#O#P&y#P~&Y~&yO[~~&|PO~&Y~'UPX~!Q!['P~'^Pl~!_!`'a~'fOt~~'kPu~#r#s'n~'sOy~~'xPx~!_!`'{~(QOw~R(VPpQ!}#O(YP(]RO#P(Y#P#Q(f#Q~(YP(iP#P#Q(lP(qOdP~(vOs~R({WRP}!O%w!c!}%w#R#S%w#T#b%w#b#c)e#c#g%w#g#h*a#h#o%wR)jURP}!O%w!c!}%w#R#S%w#T#W%w#W#X)|#X#o%wR*TS|QRP}!O%w!c!}%w#R#S%w#T#o%wR*fURP}!O%w!c!}%w#R#S%w#T#V%w#V#W*x#W#o%wR+PS!QQRP}!O%w!c!}%w#R#S%w#T#o%wR+bURP}!O%w!c!}%w#R#S%w#T#m%w#m#n+t#n#o%wR+{S!OQRP}!O%w!c!}%w#R#S%w#T#o%wR,^URP}!O%w!c!}%w#R#S%w#T#X%w#X#Y,p#Y#o%wR,uURP}!O%w!c!}%w#R#S%w#T#g%w#g#h-X#h#o%wR-^URP}!O%w!c!}%w#R#S%w#T#V%w#V#W-p#W#o%wR-wS!PQRP}!O%w!c!}%w#R#S%w#T#o%wR.YTRP}!O%w!c!}%w#R#S%w#T#U.i#U#o%wR.nURP}!O%w!c!}%w#R#S%w#T#`%w#`#a/Q#a#o%wR/VURP}!O%w!c!}%w#R#S%w#T#g%w#g#h/i#h#o%wR/nURP}!O%w!c!}%w#R#S%w#T#X%w#X#Y0Q#Y#o%wR0XSnQRP}!O%w!c!}%w#R#S%w#T#o%wR0jURP}!O%w!c!}%w#R#S%w#T#b%w#b#c0|#c#o%wR1TS{QRP}!O%w!c!}%w#R#S%w#T#o%wR1fURP}!O%w!c!}%w#R#S%w#T#]%w#]#^1x#^#o%wR1}URP}!O%w!c!}%w#R#S%w#T#a%w#a#b2a#b#o%wR2fURP}!O%w!c!}%w#R#S%w#T#]%w#]#^2x#^#o%wR2}URP}!O%w!c!}%w#R#S%w#T#h%w#h#i3a#i#o%wR3hS!RQRP}!O%w!c!}%w#R#S%w#T#o%wR3yURP}!O%w!c!}%w#R#S%w#T#i%w#i#j4]#j#o%wR4bURP}!O%w!c!}%w#R#S%w#T#`%w#`#a4t#a#o%wR4yURP}!O%w!c!}%w#R#S%w#T#`%w#`#a5]#a#o%wR5dSoQRP}!O%w!c!}%w#R#S%w#T#o%wR5uURP}!O%w!c!}%w#R#S%w#T#f%w#f#g6X#g#o%wR6^URP}!O%w!c!}%w#R#S%w#T#W%w#W#X6p#X#o%wR6uURP}!O%w!c!}%w#R#S%w#T#X%w#X#Y7X#Y#o%wR7^URP}!O%w!c!}%w#R#S%w#T#f%w#f#g7p#g#o%wR7wS}QRP}!O%w!c!}%w#R#S%w#T#o%wR8YURP}!O%w!c!}%w#R#S%w#T#X%w#X#Y8l#Y#o%wR8qURP}!O%w!c!}%w#R#S%w#T#b%w#b#c9T#c#o%wR9YURP}!O%w!c!}%w#R#S%w#T#W%w#W#X9l#X#o%wR9qURP}!O%w!c!}%w#R#S%w#T#X%w#X#Y:T#Y#o%wR:YURP}!O%w!c!}%w#R#S%w#T#f%w#f#g:l#g#o%wR:sS!UQRP}!O%w!c!}%w#R#S%w#T#o%wR;UURP}!O%w!c!}%w#R#S%w#T#X%w#X#Y;h#Y#o%wR;mURP}!O%w!c!}%w#R#S%w#T#`%w#`#a<P#a#o%wR<UURP}!O%w!c!}%w#R#S%w#T#X%w#X#Y<h#Y#o%wR<mURP}!O%w!c!}%w#R#S%w#T#V%w#V#W=P#W#o%wR=UURP}!O%w!c!}%w#R#S%w#T#h%w#h#i=h#i#o%wR=oS!SQRP}!O%w!c!}%w#R#S%w#T#o%wR>QURP}!O%w!c!}%w#R#S%w#T#f%w#f#g>d#g#o%wR>iURP}!O%w!c!}%w#R#S%w#T#i%w#i#j>{#j#o%wR?QURP}!O%w!c!}%w#R#S%w#T#X%w#X#Y?d#Y#o%wR?kSmQRP}!O%w!c!}%w#R#S%w#T#o%wR?|URP}!O%w!c!}%w#R#S%w#T#[%w#[#]@`#]#o%wR@eURP}!O%w!c!}%w#R#S%w#T#X%w#X#Y@w#Y#o%wR@|URP}!O%w!c!}%w#R#S%w#T#f%w#f#gA`#g#o%wRAeURP}!O%w!c!}%w#R#S%w#T#X%w#X#YAw#Y#o%wRBOSkQRP}!O%w!c!}%w#R#S%w#T#o%w",
|
||||
tokenizers: [0, 1],
|
||||
topRules: { "Program": [0, 1] },
|
||||
tokenPrec: 0,
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
export const Program = 1,
|
||||
Query = 2,
|
||||
Name = 3,
|
||||
WhereClause = 4,
|
||||
LogicalExpr = 5,
|
||||
AndExpr = 6,
|
||||
FilterExpr = 7,
|
||||
Value = 8,
|
||||
Number = 9,
|
||||
String = 10,
|
||||
Bool = 11,
|
||||
Regex = 12,
|
||||
Null = 13,
|
||||
List = 14,
|
||||
OrderClause = 15,
|
||||
Order = 16,
|
||||
LimitClause = 17,
|
||||
SelectClause = 18,
|
||||
RenderClause = 19,
|
||||
PageRef = 20;
|
||||
@@ -1,77 +0,0 @@
|
||||
import {
|
||||
collectNodesOfType,
|
||||
findNodeOfType,
|
||||
replaceNodesMatching,
|
||||
} from "$sb/lib/tree.ts";
|
||||
import { lezerToParseTree } from "../../common/parse_tree.ts";
|
||||
import { valueNodeToVal } from "./engine.ts";
|
||||
|
||||
// @ts-ignore auto generated
|
||||
import { parser } from "./parse-query.js";
|
||||
import { ParsedQuery, QueryFilter } from "$sb/lib/query.ts";
|
||||
|
||||
export function parseQuery(query: string): ParsedQuery {
|
||||
const n = lezerToParseTree(query, parser.parse(query).topNode);
|
||||
// Clean the tree a bit
|
||||
replaceNodesMatching(n, (n) => {
|
||||
if (!n.type) {
|
||||
const trimmed = n.text!.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
n.text = trimmed;
|
||||
}
|
||||
});
|
||||
|
||||
// console.log("Parsed", JSON.stringify(n, null, 2));
|
||||
const queryNode = n.children![0];
|
||||
const parsedQuery: ParsedQuery = {
|
||||
table: queryNode.children![0].children![0].text!,
|
||||
filter: [],
|
||||
};
|
||||
const orderByNode = findNodeOfType(queryNode, "OrderClause");
|
||||
if (orderByNode) {
|
||||
const nameNode = findNodeOfType(orderByNode, "Name");
|
||||
parsedQuery.orderBy = nameNode!.children![0].text!;
|
||||
const orderNode = findNodeOfType(orderByNode, "Order");
|
||||
parsedQuery.orderDesc = orderNode
|
||||
? orderNode.children![0].text! === "desc"
|
||||
: false;
|
||||
}
|
||||
const limitNode = findNodeOfType(queryNode, "LimitClause");
|
||||
if (limitNode) {
|
||||
const nameNode = findNodeOfType(limitNode, "Number");
|
||||
parsedQuery.limit = valueNodeToVal(nameNode!);
|
||||
}
|
||||
|
||||
const filterNodes = collectNodesOfType(queryNode, "FilterExpr");
|
||||
for (const filterNode of filterNodes) {
|
||||
let val: any = undefined;
|
||||
const valNode = filterNode.children![2].children![0];
|
||||
val = valueNodeToVal(valNode);
|
||||
const f: QueryFilter = {
|
||||
prop: filterNode.children![0].children![0].text!,
|
||||
op: filterNode.children![1].text!,
|
||||
value: val,
|
||||
};
|
||||
parsedQuery.filter.push(f);
|
||||
}
|
||||
const selectNode = findNodeOfType(queryNode, "SelectClause");
|
||||
if (selectNode) {
|
||||
parsedQuery.select = [];
|
||||
collectNodesOfType(selectNode, "Name").forEach((t) => {
|
||||
parsedQuery.select!.push(t.children![0].text!);
|
||||
});
|
||||
}
|
||||
|
||||
const renderNode = findNodeOfType(queryNode, "RenderClause");
|
||||
if (renderNode) {
|
||||
let renderNameNode = findNodeOfType(renderNode, "PageRef");
|
||||
if (!renderNameNode) {
|
||||
renderNameNode = findNodeOfType(renderNode, "String");
|
||||
}
|
||||
parsedQuery.render = valueNodeToVal(renderNameNode!);
|
||||
}
|
||||
|
||||
return parsedQuery;
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
@precedence { logic @left }
|
||||
|
||||
@top Program { Query }
|
||||
|
||||
Query {
|
||||
Name ( WhereClause | OrderClause | LimitClause | SelectClause | RenderClause )*
|
||||
}
|
||||
|
||||
commaSep<content> { content ("," content)* }
|
||||
|
||||
WhereClause { "where" LogicalExpr }
|
||||
OrderClause { "order" "by" Name Order? }
|
||||
LimitClause { "limit" Number }
|
||||
SelectClause { "select" commaSep<Name> }
|
||||
RenderClause { "render" (PageRef | String) }
|
||||
|
||||
Order {
|
||||
"desc" | "asc"
|
||||
}
|
||||
|
||||
Value { Number | String | Bool | Regex | Null | List }
|
||||
|
||||
LogicalExpr { AndExpr | FilterExpr }
|
||||
|
||||
AndExpr { FilterExpr !logic "and" FilterExpr }
|
||||
|
||||
FilterExpr {
|
||||
Name "<" Value
|
||||
| Name "<=" Value
|
||||
| Name "=" Value
|
||||
| Name "!=" Value
|
||||
| Name ">=" Value
|
||||
| Name ">" Value
|
||||
| Name "=~" Value
|
||||
| Name "!=~" Value
|
||||
| Name "in" Value
|
||||
}
|
||||
|
||||
List { "[" commaSep<Value> "]" }
|
||||
|
||||
@skip { space }
|
||||
|
||||
|
||||
|
||||
Bool {
|
||||
"true" | "false"
|
||||
}
|
||||
|
||||
Null {
|
||||
"null"
|
||||
}
|
||||
|
||||
@tokens {
|
||||
space { std.whitespace+ }
|
||||
Name { (std.asciiLetter | "-" | "_")+ }
|
||||
String {
|
||||
("\"" | "“" | "”") ![\"”“]* ("\"" | "“" | "”")
|
||||
}
|
||||
PageRef {
|
||||
"[" "[" ![\]]* "]" "]"
|
||||
}
|
||||
Regex { "/" ( ![/\\\n\r] | "\\" _ )* "/"? }
|
||||
|
||||
Number { std.digit+ }
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
name: query
|
||||
imports:
|
||||
- https://get.silverbullet.md/global.plug.json
|
||||
functions:
|
||||
updateMaterializedQueriesOnPage:
|
||||
path: ./materialized_queries.ts:updateMaterializedQueriesOnPage
|
||||
updateMaterializedQueriesCommand:
|
||||
path: ./materialized_queries.ts:updateMaterializedQueriesCommand
|
||||
command:
|
||||
name: "Materialized Queries: Update"
|
||||
key: "Alt-q"
|
||||
events:
|
||||
- editor:pageLoaded
|
||||
indexData:
|
||||
path: ./data.ts:indexData
|
||||
events:
|
||||
- page:index
|
||||
dataQueryProvider:
|
||||
path: ./data.ts:queryProvider
|
||||
events:
|
||||
- query:data
|
||||
queryComplete:
|
||||
path: ./complete.ts:queryComplete
|
||||
events:
|
||||
- page:complete
|
||||
@@ -1,66 +0,0 @@
|
||||
const maxWidth = 70;
|
||||
// Nicely format an array of JSON objects as a Markdown table
|
||||
export function jsonToMDTable(
|
||||
jsonArray: any[],
|
||||
valueTransformer: (k: string, v: any) => string = (_k, v) => "" + v,
|
||||
): string {
|
||||
const fieldWidths = new Map<string, number>();
|
||||
for (const entry of jsonArray) {
|
||||
for (const k of Object.keys(entry)) {
|
||||
let fieldWidth = fieldWidths.get(k);
|
||||
if (!fieldWidth) {
|
||||
fieldWidth = valueTransformer(k, entry[k]).length;
|
||||
} else {
|
||||
fieldWidth = Math.max(valueTransformer(k, entry[k]).length, fieldWidth);
|
||||
}
|
||||
fieldWidths.set(k, fieldWidth);
|
||||
}
|
||||
}
|
||||
|
||||
let fullWidth = 0;
|
||||
for (const v of fieldWidths.values()) {
|
||||
fullWidth += v + 1;
|
||||
}
|
||||
|
||||
const headerList = [...fieldWidths.keys()];
|
||||
const lines = [];
|
||||
lines.push(
|
||||
"|" +
|
||||
headerList
|
||||
.map(
|
||||
(headerName) =>
|
||||
headerName +
|
||||
charPad(" ", fieldWidths.get(headerName)! - headerName.length),
|
||||
)
|
||||
.join("|") +
|
||||
"|",
|
||||
);
|
||||
lines.push(
|
||||
"|" +
|
||||
headerList
|
||||
.map((title) => charPad("-", fieldWidths.get(title)!))
|
||||
.join("|") +
|
||||
"|",
|
||||
);
|
||||
for (const val of jsonArray) {
|
||||
const el = [];
|
||||
for (const prop of headerList) {
|
||||
const s = valueTransformer(prop, val[prop]);
|
||||
el.push(s + charPad(" ", fieldWidths.get(prop)! - s.length));
|
||||
}
|
||||
lines.push("|" + el.join("|") + "|");
|
||||
}
|
||||
return lines.join("\n");
|
||||
|
||||
function charPad(ch: string, length: number) {
|
||||
if (fullWidth > maxWidth && ch === "") {
|
||||
return "";
|
||||
} else if (fullWidth > maxWidth && ch === "-") {
|
||||
return "--";
|
||||
}
|
||||
if (length < 1) {
|
||||
return "";
|
||||
}
|
||||
return new Array(length + 1).join(ch);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user