diff --git a/common/markdown_parser/parse-expression.js b/common/markdown_parser/parse-expression.js index 68811536..034657a1 100644 --- a/common/markdown_parser/parse-expression.js +++ b/common/markdown_parser/parse-expression.js @@ -10,7 +10,7 @@ export const parser = LRParser.deserialize({ maxTerm: 84, skippedNodes: [0,1], repeatNodeCount: 5, - tokenData: "=Z~R!RX^$[pq$[qr%Prs%fst&Tuv&lwx&qxy'Zyz'`z{'e{|'j|}'o}!O't!O!P'y!P!Q(O!Q![*g![!]*o!^!_*t!_!`+R!`!a+`!a!b+m!b!c+r!c!},a!}#O-c#P#Q.`#S#T.e#T#U.}#U#W,a#W#X0z#X#Y,a#Y#Z1k#Z#],a#]#^4u#^#b,a#b#c6R#c#d8O#d#h,a#h#i;o#i#o,a#o#p=P#q#r=U#y#z$[$f$g$[#BY#BZ$[$IS$I_$[$I|$JO$[$JT$JU$[$KV$KW$[&FU&FV$[~$aY!Y~X^$[pq$[#y#z$[$f$g$[#BY#BZ$[$IS$I_$[$I|$JO$[$JT$JU$[$KV$KW$[&FU&FV$[R%UP!fP!_!`%XQ%^P!kQ#r#s%aQ%fO!oQ~%iTOr%frs%xs;'S%f;'S;=`%}<%lO%f~%}OU~~&QP;=`<%l%f~&YSP~OY&TZ;'S&T;'S;=`&f<%lO&T~&iP;=`<%l&T~&qO!r~~&tTOw&qwx%xx;'S&q;'S;=`'T<%lO&q~'WP;=`<%l&q~'`O!`~~'eO!a~~'jO!p~~'oO!s~~'tO!]~~'yO!g~~(OO!_~R(TW!qQOY(mZ](m^!P(m!Q#O(m#O#P)b#P;'S(m;'S;=`*a<%lO(mP(pXOY(mZ](m^!P(m!P!Q)]!Q#O(m#O#P)b#P;'S(m;'S;=`*a<%lO(mP)bOXPP)eRO;'S(m;'S;=`)n;=`O(mP)qYOY(mZ](m^!P(m!P!Q)]!Q#O(m#O#P)b#P;'S(m;'S;=`*a;=`<%l(m<%lO(mP*dP;=`<%l(m~*lPT~!Q![*g~*tO!u~~*yP!h~!_!`*|~+RO!i~~+WP!j~#r#s+Z~+`O!n~~+eP!m~!_!`+h~+mO!l~~+rO!t~~+uQ!c!}+{#T#o+{~,QT]~}!O+{!Q![+{!c!}+{#R#S+{#T#o+{V,hUfSYR}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#o,aS-PUfS}!O,z!P!Q,z!Q![,z!c!},z#R#S,z#T#o,z~-hP!Z~!}#O-k~-nTO#P-k#P#Q-}#Q;'S-k;'S;=`.Y<%lO-k~.QP#P#Q.T~.YOx~~.]P;=`<%l-k~.eO!^~~.hTO#S.e#S#T%x#T;'S.e;'S;=`.w<%lO.e~.zP;=`<%l.eV/UWfSYR}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#g,a#g#h/n#h#o,aV/uWfSYR}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#V,a#V#W0_#W#o,aV0hUfSoQYR}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#o,aV1RWfSYR}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#X,a#X#Y.}#Y#o,aV1rVfSYR}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#U2X#U#o,aV2`WfSYR}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#`,a#`#a2x#a#o,aV3PWfSYR}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#g,a#g#h3i#h#o,aV3pWfSYR}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#X,a#X#Y4Y#Y#o,aV4cUfSWPYR}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#o,aV4|WfSYR}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#b,a#b#c5f#c#o,aV5oUfS|QYR}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#o,aV6YWfSYR}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#c,a#c#d6r#d#o,aV6yWfSYR}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#h,a#h#i7c#i#o,aV7lUfSzPYR}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#o,aV8VWfSYR}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#f,a#f#g8o#g#o,aV8vWfSYR}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#W,a#W#X9`#X#o,aV9gWfSYR}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#X,a#X#Y:P#Y#o,aV:WWfSYR}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#f,a#f#g:p#g#o,aV:wVfSYRpq;^}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#o,aR;aP#U#V;dR;gP#m#n;jR;oOlRV;vWfSYR}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#f,a#f#g<`#g#o,aV spec_Identifier[value] || -1}], diff --git a/common/markdown_parser/parse-query.js b/common/markdown_parser/parse-query.js index 18948e35..5b3c7ae3 100644 --- a/common/markdown_parser/parse-query.js +++ b/common/markdown_parser/parse-query.js @@ -10,7 +10,7 @@ export const parser = LRParser.deserialize({ maxTerm: 84, skippedNodes: [0,1], repeatNodeCount: 5, - tokenData: "=Z~R!RX^$[pq$[qr%Prs%fst&Tuv&lwx&qxy'Zyz'`z{'e{|'j|}'o}!O't!O!P'y!P!Q(O!Q![*g![!]*o!^!_*t!_!`+R!`!a+`!a!b+m!b!c+r!c!},a!}#O-c#P#Q.`#S#T.e#T#U.}#U#W,a#W#X0z#X#Y,a#Y#Z1k#Z#],a#]#^4u#^#b,a#b#c6R#c#d8O#d#h,a#h#i;o#i#o,a#o#p=P#q#r=U#y#z$[$f$g$[#BY#BZ$[$IS$I_$[$I|$JO$[$JT$JU$[$KV$KW$[&FU&FV$[~$aY!Y~X^$[pq$[#y#z$[$f$g$[#BY#BZ$[$IS$I_$[$I|$JO$[$JT$JU$[$KV$KW$[&FU&FV$[U%UP!dQ!_!`%XS%^P!iS#r#s%aS%fO!mS~%iTOr%frs%xs;'S%f;'S;=`%}<%lO%f~%}OZ~~&QP;=`<%l%f~&YSP~OY&TZ;'S&T;'S;=`&f<%lO&T~&iP;=`<%l&T~&qO!p~~&tTOw&qwx%xx;'S&q;'S;=`'T<%lO&q~'WP;=`<%l&q~'`O!`~~'eO!a~~'jO!n~~'oO!q~~'tO!]~~'yO!e~~(OO!_~U(TW!oSOY(mZ](m^!P(m!Q#O(m#O#P)b#P;'S(m;'S;=`*a<%lO(mQ(pXOY(mZ](m^!P(m!P!Q)]!Q#O(m#O#P)b#P;'S(m;'S;=`*a<%lO(mQ)bO^QQ)eRO;'S(m;'S;=`)n;=`O(mQ)qYOY(mZ](m^!P(m!P!Q)]!Q#O(m#O#P)b#P;'S(m;'S;=`*a;=`<%l(m<%lO(mQ*dP;=`<%l(m~*lPY~!Q![*g~*tO!s~~*yP!f~!_!`*|~+RO!g~~+WP!h~#r#s+Z~+`O!l~~+eP!k~!_!`+h~+mO!j~~+rO!r~~+uQ!c!}+{#T#o+{~,QTa~}!O+{!Q![+{!c!}+{#R#S+{#T#o+{V,hUSPUU}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#o,aP-PUSP}!O,z!P!Q,z!Q![,z!c!},z#R#S,z#T#o,z~-hP!Z~!}#O-k~-nTO#P-k#P#Q-}#Q;'S-k;'S;=`.Y<%lO-k~.QP#P#Q.T~.YOo~~.]P;=`<%l-k~.eO!^~~.hTO#S.e#S#T%x#T;'S.e;'S;=`.w<%lO.e~.zP;=`<%l.eV/UWSPUU}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#g,a#g#h/n#h#o,aV/uWSPUU}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#V,a#V#W0_#W#o,aV0hUSPxSUU}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#o,aV1RWSPUU}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#X,a#X#Y.}#Y#o,aV1rVSPUU}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#U2X#U#o,aV2`WSPUU}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#`,a#`#a2x#a#o,aV3PWSPUU}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#g,a#g#h3i#h#o,aV3pWSPUU}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#X,a#X#Y4Y#Y#o,aV4cUSP]QUU}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#o,aV4|WSPUU}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#b,a#b#c5f#c#o,aV5oUSPlSUU}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#o,aV6YWSPUU}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#c,a#c#d6r#d#o,aV6yWSPUU}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#h,a#h#i7c#i#o,aV7lUSPjQUU}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#o,aV8VWSPUU}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#f,a#f#g8o#g#o,aV8vWSPUU}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#W,a#W#X9`#X#o,aV9gWSPUU}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#X,a#X#Y:P#Y#o,aV:WWSPUU}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#f,a#f#g:p#g#o,aV:wVSPUUpq;^}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#o,aU;aP#U#V;dU;gP#m#n;jU;oOuUV;vWSPUU}!O,a!P!Q,z!Q![,a!c!},a#R#S,a#T#f,a#f#g<`#g#o,aV spec_Identifier[value] || -1}], diff --git a/common/markdown_parser/query.grammar b/common/markdown_parser/query.grammar index 33c5664e..d88bf653 100644 --- a/common/markdown_parser/query.grammar +++ b/common/markdown_parser/query.grammar @@ -125,14 +125,13 @@ Bool { TagIdentifier { @asciiLetter (@asciiLetter | @digit | "-" | "_" | "/" )* } - Identifier { @asciiLetter (@asciiLetter | @digit | "-" | "_")* } + Identifier { @asciiLetter (@asciiLetter | @digit | "-" | "_")* | "`" ![`]* "`" } GlobalIdentifier { "@" @asciiLetter (@asciiLetter | @digit | "-" | "_")* } String { "\"" ![\"]* "\"" | "'" ![']* "'" - | "`" ![`]* "`" } PageRef { "[[" ![\]]* "]]" diff --git a/plug-api/lib/parse-query.ts b/plug-api/lib/parse-query.ts index 3adaca89..3aa594d2 100644 --- a/plug-api/lib/parse-query.ts +++ b/plug-api/lib/parse-query.ts @@ -63,10 +63,12 @@ export function astToKvQuery( query.select = []; } if (select.length === 2) { - query.select.push({ name: select[1][1] as string }); + query.select.push({ + name: cleanIdentifier(select[1][1] as string), + }); } else { query.select.push({ - name: select[3][1] as string, + name: cleanIdentifier(select[3][1] as string), expr: expressionToKvQueryExpression(select[1]), }); } @@ -88,6 +90,13 @@ export function astToKvQuery( 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]); @@ -98,11 +107,11 @@ export function expressionToKvQueryExpression(node: AST): QueryExpression { return [ "attr", expressionToKvQueryExpression(node[1]), - node[3][1] as string, + cleanIdentifier(node[3][1] as string), ]; } case "Identifier": - return ["attr", node[1] as string]; + return ["attr", cleanIdentifier(node[1] as string)]; case "String": return ["string", (node[1] as string).slice(1, -1)]; case "Number": @@ -155,7 +164,7 @@ export function expressionToKvQueryExpression(node: AST): QueryExpression { } case "Call": { // console.log("Call", node); - const fn = node[1][1] as string; + const fn = cleanIdentifier(node[1][1] as string); const args: AST[] = []; for (const expr of node.slice(2)) { if (expr[0] === "Expression") { diff --git a/plug-api/lib/parser-query.test.ts b/plug-api/lib/parser-query.test.ts index 69b90f5f..0db3fd9d 100644 --- a/plug-api/lib/parser-query.test.ts +++ b/plug-api/lib/parser-query.test.ts @@ -43,6 +43,41 @@ Deno.test("Test query parser", () => { }, ); + assertEquals( + astToKvQuery(wrapQueryParse(`page where name = "hello"`)!), + { + querySource: "page", + filter: ["=", ["attr", "name"], [ + "string", + "hello", + ]], + }, + ); + + assertEquals( + astToKvQuery(wrapQueryParse("page where `name`")!), + { + querySource: "page", + filter: ["attr", "name"], + }, + ); + + assertEquals( + astToKvQuery(wrapQueryParse("page where `something`.`name`")!), + { + querySource: "page", + filter: ["attr", ["attr", "something"], "name"], + }, + ); + + assertEquals( + astToKvQuery(wrapQueryParse("page select 10 as `something`, `name`")!), + { + querySource: "page", + select: [{ name: "something", expr: ["number", 10] }, { name: "name" }], + }, + ); + assertEquals( astToKvQuery(wrapQueryParse(`page where @page`)!), { @@ -375,7 +410,10 @@ Deno.test("Test query parser", () => { ), { querySource: "page", - filter: [">", ["call", "myCall", [["-", ["number", 3]]]], ["-", ["number", 4], ["number", 2]]], + filter: [">", ["call", "myCall", [["-", ["number", 3]]]], ["-", [ + "number", + 4, + ], ["number", 2]]], }, ); diff --git a/website/Expression Language.md b/website/Expression Language.md index 734208ca..8c681f54 100644 --- a/website/Expression Language.md +++ b/website/Expression Language.md @@ -5,13 +5,14 @@ Examples in this page will be demonstrated by embedding expressions inside of a While a custom language, it takes a lot of inspiration from JavaScript and SQL, but includes some features very specific to SilverBullet, including syntax for embedded queries and page references. # Primitive values -* strings: `"a string"`, `'a string'` or `a string` (with backticks). Escaping is not currently supported, so use your quotes wisely. +* strings: `"a string"`, or `'a string'`. Escaping is not currently supported, so use your quotes wisely. * numbers: `10` * booleans: `true` or `false` * regular expressions: `/[a-z]+/` * null: `null` * lists: `["value 1", 10, false]` * objects: `{"name": "Jack", "age": 1232}` +* identifiers: starting with a letter, followed by alphanumerics, `_` or `-`. Identifiers can also be surrounded with backticks: ` and in that case contain any non-backtick characters, including spaces. * attributes: * `.` for the current object * `attr` for the current object’s attribute with the name `attr` @@ -20,9 +21,9 @@ While a custom language, it takes a lot of inspiration from JavaScript and SQL, ## Examples ```template -String expression: {{"This is a string"}} -List expression: {{[1, 2, 3]}} -Attribute of variable: {{@page.name}} +String expression: {{"This is a string"}} +List expression: {{[1, 2, 3]}} +Attribute of variable: {{@page.name}} ``` # Function calls diff --git a/website/Query Language.md b/website/Query Language.md index 3c283d32..f34a1d94 100644 --- a/website/Query Language.md +++ b/website/Query Language.md @@ -52,12 +52,12 @@ person where page = @page.name limit 1 ``` ## select -You can use the `select` clause to select only specific attributes from the result set. You can use it either simply as `select attribute1, attribute2` but also select the value of certain expressions and give them a name via the `select age + 1 as nextYear` syntax: +You can use the `select` clause to select only specific attributes from the result set. You can use it either simply as `select attribute1, attribute2` but also select the value of certain expressions and give them a name, even one containing spaces using the backtick identifier syntax: ```query person where page = @page.name -select name, age, age + 1 as nextYear +select name, age, age + 1 as `next year` ``` ## render each