import { type AST, parseTreeToAST } from "./tree.ts"; import type { Query, QueryExpression } from "../types.ts"; import { language } from "@silverbulletmd/silverbullet/syscalls"; 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, expressionToKvQueryExpression(clause[2]), ]; } else { query.filter = expressionToKvQueryExpression(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: cleanIdentifier(select[1][1] as string), }); } else { query.select.push({ name: cleanIdentifier(select[3][1] as string), expr: expressionToKvQueryExpression(select[1]), }); } } } break; } case "RenderClause": { // console.log("Render clause", clause); const pageRef = (clause as any[]).find((c) => c[0] === "PageRef"); query.render = pageRef[1].slice(2, -2); query.renderAll = !!(clause as any[]).find((c) => c[0] === "all"); break; } default: throw new Error(`Unknown clause type: ${clauseType}`); } } return query; } function cleanIdentifier(s: string): string { if (s.startsWith("`") && s.endsWith("`")) { return s.slice(1, -1); } return s; } 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]), cleanIdentifier(node[3][1] as string), ]; } case "Identifier": return ["attr", cleanIdentifier(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 "Object": { const objAttrs: [string, QueryExpression][] = []; for (const kv of node.slice(2)) { if (typeof kv === "string") { continue; } const [_, key, _colon, expr] = kv; objAttrs.push([ key[1].slice(1, -1) as string, expressionToKvQueryExpression( expr, ), ]); } return ["object", objAttrs]; } 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": { const op1 = expressionToKvQueryExpression(node[1]); const op = node[2]; const op2 = expressionToKvQueryExpression(node[3]); return [op[1] as any, op1, op2]; } case "ParenthesizedExpression": { return expressionToKvQueryExpression(node[2]); } case "Call": { // console.log("Call", node); const fn = cleanIdentifier(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)]; } case "UnaryExpression": { // console.log("UnaryExpression", node); if (node[1][0] === "NotKW" || node[1][0] === "!") { return ["not", expressionToKvQueryExpression(node[2])]; } else if (node[1][0] === "-") { return ["-", expressionToKvQueryExpression(node[2])]; } throw new Error(`Unknown unary expression: ${node[1][0]}`); } case "TopLevelVal": { return ["attr"]; } case "GlobalIdentifier": { return ["global", (node[1] as string).substring(1)]; } case "TernaryExpression": { const [_, condition, _space, ifTrue, _space2, ifFalse] = node; return [ "?", expressionToKvQueryExpression(condition), expressionToKvQueryExpression(ifTrue), expressionToKvQueryExpression(ifFalse), ]; } case "QueryExpression": { return ["query", astToKvQuery(node[2])]; } case "PageRef": { return ["pageref", (node[1] as string).slice(2, -2)]; } default: throw new Error(`Not supported: ${node[0]}`); } } export async function parseQuery(query: string): Promise { const queryAST = parseTreeToAST( await language.parseLanguage( "query", query, ), ); return astToKvQuery(queryAST[1]); }