Complete redo of content indexing and querying (#517)
Complete redo of data store Introduces live queries and live templates
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import type { ParseTree } from "$sb/lib/tree.ts";
|
||||
import { ParsedQuery } from "$sb/lib/query.ts";
|
||||
import { TextChange } from "$sb/lib/change.ts";
|
||||
import { Query } from "$sb/types.ts";
|
||||
|
||||
export type AppEvent =
|
||||
| "page:click"
|
||||
@@ -16,7 +16,7 @@ export type AppEvent =
|
||||
| "editor:pageModified";
|
||||
|
||||
export type QueryProviderEvent = {
|
||||
query: ParsedQuery;
|
||||
query: Query;
|
||||
pageName: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { FunctionMap } from "$sb/types.ts";
|
||||
import { niceDate } from "$sb/lib/dates.ts";
|
||||
|
||||
export const builtinFunctions: FunctionMap = {
|
||||
today() {
|
||||
return niceDate(new Date());
|
||||
},
|
||||
max(...args: number[]) {
|
||||
return Math.max(...args);
|
||||
},
|
||||
min(...args: number[]) {
|
||||
return Math.min(...args);
|
||||
},
|
||||
toJSON(obj: any) {
|
||||
return JSON.stringify(obj);
|
||||
},
|
||||
// Note: these assume Monday as the first day of the week
|
||||
firstDayOfWeek(dateString: string): string {
|
||||
const date = new Date(dateString);
|
||||
const dayOfWeek = date.getDay();
|
||||
const daysToSubtract = (dayOfWeek + 7 - 1) % 7;
|
||||
const firstDayOfWeek = new Date(date);
|
||||
firstDayOfWeek.setDate(date.getDate() - daysToSubtract);
|
||||
return niceDate(firstDayOfWeek);
|
||||
},
|
||||
lastDayOfWeek(dateString: string): string {
|
||||
const date = new Date(dateString);
|
||||
const dayOfWeek = date.getDay();
|
||||
const daysToAdd = (7 - dayOfWeek) % 7;
|
||||
const lastDayOfWeek = new Date(date);
|
||||
lastDayOfWeek.setDate(date.getDate() + daysToAdd);
|
||||
return niceDate(lastDayOfWeek);
|
||||
},
|
||||
addDays(dateString: string, daysToAdd: number): string {
|
||||
const date = new Date(dateString);
|
||||
date.setDate(date.getDate() + daysToAdd);
|
||||
return niceDate(date);
|
||||
},
|
||||
};
|
||||
+11
-45
@@ -2,7 +2,7 @@ import { YAML } from "$sb/plugos-syscall/mod.ts";
|
||||
|
||||
import {
|
||||
addParentPointers,
|
||||
findNodeOfType,
|
||||
collectNodesOfType,
|
||||
ParseTree,
|
||||
renderToText,
|
||||
replaceNodesMatchingAsync,
|
||||
@@ -18,21 +18,24 @@ export async function extractFrontmatter(
|
||||
): Promise<any> {
|
||||
let data: any = {};
|
||||
addParentPointers(tree);
|
||||
let paragraphCounter = 0;
|
||||
|
||||
await replaceNodesMatchingAsync(tree, async (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 (t.type === "Paragraph") {
|
||||
paragraphCounter++;
|
||||
// Only attach hashtags in the first paragraph to the page
|
||||
if (paragraphCounter !== 1) {
|
||||
return;
|
||||
}
|
||||
collectNodesOfType(t, "Hashtag").forEach((h) => {
|
||||
if (!data.tags) {
|
||||
data.tags = [];
|
||||
}
|
||||
const tagname = h.children![0].text!.substring(1);
|
||||
if (Array.isArray(data.tags) && !data.tags.includes(tagname)) {
|
||||
data.tags.push(tagname);
|
||||
}
|
||||
}
|
||||
return;
|
||||
});
|
||||
}
|
||||
// Find FrontMatter and parse it
|
||||
if (t.type === "FrontMatter") {
|
||||
@@ -64,43 +67,6 @@ export async function extractFrontmatter(
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = (await 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;
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import type { AST } from "$sb/lib/tree.ts";
|
||||
import type { Query, QueryExpression } from "$sb/types.ts";
|
||||
|
||||
export function astToKvQuery(
|
||||
node: AST,
|
||||
): Query {
|
||||
const query: Query = {
|
||||
querySource: "",
|
||||
};
|
||||
const [queryType, querySource, ...clauses] = node;
|
||||
if (queryType !== "Query") {
|
||||
throw new Error(`Expected query type, got ${queryType}`);
|
||||
}
|
||||
query.querySource = querySource[1] as string;
|
||||
for (const clause of clauses) {
|
||||
const [clauseType] = clause;
|
||||
switch (clauseType) {
|
||||
case "WhereClause": {
|
||||
if (query.filter) {
|
||||
query.filter = [
|
||||
"and",
|
||||
query.filter,
|
||||
expressionToKvQueryFilter(clause[2]),
|
||||
];
|
||||
} else {
|
||||
query.filter = expressionToKvQueryFilter(clause[2]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "OrderClause": {
|
||||
if (!query.orderBy) {
|
||||
query.orderBy = [];
|
||||
}
|
||||
for (const orderBy of clause.slice(2)) {
|
||||
if (orderBy[0] === "OrderBy") {
|
||||
// console.log("orderBy", orderBy);
|
||||
const expr = orderBy[1][1];
|
||||
if (orderBy[2]) {
|
||||
query.orderBy.push({
|
||||
expr: expressionToKvQueryExpression(expr),
|
||||
desc: orderBy[2][1][1] === "desc",
|
||||
});
|
||||
} else {
|
||||
query.orderBy.push({
|
||||
expr: expressionToKvQueryExpression(expr),
|
||||
desc: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case "LimitClause": {
|
||||
query.limit = expressionToKvQueryExpression(clause[2][1]);
|
||||
break;
|
||||
}
|
||||
case "SelectClause": {
|
||||
for (const select of clause.slice(2)) {
|
||||
if (select[0] === "Select") {
|
||||
if (!query.select) {
|
||||
query.select = [];
|
||||
}
|
||||
if (select.length === 2) {
|
||||
query.select.push({ name: select[1][1] as string });
|
||||
} else {
|
||||
query.select.push({
|
||||
name: select[3][1] as string,
|
||||
expr: expressionToKvQueryExpression(select[1]),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "RenderClause": {
|
||||
query.render = (clause[2][1] as string).slice(2, -2);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown clause type: ${clauseType}`);
|
||||
}
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
export function expressionToKvQueryExpression(node: AST): QueryExpression {
|
||||
if (["LVal", "Expression", "Value"].includes(node[0])) {
|
||||
return expressionToKvQueryExpression(node[1]);
|
||||
}
|
||||
// console.log("Got expression", node);
|
||||
switch (node[0]) {
|
||||
case "Attribute": {
|
||||
return [
|
||||
"attr",
|
||||
expressionToKvQueryExpression(node[1]),
|
||||
node[3][1] as string,
|
||||
];
|
||||
}
|
||||
case "Identifier":
|
||||
return ["attr", node[1] as string];
|
||||
case "String":
|
||||
return ["string", (node[1] as string).slice(1, -1)];
|
||||
case "Number":
|
||||
return ["number", +(node[1])];
|
||||
case "Bool":
|
||||
return ["boolean", node[1][1] === "true"];
|
||||
case "null":
|
||||
return ["null"];
|
||||
case "Regex":
|
||||
return ["regexp", (node[1] as string).slice(1, -1), "i"];
|
||||
case "List": {
|
||||
const exprs: AST[] = [];
|
||||
for (const expr of node.slice(2)) {
|
||||
if (expr[0] === "Expression") {
|
||||
exprs.push(expr);
|
||||
}
|
||||
}
|
||||
return ["array", exprs.map(expressionToKvQueryExpression)];
|
||||
}
|
||||
case "BinExpression": {
|
||||
const lval = expressionToKvQueryExpression(node[1]);
|
||||
const binOp = (node[2] as string).trim();
|
||||
const val = expressionToKvQueryExpression(node[3]);
|
||||
return [binOp as any, lval, val];
|
||||
}
|
||||
case "LogicalExpression": {
|
||||
const op1 = expressionToKvQueryFilter(node[1]);
|
||||
const op = node[2];
|
||||
const op2 = expressionToKvQueryFilter(node[3]);
|
||||
return [op[1] as any, op1, op2];
|
||||
}
|
||||
case "ParenthesizedExpression": {
|
||||
return expressionToKvQueryFilter(node[2]);
|
||||
}
|
||||
case "Call": {
|
||||
// console.log("Call", node);
|
||||
const fn = node[1][1] as string;
|
||||
const args: AST[] = [];
|
||||
for (const expr of node.slice(2)) {
|
||||
if (expr[0] === "Expression") {
|
||||
args.push(expr);
|
||||
}
|
||||
}
|
||||
return ["call", fn, args.map(expressionToKvQueryExpression)];
|
||||
}
|
||||
default:
|
||||
throw new Error(`Not supported: ${node[0]}`);
|
||||
}
|
||||
}
|
||||
|
||||
function expressionToKvQueryFilter(
|
||||
node: AST,
|
||||
): QueryExpression {
|
||||
const [expressionType] = node;
|
||||
if (expressionType === "Expression") {
|
||||
return expressionToKvQueryFilter(node[1]);
|
||||
}
|
||||
switch (expressionType) {
|
||||
case "BinExpression": {
|
||||
const lval = expressionToKvQueryExpression(node[1]);
|
||||
const binOp = node[2][0] === "InKW" ? "in" : (node[2] as string).trim();
|
||||
const val = expressionToKvQueryExpression(node[3]);
|
||||
return [binOp as any, lval, val];
|
||||
}
|
||||
case "LogicalExpression": {
|
||||
// console.log("Logical expression", node);
|
||||
// 0 = first operand, 1 = whitespace, 2 = operator, 3 = whitespace, 4 = second operand
|
||||
const op1 = expressionToKvQueryFilter(node[1]);
|
||||
const op = node[2]; // 1 is whitespace
|
||||
const op2 = expressionToKvQueryFilter(node[3]);
|
||||
return [op[1] as any, op1, op2];
|
||||
}
|
||||
case "ParenthesizedExpression": {
|
||||
return expressionToKvQueryFilter(node[2]);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown expression type: ${expressionType}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
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 { assertEquals } from "../../test_deps.ts";
|
||||
import { astToKvQuery } from "$sb/lib/parse-query.ts";
|
||||
|
||||
const lang = buildMarkdown([]);
|
||||
|
||||
function wrapQueryParse(query: string): AST | null {
|
||||
const tree = parse(lang, `<!-- #query ${query} -->\n$\n<!-- /query -->`);
|
||||
return parseTreeToAST(findNodeOfType(tree, "Query")!);
|
||||
}
|
||||
|
||||
Deno.test("Test directive parser", () => {
|
||||
// const query = ;
|
||||
// console.log("query", query);
|
||||
assertEquals(
|
||||
astToKvQuery(wrapQueryParse(`page where name = "test"`)!),
|
||||
{
|
||||
querySource: "page",
|
||||
filter: ["=", ["attr", "name"], ["string", "test"]],
|
||||
},
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
astToKvQuery(wrapQueryParse(`page where name =~ /test/`)!),
|
||||
{
|
||||
querySource: "page",
|
||||
filter: ["=~", ["attr", "name"], ["regexp", "test", "i"]],
|
||||
},
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
astToKvQuery(wrapQueryParse(`page where parent.name = "test"`)!),
|
||||
{
|
||||
querySource: "page",
|
||||
filter: ["=", ["attr", ["attr", "parent"], "name"], ["string", "test"]],
|
||||
},
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
astToKvQuery(
|
||||
wrapQueryParse(`page where name = "test" and age > 20`)!,
|
||||
),
|
||||
{
|
||||
querySource: "page",
|
||||
filter: ["and", ["=", ["attr", "name"], ["string", "test"]], [">", [
|
||||
"attr",
|
||||
"age",
|
||||
], ["number", 20]]],
|
||||
},
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
astToKvQuery(
|
||||
wrapQueryParse(`page where name = "test" and age > 20 or done = true`)!,
|
||||
),
|
||||
{
|
||||
querySource: "page",
|
||||
filter: ["or", ["and", ["=", ["attr", "name"], ["string", "test"]], [
|
||||
">",
|
||||
[
|
||||
"attr",
|
||||
"age",
|
||||
],
|
||||
["number", 20],
|
||||
]], ["=", ["attr", "done"], ["boolean", true]]],
|
||||
},
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
astToKvQuery(
|
||||
wrapQueryParse(`page where (age <= 20) or task.done = null`)!,
|
||||
),
|
||||
{
|
||||
querySource: "page",
|
||||
filter: ["or", ["<=", ["attr", "age"], ["number", 20]], [
|
||||
"=",
|
||||
[
|
||||
"attr",
|
||||
[
|
||||
"attr",
|
||||
"task",
|
||||
],
|
||||
"done",
|
||||
],
|
||||
["null"],
|
||||
]],
|
||||
},
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
astToKvQuery(
|
||||
wrapQueryParse(`task order by lastModified asc`)!,
|
||||
),
|
||||
{
|
||||
querySource: "task",
|
||||
orderBy: [{ expr: ["attr", "lastModified"], desc: false }],
|
||||
},
|
||||
);
|
||||
assertEquals(
|
||||
astToKvQuery(
|
||||
wrapQueryParse(`task order by lastModified`)!,
|
||||
),
|
||||
{
|
||||
querySource: "task",
|
||||
orderBy: [{ expr: ["attr", "lastModified"], desc: false }],
|
||||
},
|
||||
);
|
||||
assertEquals(
|
||||
astToKvQuery(
|
||||
wrapQueryParse(`task order by lastModified desc, name, age asc`)!,
|
||||
),
|
||||
{
|
||||
querySource: "task",
|
||||
orderBy: [{ expr: ["attr", "lastModified"], desc: true }, {
|
||||
expr: ["attr", "name"],
|
||||
desc: false,
|
||||
}, { expr: ["attr", "age"], desc: false }],
|
||||
},
|
||||
);
|
||||
assertEquals(
|
||||
astToKvQuery(
|
||||
wrapQueryParse(`task order by lastModified desc limit 5`)!,
|
||||
),
|
||||
{
|
||||
querySource: "task",
|
||||
orderBy: [{ expr: ["attr", "lastModified"], desc: true }],
|
||||
limit: ["number", 5],
|
||||
},
|
||||
);
|
||||
assertEquals(
|
||||
astToKvQuery(
|
||||
wrapQueryParse(`task select name, lastModified + 20 as modified`)!,
|
||||
),
|
||||
{
|
||||
querySource: "task",
|
||||
select: [{ name: "name" }, {
|
||||
name: "modified",
|
||||
expr: ["+", ["attr", "lastModified"], ["number", 20]],
|
||||
}],
|
||||
},
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
astToKvQuery(
|
||||
wrapQueryParse(`task render [[my/page]]`)!,
|
||||
),
|
||||
{
|
||||
querySource: "task",
|
||||
render: "my/page",
|
||||
},
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
astToKvQuery(
|
||||
wrapQueryParse(`task where name in ["hello", 1]`)!,
|
||||
),
|
||||
{
|
||||
querySource: "task",
|
||||
filter: ["in", ["attr", "name"], ["array", [["string", "hello"], [
|
||||
"number",
|
||||
1,
|
||||
]]]],
|
||||
},
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
astToKvQuery(
|
||||
wrapQueryParse(`task select today() as today2`)!,
|
||||
),
|
||||
{
|
||||
querySource: "task",
|
||||
select: [{
|
||||
name: "today2",
|
||||
expr: ["call", "today", []],
|
||||
}],
|
||||
},
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
astToKvQuery(
|
||||
wrapQueryParse(`task select today(1, 2, 3) as today`)!,
|
||||
),
|
||||
{
|
||||
querySource: "task",
|
||||
select: [{
|
||||
name: "today",
|
||||
expr: ["call", "today", [["number", 1], ["number", 2], ["number", 3]]],
|
||||
}],
|
||||
},
|
||||
);
|
||||
});
|
||||
+200
-118
@@ -1,145 +1,227 @@
|
||||
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 type QueryFilter = {
|
||||
op: string;
|
||||
prop: string;
|
||||
value: any;
|
||||
};
|
||||
export function evalQueryExpression(
|
||||
val: QueryExpression,
|
||||
obj: any,
|
||||
functionMap: FunctionMap = {},
|
||||
): any {
|
||||
const [type, op1] = val;
|
||||
|
||||
export type QueryOrdering = {
|
||||
orderBy: string;
|
||||
orderDesc: boolean;
|
||||
};
|
||||
|
||||
export type ParsedQuery = {
|
||||
table: string;
|
||||
limit?: number;
|
||||
ordering: QueryOrdering[];
|
||||
/** @deprecated Please use ordering.
|
||||
* Deprecated due to PR #387
|
||||
* Currently holds ordering[0] if exists
|
||||
*/
|
||||
orderBy?: string;
|
||||
/** @deprecated Please use ordering.
|
||||
* Deprecated due to PR #387
|
||||
* Currently holds ordering[0] if exists
|
||||
*/
|
||||
orderDesc?: boolean;
|
||||
filter: QueryFilter[];
|
||||
select?: string[];
|
||||
render?: string;
|
||||
};
|
||||
|
||||
export function applyQuery<T>(parsedQuery: ParsedQuery, records: T[]): T[] {
|
||||
let resultRecords: any[] = [];
|
||||
if (parsedQuery.filter.length === 0) {
|
||||
resultRecords = records.slice();
|
||||
} else {
|
||||
recordLoop:
|
||||
for (const record of records) {
|
||||
const recordAny: any = record;
|
||||
for (const { op, prop, value } of parsedQuery.filter) {
|
||||
switch (op) {
|
||||
case "=": {
|
||||
const recordPropVal = recordAny[prop];
|
||||
if (Array.isArray(recordPropVal) && !Array.isArray(value)) {
|
||||
// Record property is an array, and value is a scalar: find the value in the array
|
||||
if (!recordPropVal.includes(value)) {
|
||||
continue recordLoop;
|
||||
}
|
||||
} else if (Array.isArray(recordPropVal) && Array.isArray(value)) {
|
||||
// Record property is an array, and value is an array: find the value in the array
|
||||
if (!recordPropVal.some((v) => value.includes(v))) {
|
||||
continue recordLoop;
|
||||
}
|
||||
} else if (!(recordPropVal == value)) {
|
||||
// Both are scalars: exact value
|
||||
continue recordLoop;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "!=":
|
||||
if (!(recordAny[prop] != value)) {
|
||||
continue recordLoop;
|
||||
}
|
||||
break;
|
||||
case "<":
|
||||
if (!(recordAny[prop] < value)) {
|
||||
continue recordLoop;
|
||||
}
|
||||
break;
|
||||
case "<=":
|
||||
if (!(recordAny[prop] <= value)) {
|
||||
continue recordLoop;
|
||||
}
|
||||
break;
|
||||
case ">":
|
||||
if (!(recordAny[prop] > value)) {
|
||||
continue recordLoop;
|
||||
}
|
||||
break;
|
||||
case ">=":
|
||||
if (!(recordAny[prop] >= value)) {
|
||||
continue recordLoop;
|
||||
}
|
||||
break;
|
||||
case "=~":
|
||||
// TODO: Cache regexps somehow
|
||||
if (!new RegExp(value).exec(recordAny[prop])) {
|
||||
continue recordLoop;
|
||||
}
|
||||
break;
|
||||
case "!=~":
|
||||
if (new RegExp(value).exec(recordAny[prop])) {
|
||||
continue recordLoop;
|
||||
}
|
||||
break;
|
||||
case "in":
|
||||
if (!value.includes(recordAny[prop])) {
|
||||
continue recordLoop;
|
||||
}
|
||||
break;
|
||||
switch (type) {
|
||||
// Logical operators
|
||||
case "and":
|
||||
return evalQueryExpression(op1, obj, functionMap) &&
|
||||
evalQueryExpression(val[2], obj, functionMap);
|
||||
case "or":
|
||||
return evalQueryExpression(op1, obj, functionMap) ||
|
||||
evalQueryExpression(val[2], obj, functionMap);
|
||||
// Value types
|
||||
case "null":
|
||||
return null;
|
||||
case "number":
|
||||
case "string":
|
||||
case "boolean":
|
||||
return op1;
|
||||
case "regexp":
|
||||
return [op1, val[2]];
|
||||
case "attr": {
|
||||
let attributeVal = obj;
|
||||
if (val.length === 3) {
|
||||
attributeVal = evalQueryExpression(val[1], obj, functionMap);
|
||||
if (attributeVal) {
|
||||
return attributeVal[val[2]];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else if (!val[1]) {
|
||||
return obj;
|
||||
} else {
|
||||
return attributeVal[val[1]];
|
||||
}
|
||||
resultRecords.push(recordAny);
|
||||
}
|
||||
case "array": {
|
||||
return op1.map((v) => evalQueryExpression(v, obj, functionMap));
|
||||
}
|
||||
case "object":
|
||||
return obj;
|
||||
case "call": {
|
||||
const fn = functionMap[op1];
|
||||
if (!fn) {
|
||||
throw new Error(`Unknown function: ${op1}`);
|
||||
}
|
||||
return fn(
|
||||
...val[2].map((v) => evalQueryExpression(v, obj, functionMap)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedQuery.ordering.length > 0) {
|
||||
resultRecords = resultRecords.sort((a: any, b: any) => {
|
||||
for (const { orderBy, orderDesc } of parsedQuery.ordering) {
|
||||
if (a[orderBy] < b[orderBy] || a[orderBy] === undefined) {
|
||||
return orderDesc ? 1 : -1;
|
||||
// Binary operators, here we can pre-calculate the two operand values
|
||||
const val1 = evalQueryExpression(op1, obj, functionMap);
|
||||
const val2 = evalQueryExpression(val[2], obj, functionMap);
|
||||
|
||||
switch (type) {
|
||||
case "+":
|
||||
return val1 + val2;
|
||||
case "-":
|
||||
return val1 - val2;
|
||||
case "*":
|
||||
return val1 * val2;
|
||||
case "/":
|
||||
return val1 / val2;
|
||||
case "%":
|
||||
return val1 % val2;
|
||||
case "=": {
|
||||
if (Array.isArray(val1) && !Array.isArray(val2)) {
|
||||
// Record property is an array, and value is a scalar: find the value in the array
|
||||
if (val1.includes(val2)) {
|
||||
return true;
|
||||
}
|
||||
if (a[orderBy] > b[orderBy] || b[orderBy] === undefined) {
|
||||
return orderDesc ? -1 : 1;
|
||||
} else if (Array.isArray(val1) && Array.isArray(val2)) {
|
||||
// Record property is an array, and value is an array: find the value in the array
|
||||
if (val1.some((v) => val2.includes(v))) {
|
||||
return true;
|
||||
}
|
||||
// Consider them equal. This way helps with comparing arrays (like tags)
|
||||
}
|
||||
return val1 == val2;
|
||||
}
|
||||
case "!=":
|
||||
return val1 != val2;
|
||||
case "=~": {
|
||||
if (!Array.isArray(val2)) {
|
||||
throw new Error(`Invalid regexp: ${val2}`);
|
||||
}
|
||||
const r = new RegExp(val2[0], val2[1]);
|
||||
return r.test(val1);
|
||||
}
|
||||
case "!=~": {
|
||||
if (!Array.isArray(val2)) {
|
||||
throw new Error(`Invalid regexp: ${val2}`);
|
||||
}
|
||||
const r = new RegExp(val2[0], val2[1]);
|
||||
return !r.test(val1);
|
||||
}
|
||||
case "<":
|
||||
return val1 < val2;
|
||||
case "<=":
|
||||
return val1 <= val2;
|
||||
case ">":
|
||||
return val1 > val2;
|
||||
case ">=":
|
||||
return val1 >= val2;
|
||||
case "in":
|
||||
return val2.includes(val1);
|
||||
default:
|
||||
throw new Error(`Unupported operator: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks for an attribute assignment in the expression, and returns the expression assigned to the attribute or throws an error when not found
|
||||
* Side effect: effectively removes the attribute assignment from the expression (by replacing it with true = true)
|
||||
*/
|
||||
export function liftAttributeFilter(
|
||||
expression: QueryExpression | undefined,
|
||||
attributeName: string,
|
||||
): QueryExpression {
|
||||
if (!expression) {
|
||||
throw new Error(`Cannot find attribute assignment for ${attributeName}`);
|
||||
}
|
||||
switch (expression[0]) {
|
||||
case "=": {
|
||||
if (expression[1][0] === "attr" && expression[1][1] === attributeName) {
|
||||
const val = expression[2];
|
||||
// Remove the filter by changing it to true = true
|
||||
expression[1] = ["boolean", true];
|
||||
expression[2] = ["boolean", true];
|
||||
return val;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "and":
|
||||
case "or": {
|
||||
const newOp1 = liftAttributeFilter(expression[1], attributeName);
|
||||
if (newOp1) {
|
||||
return newOp1;
|
||||
}
|
||||
const newOp2 = liftAttributeFilter(expression[2], attributeName);
|
||||
if (newOp2) {
|
||||
return newOp2;
|
||||
}
|
||||
throw new Error(`Cannot find attribute assignment for ${attributeName}`);
|
||||
}
|
||||
}
|
||||
throw new Error(`Cannot find attribute assignment for ${attributeName}`);
|
||||
}
|
||||
|
||||
export function applyQuery<T>(query: Query, allItems: T[]): T[] {
|
||||
// Filter
|
||||
if (query.filter) {
|
||||
allItems = allItems.filter((item) =>
|
||||
evalQueryExpression(query.filter!, item)
|
||||
);
|
||||
}
|
||||
// Add dummy keys, then remove them
|
||||
return applyQueryNoFilterKV(
|
||||
query,
|
||||
allItems.map((v) => ({ key: [], value: v })),
|
||||
).map((v) => v.value);
|
||||
}
|
||||
|
||||
export function applyQueryNoFilterKV(
|
||||
query: Query,
|
||||
allItems: KV[],
|
||||
functionMap: FunctionMap = {}, // TODO: Figure this out later
|
||||
): KV[] {
|
||||
// Order by
|
||||
if (query.orderBy) {
|
||||
allItems.sort((a, b) => {
|
||||
const aVal = a.value;
|
||||
const bVal = b.value;
|
||||
for (const { expr, desc } of query.orderBy!) {
|
||||
const evalA = evalQueryExpression(expr, aVal, functionMap);
|
||||
const evalB = evalQueryExpression(expr, bVal, functionMap);
|
||||
if (
|
||||
evalA < evalB || evalA === undefined
|
||||
) {
|
||||
return desc ? 1 : -1;
|
||||
}
|
||||
if (
|
||||
evalA > evalB || evalB === undefined
|
||||
) {
|
||||
return desc ? -1 : 1;
|
||||
}
|
||||
}
|
||||
// Consider them equal. This helps with comparing arrays (like tags)
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
if (parsedQuery.limit) {
|
||||
resultRecords = resultRecords.slice(0, parsedQuery.limit);
|
||||
}
|
||||
if (parsedQuery.select) {
|
||||
resultRecords = resultRecords.map((rec) => {
|
||||
if (query.select) {
|
||||
for (let i = 0; i < allItems.length; i++) {
|
||||
const rec = allItems[i].value;
|
||||
const newRec: any = {};
|
||||
for (const k of parsedQuery.select!) {
|
||||
newRec[k] = rec[k];
|
||||
for (const { name, expr } of query.select) {
|
||||
newRec[name] = expr
|
||||
? evalQueryExpression(expr, rec, functionMap)
|
||||
: rec[name];
|
||||
}
|
||||
return newRec;
|
||||
});
|
||||
allItems[i].value = newRec;
|
||||
}
|
||||
}
|
||||
return resultRecords;
|
||||
if (query.limit) {
|
||||
const limit = evalQueryExpression(query.limit, {}, functionMap);
|
||||
if (allItems.length > limit) {
|
||||
allItems = allItems.slice(0, limit);
|
||||
}
|
||||
}
|
||||
return allItems;
|
||||
}
|
||||
|
||||
export function removeQueries(pt: ParseTree) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
collectNodesMatching,
|
||||
findParentMatching,
|
||||
nodeAtPos,
|
||||
parseTreeToAST,
|
||||
removeParentPointers,
|
||||
renderToText,
|
||||
replaceNodesMatching,
|
||||
@@ -77,3 +78,9 @@ Deno.test("Test parsing", () => {
|
||||
let mdTree3 = parse(lang, mdTest3);
|
||||
// console.log(JSON.stringify(mdTree3, null, 2));
|
||||
});
|
||||
|
||||
Deno.test("AST functions", () => {
|
||||
const lang = wikiMarkdownLang([]);
|
||||
const mdTree = parse(lang, mdTest1);
|
||||
console.log(JSON.stringify(parseTreeToAST(mdTree), null, 2));
|
||||
});
|
||||
|
||||
@@ -8,6 +8,8 @@ export type ParseTree = {
|
||||
parent?: ParseTree;
|
||||
};
|
||||
|
||||
export type AST = [string, ...AST[]] | string;
|
||||
|
||||
export function addParentPointers(tree: ParseTree) {
|
||||
if (!tree.children) {
|
||||
return;
|
||||
@@ -208,3 +210,19 @@ export function cloneTree(tree: ParseTree): ParseTree {
|
||||
delete newTree.parent;
|
||||
return newTree;
|
||||
}
|
||||
|
||||
export function parseTreeToAST(tree: ParseTree): AST {
|
||||
if (tree.text !== undefined) {
|
||||
return tree.text;
|
||||
}
|
||||
const ast: AST = [tree.type!];
|
||||
for (const node of tree.children!) {
|
||||
if (node.type && !node.type.endsWith("Mark")) {
|
||||
ast.push(parseTreeToAST(node));
|
||||
}
|
||||
if (node.text && node.text.trim()) {
|
||||
ast.push(node.text);
|
||||
}
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { syscall } from "$sb/plugos-syscall/syscall.ts";
|
||||
import { KV, KvKey, KvQuery } from "$sb/types.ts";
|
||||
|
||||
export function set(key: KvKey, value: any): Promise<void> {
|
||||
return syscall("datastore.set", key, value);
|
||||
}
|
||||
|
||||
export function batchSet(kvs: KV[]): Promise<void> {
|
||||
return syscall("datastore.batchSet", kvs);
|
||||
}
|
||||
|
||||
export function get(key: KvKey): Promise<any> {
|
||||
return syscall("datastore.get", key);
|
||||
}
|
||||
|
||||
export function batchGet(keys: KvKey[]): Promise<(any | undefined)[]> {
|
||||
return syscall("datastore.batchGet", keys);
|
||||
}
|
||||
|
||||
export function del(key: KvKey): Promise<void> {
|
||||
return syscall("datastore.delete", key);
|
||||
}
|
||||
|
||||
export function batchDel(keys: KvKey[]): Promise<void> {
|
||||
return syscall("datastore.batchDelete", keys);
|
||||
}
|
||||
|
||||
export function query(
|
||||
query: KvQuery,
|
||||
): Promise<KV[]> {
|
||||
return syscall("datastore.query", query);
|
||||
}
|
||||
|
||||
export function queryDelete(
|
||||
query: KvQuery,
|
||||
): Promise<void> {
|
||||
return syscall("datastore.queryDelete", query);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
export * as asset from "./asset.ts";
|
||||
export * as events from "./event.ts";
|
||||
export * as shell from "./shell.ts";
|
||||
export * as store from "./store.ts";
|
||||
export * as YAML from "./yaml.ts";
|
||||
export * as mq from "./mq.ts";
|
||||
export * from "./syscall.ts";
|
||||
export * as datastore from "./datastore.ts";
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { syscall } from "./syscall.ts";
|
||||
|
||||
export type KV = {
|
||||
key: string;
|
||||
value: any;
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
filter?: Filter[];
|
||||
orderBy?: string;
|
||||
orderDesc?: boolean;
|
||||
limit?: number;
|
||||
select?: string[];
|
||||
};
|
||||
|
||||
export type Filter = {
|
||||
op: string;
|
||||
prop: string;
|
||||
value: any;
|
||||
};
|
||||
|
||||
export function set(key: string, value: any): Promise<void> {
|
||||
return syscall("store.set", key, value);
|
||||
}
|
||||
|
||||
export function batchSet(kvs: KV[]): Promise<void> {
|
||||
return syscall("store.batchSet", kvs);
|
||||
}
|
||||
|
||||
export function get(key: string): Promise<any> {
|
||||
return syscall("store.get", key);
|
||||
}
|
||||
|
||||
export function batchGet(keys: string[]): Promise<(any | undefined)[]> {
|
||||
return syscall("store.batchGet", keys);
|
||||
}
|
||||
|
||||
export function has(key: string): Promise<boolean> {
|
||||
return syscall("store.has", key);
|
||||
}
|
||||
|
||||
export function del(key: string): Promise<void> {
|
||||
return syscall("store.delete", key);
|
||||
}
|
||||
|
||||
export function batchDel(keys: string[]): Promise<void> {
|
||||
return syscall("store.batchDelete", keys);
|
||||
}
|
||||
|
||||
export function queryPrefix(
|
||||
prefix: string,
|
||||
): Promise<{ key: string; value: any }[]> {
|
||||
return syscall("store.queryPrefix", prefix);
|
||||
}
|
||||
|
||||
export function deletePrefix(prefix: string): Promise<void> {
|
||||
return syscall("store.deletePrefix", prefix);
|
||||
}
|
||||
|
||||
export function deleteAll(): Promise<void> {
|
||||
return syscall("store.deleteAll");
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
import { syscall } from "./syscall.ts";
|
||||
|
||||
/**
|
||||
* Implements a very simple (string) key value store for the client.
|
||||
* Generally should only be used to set some client-specific states, such as preferences.
|
||||
*/
|
||||
|
||||
export function set(key: string, value: any): Promise<void> {
|
||||
return syscall("clientStore.set", key, value);
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ export function filterBox(
|
||||
}
|
||||
|
||||
export function showPanel(
|
||||
id: "lhs" | "rhs" | "bhs" | "modal",
|
||||
id: "lhs" | "rhs" | "bhs" | "modal" | "ps",
|
||||
mode: number,
|
||||
html: string,
|
||||
script = "",
|
||||
@@ -80,7 +80,9 @@ export function showPanel(
|
||||
return syscall("editor.showPanel", id, mode, html, script);
|
||||
}
|
||||
|
||||
export function hidePanel(id: "lhs" | "rhs" | "bhs" | "modal"): Promise<void> {
|
||||
export function hidePanel(
|
||||
id: "lhs" | "rhs" | "bhs" | "modal" | "ps",
|
||||
): Promise<void> {
|
||||
return syscall("editor.hidePanel", id);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { syscall } from "$sb/silverbullet-syscall/syscall.ts";
|
||||
|
||||
/**
|
||||
* Renders
|
||||
* @param template
|
||||
* @param obj
|
||||
* @param globals
|
||||
* @returns
|
||||
*/
|
||||
export function renderTemplate(
|
||||
template: string,
|
||||
obj: any,
|
||||
globals: Record<string, any> = {},
|
||||
): Promise<string> {
|
||||
return syscall("handlebars.renderTemplate", template, obj, globals);
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import type { Query } from "../plugos-syscall/store.ts";
|
||||
import { syscall } from "./syscall.ts";
|
||||
|
||||
export type KV = {
|
||||
key: string;
|
||||
value: any;
|
||||
};
|
||||
|
||||
export function set(
|
||||
page: string,
|
||||
key: string,
|
||||
value: any,
|
||||
): Promise<void> {
|
||||
return syscall("index.set", page, key, value);
|
||||
}
|
||||
|
||||
export function batchSet(page: string, kvs: KV[]): Promise<void> {
|
||||
return syscall("index.batchSet", page, kvs);
|
||||
}
|
||||
|
||||
export function get(page: string, key: string): Promise<any> {
|
||||
return syscall("index.get", page, key);
|
||||
}
|
||||
|
||||
export function del(page: string, key: string): Promise<void> {
|
||||
return syscall("index.delete", page, key);
|
||||
}
|
||||
|
||||
export function queryPrefix(
|
||||
prefix: string,
|
||||
): Promise<{ key: string; page: string; value: any }[]> {
|
||||
return syscall("index.queryPrefix", prefix);
|
||||
}
|
||||
|
||||
export function query(
|
||||
query: Query,
|
||||
): Promise<{ key: string; page: string; value: any }[]> {
|
||||
return syscall("index.query", query);
|
||||
}
|
||||
|
||||
export function clearPageIndexForPage(page: string): Promise<void> {
|
||||
return syscall("index.clearPageIndexForPage", page);
|
||||
}
|
||||
|
||||
export function deletePrefixForPage(
|
||||
page: string,
|
||||
prefix: string,
|
||||
): Promise<void> {
|
||||
return syscall("index.deletePrefixForPage", page, prefix);
|
||||
}
|
||||
|
||||
export function clearPageIndex(): Promise<void> {
|
||||
return syscall("index.clearPageIndex");
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { syscall } from "$sb/silverbullet-syscall/syscall.ts";
|
||||
|
||||
import type { ParseTree } from "$sb/lib/tree.ts";
|
||||
|
||||
/**
|
||||
* Parses a piece of code using any of the supported SB languages, see `common/languages.ts` for a list
|
||||
* @param language the language to parse
|
||||
* @param code the code to parse
|
||||
* @returns a ParseTree representation of the code
|
||||
*/
|
||||
export function parseLanguage(
|
||||
language: string,
|
||||
code: string,
|
||||
): Promise<ParseTree> {
|
||||
return syscall("language.parseLanguage", language, code);
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
export * as editor from "./editor.ts";
|
||||
export * as index from "./index.ts";
|
||||
export * as markdown from "./markdown.ts";
|
||||
export * as space from "./space.ts";
|
||||
export * as system from "./system.ts";
|
||||
export * as clientStore from "./clientStore.ts";
|
||||
export * as sync from "./sync.ts";
|
||||
export * as debug from "./debug.ts";
|
||||
export * as language from "./language.ts";
|
||||
export * as handlebars from "./handlebars.ts";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { syscall } from "./syscall.ts";
|
||||
import type { AttachmentMeta, PageMeta } from "../../web/types.ts";
|
||||
import { FileMeta } from "$sb/types.ts";
|
||||
import { AttachmentMeta, FileMeta, PageMeta } from "$sb/types.ts";
|
||||
|
||||
export function listPages(unfiltered = false): Promise<PageMeta[]> {
|
||||
return syscall("space.listPages", unfiltered);
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { syscall } from "./syscall.ts";
|
||||
|
||||
export function set(key: string, value: any): Promise<void> {
|
||||
return syscall("store.set", key, value);
|
||||
}
|
||||
|
||||
export function get(key: string): Promise<any> {
|
||||
return syscall("store.get", key);
|
||||
}
|
||||
|
||||
export function del(key: string): Promise<void> {
|
||||
return syscall("store.delete", key);
|
||||
}
|
||||
+97
-7
@@ -1,3 +1,28 @@
|
||||
export type FileMeta = {
|
||||
name: string;
|
||||
lastModified: number;
|
||||
contentType: string;
|
||||
size: number;
|
||||
perm: "ro" | "rw";
|
||||
noSync?: boolean;
|
||||
};
|
||||
|
||||
export type PageMeta = {
|
||||
name: string;
|
||||
lastModified: number;
|
||||
lastOpened?: number;
|
||||
perm: "ro" | "rw";
|
||||
};
|
||||
|
||||
export type AttachmentMeta = {
|
||||
name: string;
|
||||
contentType: string;
|
||||
lastModified: number;
|
||||
size: number;
|
||||
perm: "ro" | "rw";
|
||||
};
|
||||
|
||||
// Message Queue related types
|
||||
export type MQMessage = {
|
||||
id: string;
|
||||
queue: string;
|
||||
@@ -16,11 +41,76 @@ export type MQSubscribeOptions = {
|
||||
pollInterval?: number;
|
||||
};
|
||||
|
||||
export type FileMeta = {
|
||||
// Key-Value Store related types
|
||||
export type KvKey = string[];
|
||||
|
||||
export type KV<T = any> = {
|
||||
key: KvKey;
|
||||
value: T;
|
||||
};
|
||||
|
||||
export type OrderBy = {
|
||||
expr: QueryExpression;
|
||||
desc: boolean;
|
||||
};
|
||||
|
||||
export type Select = {
|
||||
name: string;
|
||||
lastModified: number;
|
||||
contentType: string;
|
||||
size: number;
|
||||
perm: "ro" | "rw";
|
||||
noSync?: boolean;
|
||||
} & Record<string, any>;
|
||||
expr?: QueryExpression;
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
querySource?: string;
|
||||
filter?: QueryExpression;
|
||||
orderBy?: OrderBy[];
|
||||
select?: Select[];
|
||||
limit?: QueryExpression;
|
||||
render?: string;
|
||||
};
|
||||
|
||||
export type KvQuery = Omit<Query, "querySource"> & {
|
||||
prefix?: KvKey;
|
||||
};
|
||||
|
||||
export type QueryExpression =
|
||||
| ["and", QueryExpression, QueryExpression]
|
||||
| ["or", QueryExpression, QueryExpression]
|
||||
| ["=", QueryExpression, QueryExpression]
|
||||
| ["!=", QueryExpression, QueryExpression]
|
||||
| ["=~", QueryExpression, QueryExpression]
|
||||
| ["!=~", QueryExpression, QueryExpression]
|
||||
| ["<", QueryExpression, QueryExpression]
|
||||
| ["<=", QueryExpression, QueryExpression]
|
||||
| [">", QueryExpression, QueryExpression]
|
||||
| [">=", QueryExpression, QueryExpression]
|
||||
| ["in", QueryExpression, QueryExpression]
|
||||
| ["attr", QueryExpression, string]
|
||||
| ["attr", string]
|
||||
| ["number", number]
|
||||
| ["string", string]
|
||||
| ["boolean", boolean]
|
||||
| ["null"]
|
||||
| ["array", QueryExpression[]]
|
||||
| ["object", Record<string, any>]
|
||||
| ["regexp", string, string] // regex, modifier
|
||||
| ["+", QueryExpression, QueryExpression]
|
||||
| ["-", QueryExpression, QueryExpression]
|
||||
| ["*", QueryExpression, QueryExpression]
|
||||
| ["%", QueryExpression, QueryExpression]
|
||||
| ["/", QueryExpression, QueryExpression]
|
||||
| ["call", string, QueryExpression[]];
|
||||
|
||||
export type FunctionMap = Record<string, (...args: any[]) => any>;
|
||||
|
||||
/**
|
||||
* An ObjectValue that can be indexed by the `index` plug, needs to have a minimum of
|
||||
* of two fields:
|
||||
* - ref: a unique reference (id) for the object, ideally a page reference
|
||||
* - tags: a list of tags that the object belongs to
|
||||
*/
|
||||
export type ObjectValue<T> = {
|
||||
ref: string;
|
||||
tags: string[];
|
||||
} & T;
|
||||
|
||||
export type ObjectQuery = Omit<Query, "prefix">;
|
||||
|
||||
Reference in New Issue
Block a user