From bf6a34f82c33ccd0aece3010d4335d47d931da6a Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Mon, 13 Jan 2025 20:25:39 +0100 Subject: [PATCH] Lua Integrated Query --- common/space_lua.ts | 16 ---- common/space_lua/ast.ts | 4 +- common/space_lua/eval.ts | 24 ++--- common/space_lua/language_test.lua | 7 ++ common/space_lua/lua.grammar | 4 +- common/space_lua/parse-lua.js | 8 +- common/space_lua/parse.test.ts | 2 +- common/space_lua/parse.ts | 27 +++--- common/space_lua/query_collection.test.ts | 26 ++--- common/space_lua/query_collection.ts | 39 +++----- common/space_lua/runtime.ts | 12 +++ common/space_lua/stdlib/table.ts | 23 +++++ lib/plugos/syscalls/datastore.ts | 8 +- web/cm_plugins/lua_widget.ts | 15 +-- web/cm_plugins/smart_quotes.ts | 1 + web/styles/editor.scss | 16 +++- website/Space Lua.md | 94 +++++++++++------- website/Space Lua/Lua Integrated Query.md | 110 ++++++++++++++++++++++ website/Space Lua/stdlib.md | 33 +++++++ 19 files changed, 324 insertions(+), 145 deletions(-) create mode 100644 website/Space Lua/Lua Integrated Query.md create mode 100644 website/Space Lua/stdlib.md diff --git a/common/space_lua.ts b/common/space_lua.ts index be9f919b..d4598f2e 100644 --- a/common/space_lua.ts +++ b/common/space_lua.ts @@ -2,19 +2,16 @@ import type { System } from "../lib/plugos/system.ts"; import type { ScriptObject } from "../plugs/index/script.ts"; import { LuaEnv, - LuaFunction, LuaRuntimeError, LuaStackFrame, } from "$common/space_lua/runtime.ts"; import { parse as parseLua } from "$common/space_lua/parse.ts"; import { evalStatement } from "$common/space_lua/eval.ts"; -import { jsToLuaValue } from "$common/space_lua/runtime.ts"; import { type PageRef, parsePageRef, } from "@silverbulletmd/silverbullet/lib/page_ref"; import type { ScriptEnvironment } from "$common/space_script.ts"; -import { luaValueToJS } from "$common/space_lua/runtime.ts"; import type { ASTCtx } from "$common/space_lua/ast.ts"; import { buildLuaEnv } from "$common/space_lua_api.ts"; @@ -59,19 +56,6 @@ export class SpaceLuaEnvironment { } } - // Find all functions and register them - for (const globalName of this.env.keys()) { - const value = this.env.get(globalName); - if (value instanceof LuaFunction) { - console.log( - `[Lua] Registering global function '${globalName}' (source: ${value.body.ctx.ref})`, - ); - scriptEnv.registerFunction({ name: globalName }, (...args: any[]) => { - const sf = new LuaStackFrame(tl, value.body.ctx); - return luaValueToJS(value.call(sf, ...args.map(jsToLuaValue))); - }); - } - } console.log("[Lua] Loaded", allScripts.length, "scripts"); } } diff --git a/common/space_lua/ast.ts b/common/space_lua/ast.ts index a30af339..41503b23 100644 --- a/common/space_lua/ast.ts +++ b/common/space_lua/ast.ts @@ -271,7 +271,7 @@ export type LuaQueryClause = export type LuaFromClause = { type: "From"; - name: string; + name?: string; expression: LuaExpression; } & ASTContext; @@ -299,5 +299,5 @@ export type LuaOrderBy = { export type LuaSelectClause = { type: "Select"; - tableConstructor: LuaTableConstructor; + expression: LuaExpression; } & ASTContext; diff --git a/common/space_lua/eval.ts b/common/space_lua/eval.ts index df3e6aca..b1fc3a8a 100644 --- a/common/space_lua/eval.ts +++ b/common/space_lua/eval.ts @@ -7,6 +7,7 @@ import type { import { evalPromiseValues } from "$common/space_lua/util.ts"; import { luaCall, + luaEquals, luaSet, type LuaStackFrame, } from "$common/space_lua/runtime.ts"; @@ -256,7 +257,7 @@ export function evalExpression( if (!findFromClause) { throw new LuaRuntimeError("No from clause found", sf.withCtx(e.ctx)); } - const objectVariable = findFromClause.name; + const objectVariable = findFromClause.name || "_"; const objectExpression = findFromClause.expression; return Promise.resolve(evalExpression(objectExpression, env, sf)).then( async (collection: LuaValue) => { @@ -298,20 +299,7 @@ export function evalExpression( break; } case "Select": { - query.select = clause.tableConstructor.fields.map((f) => { - if (f.type === "PropField") { - return { - name: f.key, - expr: f.value, - }; - } else { - throw new LuaRuntimeError( - "Select fields must be named", - sf.withCtx(f.ctx), - ); - } - }); - + query.select = clause.expression; break; } case "Limit": { @@ -524,15 +512,15 @@ const operatorsMetaMethods: Record a === b, + nativeImplementation: (a, b) => luaEquals(a, b), }, "~=": { metaMethod: "__ne", - nativeImplementation: (a, b) => a !== b, + nativeImplementation: (a, b) => !luaEquals(a, b), }, "!=": { metaMethod: "__ne", - nativeImplementation: (a, b) => a !== b, + nativeImplementation: (a, b) => !luaEquals(a, b), }, "<": { metaMethod: "__lt", nativeImplementation: (a, b) => a < b }, "<=": { metaMethod: "__le", nativeImplementation: (a, b) => a <= b }, diff --git a/common/space_lua/language_test.lua b/common/space_lua/language_test.lua index ab38ddb3..58c27cb8 100644 --- a/common/space_lua/language_test.lua +++ b/common/space_lua/language_test.lua @@ -730,3 +730,10 @@ assert_equal(r[2].name, "Jane") assert_equal(r[2].age, 21) assert_equal(r[1].lastModified, nil) assert_equal(r[2].lastModified, nil) + +-- Random select test +local r = query [[from {1, 2, 3} select _ + 1]] +assert_equal(#r, 3) +assert_equal(r[1], 2) +assert_equal(r[2], 3) +assert_equal(r[3], 4) diff --git a/common/space_lua/lua.grammar b/common/space_lua/lua.grammar index 4e99cafc..b8ab174d 100644 --- a/common/space_lua/lua.grammar +++ b/common/space_lua/lua.grammar @@ -97,12 +97,12 @@ QueryClause { LimitClause } -FromClause { ckw<"from"> Name "=" exp } +FromClause { ckw<"from"> (Name "=")? exp } WhereClause { ckw<"where"> exp } LimitClause { ckw<"limit"> exp ("," exp)? } OrderByClause { ckw<"order"> ckw<"by"> list } OrderBy { exp ckw<"desc">? } -SelectClause { ckw<"select"> TableConstructor } +SelectClause { ckw<"select"> exp } field[@isGroup=Field] { diff --git a/common/space_lua/parse-lua.js b/common/space_lua/parse-lua.js index 76d1fea4..6835f3e7 100644 --- a/common/space_lua/parse-lua.js +++ b/common/space_lua/parse-lua.js @@ -3,9 +3,9 @@ import {LRParser} from "@lezer/lr" const spec_identifier = {__proto__:null,break:16, goto:20, do:24, end:26, while:30, nil:32, true:34, false:36, or:80, and:82, not:104, function:114, query:122, from:129, where:133, order:137, by:139, desc:143, select:147, limit:151, repeat:154, until:156, if:160, then:162, elseif:164, else:166, for:170, in:178, local:188, return:204} export const parser = LRParser.deserialize({ version: 14, - states: "EOO!ZQPOOOOQO'#Cc'#CcO!UQPO'#CaO!bQPOOOOQO'#E{'#E{O!vQQO'#CwO$`QPO'#EzOOQO'#Ez'#EzO$jQPO'#EzOOQO'#E`'#E`O%yQPO'#E_OOQO'#Ev'#EvOOQO'#Eg'#EgO&OQPO'#C_OOQO'#C_'#C_QOQPOOO!UQPO'#CeO&cQPO'#CgO!vQQO'#CjO&jQPO'#DzO!vQQO'#D}O!UQPO'#ESO!UQPO'#EZO&qQPO'#EaO&yQQO'#EeO'aQPO,58{OOQO'#Cq'#CqO!UQPO,59^O!vQQO,59`O(mQQO'#C|O(tQQO'#FQOOQO'#E|'#E|OOQO,59f,59fO!UQPO,59fO({QPO'#ExO,kQPO,59cOOQO'#Dc'#DcOOQO'#Dd'#DdOOQO'#De'#DeO!vQQO'#DaOOQO'#Ex'#ExO,rQPO'#DfO,wQSO'#DjO,|QPO'#EpO-UQPO,5UQPO,5:vO!UQPO,5:vOOQO1G0a1G0aO!UQPO'#EdOOQO,5:},5:}O!UQPO'#EqO>aQPO,5uAN>uO&cQPOAN>uO!9yQPO,5;XO!:QQPOAN>uO!:VQPO<|Q`O1G/ZO!?TQ`O1G/ZO!@oQ`O1G/ZO!@|Q`O1G/ZO!AZQ`O1G/ZO!COQ`O1G/ZO!CVQ`O1G/ZO!C^QSO7+%jO!CtQPOG24aO=oQPO1G0sOOQOG24aG24aO!vQQOAN>|OOQO,5;W,5;WOOQO-E8j-E8jOOQOLD){LD){OOQO7+&_7+&_O!CyQPOG24hOAXQQO'#DaO!%dQQO'#DaOAXQQO,59oO!%dQQO,59oOAXQQO,59oO!%dQQO,59oOAXQQO,59oO!%dQQO,59oOAXQQO,59oO!%dQQO,59oOAXQQO,59oO!%dQQO,59oOAXQQO,59oO!%dQQO,59oOAXQQO,59oO!%dQQO,59oOAXQQO,59oO!%dQQO,59oOAXQQO,59oO!%dQQO,59o", - stateData: "!DT~O#hOS#iOSPOS~OSZOUQOWZOY`O[aO_bOlTO!ZfO!ocO!rdO!weO#QgO#YhO#kPO~O#fRP~P]OgkOilOlnOoqOqmO#mjO~O`xOaxObxOcxOdxOlTOqmO!UwO!ZyO!_zO#kPO#mjO#xuO#{tO#|tO$SvO~Og#nXi#nXl#nXo#nXq#nX#m#nX~Ov{O#r$YX~P#zOS#jXU#jXW#jXY#jX[#jX_#jXl#jX!Z#jX!o#jX!r#jX!w#jX#Q#jX#Y#jX#f#jX#k#jX]#jX!p#jX!t#jX!u#jX~P#zO#r}O~O#fRX]RX!pRX!tRX!uRX~P]O]RP~P]O!pRP~P]O!Z!`O#kPO~OS!dO#f#XX]#XX!p#XX!t#XX!u#XX~P!vOU!fO~O`xOaxObxOcxOdxOi!kOlTOqmO!U&jO!ZyO!_zO#kPO#mjO#xuO#{tO#|tO$SvO~Ou!nO~P'fOm!pO~P!vOm#lXx#lXy#lXz#lX!P#lX#v#lX#w#lX#x#lX#y#lX#z#lX#{#lX#|#lX#}#lX$O#lX$P#lX$Q#lX$R#lX[#lX!s#lXS#lXv#lX#f#lXj#lXU#lXW#lXY#lX_#lX!Z#lX!o#lX!r#lX!w#lX#Q#lX#Y#lX#k#lX]#lX!p#lX!t#lX!u#lX~P!bOx#SOy#TOz!{O!P#PO#v!tO#w!uO#x!vO#y!wO#z!wO#{!xO#|!xO#}!yO$O!yO$P!yO$Q!yO$R!zO~Om!sO~P+gOl#VO~O$U#XO~OlTO#kPO~Ov{O#r$Ya~O]#]O~O[#^O~P+gO!p#_O~O!s#`O~P+gOv#bO#r#aO!{$XX~O!{#dO~O[#eO~Og#fOo#hOl#OX~O$[#jOS#WPU#WPW#WPY#WP[#WP_#WPl#WPv#WP!Z#WP!o#WP!r#WP!w#WP#Q#WP#Y#WP#f#WP#k#WP#r#WP]#WP!p#WP!t#WP!u#WP~Ov#lOS$ZXU$ZXW$ZXY$ZX[$ZX_$ZXl$ZX!Z$ZX!o$ZX!r$ZX!w$ZX#Q$ZX#Y$ZX#f$ZX#k$ZX#r$ZX]$ZX!p$ZX!t$ZX!u$ZX~O#r#oOS#TaU#TaW#TaY#Ta[#Ta_#Tal#Ta!Z#Ta!o#Ta!r#Ta!w#Ta#Q#Ta#Y#Ta#f#Ta#k#Ta]#Ta!p#Ta!t#Ta!u#Ta~Ov#pOS#uX#f#uXm#uXU#uXW#uXY#uX[#uX_#uXl#uX!Z#uX!o#uX!r#uX!w#uX#Q#uX#Y#uX#k#uX]#uX!p#uX!t#uX!u#uX~P+gOS#rO#f#Xa]#Xa!p#Xa!t#Xa!u#Xa~Oj#sO~P+gOu#lXx#lXy#lXz#lX!P#lX#s#lX#v#lX#w#lX#x#lX#y#lX#z#lX#{#lX#|#lX#}#lX$O#lX$P#lX$Q#lX$R#lX!b#lX!d#lX!f#lX!k#lX!m#lX$W#lX~P!bO#r#tOg#oXi#oXl#oXo#oXq#oXu#oXx#oXy#oXz#oX!P#oX#m#oX#s#oX#v#oX#w#oX#x#oX#y#oX#z#oX#{#oX#|#oX#}#oX$O#oX$P#oX$Q#oX$R#oX~Ox&zOy&|Oz&lO!P&tO#v!tO#w!uO#x!vO#y!wO#z!wO#{!xO#|!xO#}!yO$O!yO$P!yO$Q!yO$R!zO~OutX#stX~P8UOu#xO#s#vO~Om#zO~OlnOqmO#mjO~O$R!zOx!Tay!Taz!Ta!P!Ta#v!Ta#w!Ta#x!Ta#y!Ta#z!Ta#{!Ta#|!Ta#}!Ta$O!Ta$P!Ta$Q!Tav!Ta~Om!Ta[!Ta!s!TaS!Ta#f!Taj!TaU!TaW!TaY!Ta_!Tal!Ta!Z!Ta!o!Ta!r!Ta!w!Ta#Q!Ta#Y!Ta#k!Ta]!Ta!p!Ta!t!Ta!u!Ta~P9|Oc$WOlTO#kPOm!]P~O!b$^O!d$_O!f$`O!k$aO!m$bO~O$W$cO~P<{Ov#da#r#da~P#zO]RP!tRP!uRP~P]Ov#bO!{$Xa~Og#fOo$nOl#Oa~Ov#lOS$ZaU$ZaW$ZaY$Za[$Za_$Zal$Za!Z$Za!o$Za!r$Za!w$Za#Q$Za#Y$Za#f$Za#k$Za#r$Za]$Za!p$Za!t$Za!u$Za~Ov#pOS#ua#f#uam#uaU#uaW#uaY#ua[#ua_#ual#ua!Z#ua!o#ua!r#ua!w#ua#Q#ua#Y#ua#k#ua]#ua!p#ua!t#ua!u#ua~O`xOaxObxOcxOdxOlTOqmO!U&jO!ZyO!_zO#kPO#mjO#xuO#{tO#|tO$SvO~Oj$xO~P+gOu%TO~P'fOu%TO#s%UO~O$R!zOx!Tay!Taz!Ta!P!Ta#v!Ta#w!Ta#x!Ta#y!Ta#z!Ta#{!Ta#|!Ta#}!Ta$O!Ta$P!Ta$Q!Ta!b!Ta!d!Ta!f!Ta!k!Ta!m!Ta$W!Ta~Ou!Ta#s!Ta~PBsO!P#PO#w!uO#x!vO#y!wO#z!wO#{!xO#|!xO#}!yO$O!yO$P!yO$Q!yO$R!zOmwixwiywizwi[wi!swiSwivwi#fwijwiUwiWwiYwi_wilwi!Zwi!owi!rwi!wwi#Qwi#Ywi#kwi]wi!pwi!twi!uwi~O#v!tO~PDeO#vwi~PDeO!P#PO#y!wO#z!wO#{!xO#|!xO#}!yO$O!yO$P!yO$Q!yO$R!zOmwixwiywizwi#vwi#xwi[wi!swiSwivwi#fwijwiUwiWwiYwi_wilwi!Zwi!owi!rwi!wwi#Qwi#Ywi#kwi]wi!pwi!twi!uwi~O#wwi~PG]O#w!uO~PG]O#}!yO$O!yO$P!yO$Q!yO$R!zOmwixwiywizwi#vwi#wwi#xwi#ywi#zwi[wi!swiSwivwi#fwijwiUwiWwiYwi_wilwi!Zwi!owi!rwi!wwi#Qwi#Ywi#kwi]wi!pwi!twi!uwi~O!P#PO#{!xO#|!xO~PJTO!Pwi#{wi#|wi~PJTO$R!zOmwixwiywi[wi!swiSwivwi#fwijwiUwiWwiYwi_wilwi!Zwi!owi!rwi!wwi#Qwi#Ywi#kwi]wi!pwi!twi!uwi~Ozwi!Pwi#vwi#wwi#xwi#ywi#zwi#{wi#|wi#}wi$Owi$Pwi$Qwi~PMROz!{O!P#PO#v!tO#w!uO#x!vO#y!wO#z!wO#{!xO#|!xO#}!yO$O!yO$P!yO$Q!yO$R!zOmwixwi[wi!swiSwivwi#fwijwiUwiWwiYwi_wilwi!Zwi!owi!rwi!wwi#Qwi#Ywi#kwi]wi!pwi!twi!uwi~Oy#TO~P! rOywi~P! rOv%WOm$TX~P#zOv%WOm$TX~Om%YO~O$W%[O~P<{O!g%_O~OqmO~O`xOaxObxOcxOdxOlTOqmO!U&kO!ZyO!_zO#kPO#mjO#xuO#{tO#|tO$SvO~O]%cO~OS!nqU!nqW!nqY!nq[!nq_!nql!nq!Z!nq!o!nq!r!nq!w!nq#Q!nq#Y!nq#f!nq#k!nq]!nq!p!nq!t!nq!u!nq~P+gO]%fO!t%eO!u%gO~Ov%hO~P+gO]%iO~O$]%kO~OS#]av#]a#f#]am#]aU#]aW#]aY#]a[#]a_#]al#]a!Z#]a!o#]a!r#]a!w#]a#Q#]a#Y#]a#k#]a]#]a!p#]a!t#]a!u#]a~P+gOusi#ssi~P8UO#r%lO~O!P&tO#w!uO#x!vO#y!wO#z!wO#{!xO#|!xO#}!yO$O!yO$P!yO$Q!yO$R!zOuwixwiywizwi#swi!bwi!dwi!fwi!kwi!mwi$Wwi~O#v!tO~P!*`O#vwi~P!*`O!P&tO#y!wO#z!wO#{!xO#|!xO#}!yO$O!yO$P!yO$Q!yO$R!zOuwixwiywizwi#swi#vwi#xwi!bwi!dwi!fwi!kwi!mwi$Wwi~O#wwi~P!,XO#w!uO~P!,XO#}!yO$O!yO$P!yO$Q!yO$R!zOuwixwiywizwi#swi#vwi#wwi#xwi#ywi#zwi!bwi!dwi!fwi!kwi!mwi$Wwi~O!P&tO#{!xO#|!xO~P!.QO!Pwi#{wi#|wi~P!.QO$R!zOxwiywizwi!Pwi#vwi#wwi#xwi#ywi#zwi#{wi#|wi#}wi$Owi$Pwi$Qwi!bwi!dwi!fwi!kwi!mwi$Wwi~Ouwi#swi~P!0POz&lO!P&tO#v!tO#w!uO#x!vO#y!wO#z!wO#{!xO#|!xO#}!yO$O!yO$P!yO$Q!yO$R!zOuwixwi#swi!bwi!dwi!fwi!kwi!mwi$Wwi~Oy&|O~P!1qOywi~P!1qOu%mO~P'fOc%pOlTO#kPO~Ov%WOm$Ta~O#r%sO~O!b!ca!d!ca!f!ca!k!ca!m!ca$W!ca~P8UOv#lXx#lXy#lXz#lX!P#lX!b#lX!d#lX!f#lX!k#lX!m#lX#v#lX#w#lX#x#lX#y#lX#z#lX#{#lX#|#lX#}#lX$O#lX$P#lX$Q#lX$R#lX$W#lX!i#lX~P!bOx&{Oy&}Oz&mO!P&uO#v!tO#w!uO#x!vO#y!wO#z!wO#{!xO#|!xO#}!yO$O!yO$P!yO$Q!yO$R!zO~Ov%wO!b!la!d!la!f!la!k!la!m!la$W!la~P!6aO]%zO!t%eO!u%{O~Om#^av#^a~P#zO]&QO~O!i&SOv!hX!b!hX!d!hX!f!hX!k!hX!m!hX$W!hX~P!6aOv&TO!b$VX!d$VX!f$VX!k$VX!m$VX$W$VX~Ov!Ta!i!Ta~PBsO!s&bO~P+gO]&cO~Ov&dO[!xy~P+gOury#sry~P8UO!b!aq!d!aq!f!aq!k!aq!m!aq$W!aq~P8UOv&TO!b$Va!d$Va!f$Va!k$Va!m$Va$W$Va~O!P&uO#w!uO#x!vO#y!wO#z!wO#{!xO#|!xO#}!yO$O!yO$P!yO$Q!yO$R!zOvwixwiywizwi!bwi!dwi!fwi!kwi!mwi$Wwi!iwi~O#v!tO~P!;jO#vwi~P!;jO!P&uO#y!wO#z!wO#{!xO#|!xO#}!yO$O!yO$P!yO$Q!yO$R!zOvwixwiywizwi!bwi!dwi!fwi!kwi!mwi#vwi#xwi$Wwi!iwi~O#wwi~P!=cO#w!uO~P!=cO#}!yO$O!yO$P!yO$Q!yO$R!zOvwixwiywizwi!bwi!dwi!fwi!kwi!mwi#vwi#wwi#xwi#ywi#zwi$Wwi!iwi~O!P&uO#{!xO#|!xO~P!?[O!Pwi#{wi#|wi~P!?[Ovwi!iwi~P!0POz&mO!P&uO#v!tO#w!uO#x!vO#y!wO#z!wO#{!xO#|!xO#}!yO$O!yO$P!yO$Q!yO$R!zOvwixwi!bwi!dwi!fwi!kwi!mwi$Wwi!iwi~Oy&}O~P!AeOywi~P!AeO!b!lq!d!lq!f!lq!k!lq!m!lq$W!lq~P8UO]&gO~O[!x!Z~P+gOP#|~", - goto: "C}$[PPP$]P${P%YP${P${PP${PPPPPP'u)[P)[PP*xPP,fP.UP/n/n/nPP'|PPP/t0i1d2[P3Y4Z5_'|P6i6i6i'|P7{8U'|P8X8]P8]P8]PP8aP8]P8]P${PP${PPPP${P8g8g8jP8m${8y${P${8|${9Z9^9d9gP9v:V:]:c:j:p:v:|;S;Y;`PPPP;fP;sP?X@{BqBzPPCSCZPPPPPPPPPPPPPCdPCgPCjCmCzQ_OQ!RaQ!TcQ$d#^Q$f#`Q$k#eQ%r%YQ%}%gQ&a%{R&h&bgZO]ac#^#`#e%Y%g%{&b#|SOT]abcdhlnw{}!k!{!|!}#O#P#Q#R#S#T#V#^#_#`#a#d#e#o#p#t$_$b%W%Y%_%e%g%h%l%s%w%{&T&b&d&j&k&l&m&n&o&p&q&r&s&t&u&v&w&x&y&z&{&|&}QiQQ!Q`Q!VeQ!ZfS!]g#lQ!gkW!jm#v%U%nQ!rqQ#n!`Q$h#bQ$l#fQ$o#hQ$p#jQ%]$^R%j$nYoRr!i!r%a#ixTbdhlmnw}!k!{!|!}#O#P#Q#R#S#T#_#a#d#o#p#t#v$_$b%U%_%e%h%l%n%s%w&T&d&j&k&l&m&n&o&p&q&r&s&t&u&v&w&x&y&z&{&|&}$VSOT]abcdhlmnw{}!k!{!|!}#O#P#Q#R#S#T#V#^#_#`#a#d#e#o#p#t#v$_$b%U%W%Y%_%e%g%h%l%n%s%w%{&T&b&d&j&k&l&m&n&o&p&q&r&s&t&u&v&w&x&y&z&{&|&}$VVOT]abcdhlmnw{}!k!{!|!}#O#P#Q#R#S#T#V#^#_#`#a#d#e#o#p#t#v$_$b%U%W%Y%_%e%g%h%l%n%s%w%{&T&b&d&j&k&l&m&n&o&p&q&r&s&t&u&v&w&x&y&z&{&|&}#nVTbdhlmnw{}!k!{!|!}#O#P#Q#R#S#T#V#_#a#d#o#p#t#v$_$b%U%W%_%e%h%l%n%s%w&T&d&j&k&l&m&n&o&p&q&r&s&t&u&v&w&x&y&z&{&|&}gWO]ac#^#`#e%Y%g%{&bYoRr!i!r%a#hxTbdhlmnw}!k!{!|!}#O#P#Q#R#S#T#_#a#d#o#p#t#v$_$b%U%_%e%h%l%n%s%w&T&d&j&k&l&m&n&o&p&q&r&s&t&u&v&w&x&y&z&{&|&}R%`$aX!mm#v%U%nn!|s!S!U!b!h#u#|$T$U$e$g$u%|&O&ib&n!l$w$y%Q%R%^&P&R&`Z&o%b%t&V&^&_r!}s!S!U!b!h#u#|#}$P$T$U$e$g$u%|&O&if&p!l$w$y$z$|%Q%R%^&P&R&`_&q%b%t&V&W&Y&^&_p#Os!S!U!b!h#u#|#}$T$U$e$g$u%|&O&id&r!l$w$y$z%Q%R%^&P&R&`]&s%b%t&V&W&^&_t#Ps!S!U!b!h#u#|#}$O$P$T$U$e$g$u%|&O&ih&t!l$w$y$z${$|%Q%R%^&P&R&`a&u%b%t&V&W&X&Y&^&_v#Qs!S!U!b!h#u#|#}$O$P$Q$T$U$e$g$u%|&O&ij&v!l$w$y$z${$|$}%Q%R%^&P&R&`c&w%b%t&V&W&X&Y&Z&^&_x#Rs!S!U!b!h#u#|#}$O$P$Q$R$T$U$e$g$u%|&O&il&x!l$w$y$z${$|$}%O%Q%R%^&P&R&`e&y%b%t&V&W&X&Y&Z&[&^&_|#Rs!S!U!b!h#U#u#|#}$O$P$Q$R$S$T$U$e$g$u%|&O&ip&x!l#y$w$y$z${$|$}%O%P%Q%R%^&P&R&`i&y%b%t%x&V&W&X&Y&Z&[&]&^&_!UwTbdhlnw}!k!{!|!}#O#P#Q#R#S#T#_#a#d#o#p%e%h&dv&jm#t#v$_%U%l%n%s%w&j&l&n&p&r&t&v&x&z&|k&k$b%_&T&k&m&o&q&s&u&w&y&{&}Q#WyQ#i![R$s#nR$Y#VT$[#X$]T$Z#X$]Q%u%_R&e&TR!YeR!XeQ!ehQ#[}Q$j#dR$t#oR![fgYO]ac#^#`#e%Y%g%{&bR!agQ!^gR$q#lR#k!]d^Oac#^#`#e%Y%g%{&bR!P]d]Oac#^#`#e%Y%g%{&bR!O]Q#w!oR%V#wQ#q!bR$v#qS%X$V$WR%q%XQ$]#XR%Z$]Q&U%uR&f&UQ%d$fR%y%dQ#c!VR$i#cQ#g!ZR$m#gQ|UR#Z|Q#m!^R$r#mg[O]ac#^#`#e%Y%g%{&bQsTQ!SbQ!UdY!bhn}#d#oQ!hlW!lm#v%U%nQ#UwQ#u!kQ#y&jQ#|!{Q#}!|Q$O!}Q$P#OQ$Q#PQ$R#QQ$S#RQ$T#SQ$U#TQ$e#_Q$g#aQ$u#pQ$w#tQ$y&lQ$z&nQ${&pQ$|&rQ$}&tQ%O&vQ%P&xQ%Q&zQ%R&|Q%^$_Q%b$bS%t%_&TQ%x&kQ%|%eQ&O%hQ&P%lQ&R%sQ&V&mQ&W&oQ&X&qQ&Y&sQ&Z&uQ&[&wQ&]&yQ&^&{Q&_&}Q&`%wR&i&dlRO]ac{#V#^#`#e%W%Y%g%{&b!UrTbdhlnw}!k!{!|!}#O#P#Q#R#S#T#_#a#d#o#p%e%h&dv!im#t#v$_%U%l%n%s%w&j&l&n&p&r&t&v&x&z&|k%a$b%_&T&k&m&o&q&s&u&w&y&{&}fUO]ac#^#`#e%Y%g%{&b#hVTbdhlmnw}!k!{!|!}#O#P#Q#R#S#T#_#a#d#o#p#t#v$_$b%U%_%e%h%l%n%s%w&T&d&j&k&l&m&n&o&p&q&r&s&t&u&v&w&x&y&z&{&|&}Q#Y{Q$V#VR%o%WWpRr!i%aR#{!rQ!omV%S#v%U%nZoRr!i!r%aW!ch}#d#oR!qnR$X#VR%v%_R!WegXO]ac#^#`#e%Y%g%{&bR!_g", + states: "EUO!ZQPOOOOQO'#Cc'#CcO!UQPO'#CaO!bQPOOOOQO'#E{'#E{O!vQQO'#CwO$`QPO'#EzOOQO'#Ez'#EzO$jQPO'#EzOOQO'#E`'#E`O%yQPO'#E_OOQO'#Ev'#EvOOQO'#Eg'#EgO&OQPO'#C_OOQO'#C_'#C_QOQPOOO!UQPO'#CeO&cQPO'#CgO!vQQO'#CjO&jQPO'#DzO!vQQO'#D}O!UQPO'#ESO!UQPO'#EZO&qQPO'#EaO&yQQO'#EeO'aQPO,58{OOQO'#Cq'#CqO!UQPO,59^O!vQQO,59`O(mQQO'#C|O(tQQO'#FQOOQO'#E|'#E|OOQO,59f,59fO!UQPO,59fO({QPO'#ExO,kQPO,59cOOQO'#Dc'#DcOOQO'#Dd'#DdOOQO'#De'#DeO!vQQO'#DaOOQO'#Ex'#ExO,rQPO'#DfO,wQSO'#DjO,|QPO'#EpO-UQPO,5QQPO,5YQPO,5:vO!UQPO,5:vOOQO1G0a1G0aO!UQPO'#EdOOQO,5:},5:}O!UQPO'#EqO>eQPO,5uAN>uO&cQPOAN>uO!;[QPO,5;XO!;cQPOAN>uO!;hQPO<fQ`O1G/ZO!>mQ`O1G/ZO!@_Q`O1G/ZO!@fQ`O1G/ZO!BQQ`O1G/ZO!B_Q`O1G/ZO!BlQ`O1G/ZO!DaQ`O1G/ZO!DhQ`O1G/ZO!DoQSO7+%jO!EVQPOG24aO=sQPO1G0sOOQOG24aG24aO!vQQOAN>|OOQO,5;W,5;WOOQO-E8j-E8jOOQOLD){LD){OOQO7+&_7+&_O!E[QPOG24hOA]QQO'#DaO!%cQQO'#DaOA]QQO,59oO!%cQQO,59oOA]QQO,59oO!%cQQO,59oOA]QQO,59oO!%cQQO,59oOA]QQO,59oO!%cQQO,59oOA]QQO,59oO!%cQQO,59oOA]QQO,59oO!%cQQO,59oOA]QQO,59oO!%cQQO,59oOA]QQO,59oO!%cQQO,59oOA]QQO,59oO!%cQQO,59o", + stateData: "!Ef~O#hOS#iOSPOS~OSZOUQOWZOY`O[aO_bOlTO!ZfO!ocO!rdO!weO#QgO#YhO#kPO~O#fRP~P]OgkOilOlnOoqOqmO#mjO~O`xOaxObxOcxOdxOlTOqmO!UwO!ZyO!_zO#kPO#mjO#xuO#{tO#|tO$SvO~Og#nXi#nXl#nXo#nXq#nX#m#nX~Ov{O#r$YX~P#zOS#jXU#jXW#jXY#jX[#jX_#jXl#jX!Z#jX!o#jX!r#jX!w#jX#Q#jX#Y#jX#f#jX#k#jX]#jX!p#jX!t#jX!u#jX~P#zO#r}O~O#fRX]RX!pRX!tRX!uRX~P]O]RP~P]O!pRP~P]O!Z!`O#kPO~OS!dO#f#XX]#XX!p#XX!t#XX!u#XX~P!vOU!fO~O`xOaxObxOcxOdxOi!kOlTOqmO!U&kO!ZyO!_zO#kPO#mjO#xuO#{tO#|tO$SvO~Ou!nO~P'fOm!pO~P!vOm#lXx#lXy#lXz#lX!P#lX#v#lX#w#lX#x#lX#y#lX#z#lX#{#lX#|#lX#}#lX$O#lX$P#lX$Q#lX$R#lX[#lX!s#lXS#lXv#lX#f#lXj#lXU#lXW#lXY#lX_#lX!Z#lX!o#lX!r#lX!w#lX#Q#lX#Y#lX#k#lX]#lX!p#lX!t#lX!u#lX~P!bOx#SOy#TOz!{O!P#PO#v!tO#w!uO#x!vO#y!wO#z!wO#{!xO#|!xO#}!yO$O!yO$P!yO$Q!yO$R!zO~Om!sO~P+gOl#VO~O$U#XO~OlTO#kPO~Ov{O#r$Ya~O]#]O~O[#^O~P+gO!p#_O~O!s#`O~P+gOv#bO#r#aO!{$XX~O!{#dO~O[#eO~Og#fOo#hOl#OX~O$[#jOS#WPU#WPW#WPY#WP[#WP_#WPl#WPv#WP!Z#WP!o#WP!r#WP!w#WP#Q#WP#Y#WP#f#WP#k#WP#r#WP]#WP!p#WP!t#WP!u#WP~Ov#lOS$ZXU$ZXW$ZXY$ZX[$ZX_$ZXl$ZX!Z$ZX!o$ZX!r$ZX!w$ZX#Q$ZX#Y$ZX#f$ZX#k$ZX#r$ZX]$ZX!p$ZX!t$ZX!u$ZX~O#r#oOS#TaU#TaW#TaY#Ta[#Ta_#Tal#Ta!Z#Ta!o#Ta!r#Ta!w#Ta#Q#Ta#Y#Ta#f#Ta#k#Ta]#Ta!p#Ta!t#Ta!u#Ta~Ov#pOS#uX#f#uXm#uXU#uXW#uXY#uX[#uX_#uXl#uX!Z#uX!o#uX!r#uX!w#uX#Q#uX#Y#uX#k#uX]#uX!p#uX!t#uX!u#uX~P+gOS#rO#f#Xa]#Xa!p#Xa!t#Xa!u#Xa~Oj#sO~P+gOu#lXx#lXy#lXz#lX!P#lX#s#lX#v#lX#w#lX#x#lX#y#lX#z#lX#{#lX#|#lX#}#lX$O#lX$P#lX$Q#lX$R#lX!b#lX!d#lX!f#lX!k#lX!m#lX$W#lX~P!bOg#oXi#oXl#oXo#oXq#oXx#oXy#oXz#oX!P#oX#m#oX#v#oX#w#oX#x#oX#y#oX#z#oX#{#oX#|#oX#}#oX$O#oX$P#oX$Q#oX$R#oX~O#r#tOu#oX#s#oX~P6eOx&{Oy&}Oz&mO!P&uO#v!tO#w!uO#x!vO#y!wO#z!wO#{!xO#|!xO#}!yO$O!yO$P!yO$Q!yO$R!zO~OutX#stX~P8YOu#xO#s#vO~Om#zO~OlnOqmO#mjO~O$R!zOx!Tay!Taz!Ta!P!Ta#v!Ta#w!Ta#x!Ta#y!Ta#z!Ta#{!Ta#|!Ta#}!Ta$O!Ta$P!Ta$Q!Tav!Ta~Om!Ta[!Ta!s!TaS!Ta#f!Taj!TaU!TaW!TaY!Ta_!Tal!Ta!Z!Ta!o!Ta!r!Ta!w!Ta#Q!Ta#Y!Ta#k!Ta]!Ta!p!Ta!t!Ta!u!Ta~P:QOc$WOlTO#kPOm!]P~O!b$^O!d$_O!f$`O!k$aO!m$bO~O$W$cO~P=POv#da#r#da~P#zO]RP!tRP!uRP~P]Ov#bO!{$Xa~Og#fOo$nOl#Oa~Ov#lOS$ZaU$ZaW$ZaY$Za[$Za_$Zal$Za!Z$Za!o$Za!r$Za!w$Za#Q$Za#Y$Za#f$Za#k$Za#r$Za]$Za!p$Za!t$Za!u$Za~Ov#pOS#ua#f#uam#uaU#uaW#uaY#ua[#ua_#ual#ua!Z#ua!o#ua!r#ua!w#ua#Q#ua#Y#ua#k#ua]#ua!p#ua!t#ua!u#ua~O`xOaxObxOcxOdxOlTOqmO!U&kO!ZyO!_zO#kPO#mjO#xuO#{tO#|tO$SvO~Oj$xO~P+gOu%TO~P'fOu%TO#s%UO~O$R!zOx!Tay!Taz!Ta!P!Ta#v!Ta#w!Ta#x!Ta#y!Ta#z!Ta#{!Ta#|!Ta#}!Ta$O!Ta$P!Ta$Q!Ta!b!Ta!d!Ta!f!Ta!k!Ta!m!Ta$W!Ta~Ou!Ta#s!Ta~PBwO!P#PO#w!uO#x!vO#y!wO#z!wO#{!xO#|!xO#}!yO$O!yO$P!yO$Q!yO$R!zOmwixwiywizwi[wi!swiSwivwi#fwijwiUwiWwiYwi_wilwi!Zwi!owi!rwi!wwi#Qwi#Ywi#kwi]wi!pwi!twi!uwi~O#v!tO~PDiO#vwi~PDiO!P#PO#y!wO#z!wO#{!xO#|!xO#}!yO$O!yO$P!yO$Q!yO$R!zOmwixwiywizwi#vwi#xwi[wi!swiSwivwi#fwijwiUwiWwiYwi_wilwi!Zwi!owi!rwi!wwi#Qwi#Ywi#kwi]wi!pwi!twi!uwi~O#wwi~PGaO#w!uO~PGaO#}!yO$O!yO$P!yO$Q!yO$R!zOmwixwiywizwi#vwi#wwi#xwi#ywi#zwi[wi!swiSwivwi#fwijwiUwiWwiYwi_wilwi!Zwi!owi!rwi!wwi#Qwi#Ywi#kwi]wi!pwi!twi!uwi~O!P#PO#{!xO#|!xO~PJXO!Pwi#{wi#|wi~PJXO$R!zOmwixwiywi[wi!swiSwivwi#fwijwiUwiWwiYwi_wilwi!Zwi!owi!rwi!wwi#Qwi#Ywi#kwi]wi!pwi!twi!uwi~Ozwi!Pwi#vwi#wwi#xwi#ywi#zwi#{wi#|wi#}wi$Owi$Pwi$Qwi~PMVOz!{O!P#PO#v!tO#w!uO#x!vO#y!wO#z!wO#{!xO#|!xO#}!yO$O!yO$P!yO$Q!yO$R!zOmwixwi[wi!swiSwivwi#fwijwiUwiWwiYwi_wilwi!Zwi!owi!rwi!wwi#Qwi#Ywi#kwi]wi!pwi!twi!uwi~Oy#TO~P! vOywi~P! vOv%WOm$TX~P#zOv%WOm$TX~Om%YO~O$W%[O~P=PO!g%`O~O`xOaxObxOcxOdxOlTOqmO!U&lO!ZyO!_zO#kPO#mjO#xuO#{tO#|tO$SvO~O]%dO~OS!nqU!nqW!nqY!nq[!nq_!nql!nq!Z!nq!o!nq!r!nq!w!nq#Q!nq#Y!nq#f!nq#k!nq]!nq!p!nq!t!nq!u!nq~P+gO]%gO!t%fO!u%hO~Ov%iO~P+gO]%jO~O$]%lO~OS#]av#]a#f#]am#]aU#]aW#]aY#]a[#]a_#]al#]a!Z#]a!o#]a!r#]a!w#]a#Q#]a#Y#]a#k#]a]#]a!p#]a!t#]a!u#]a~P+gOusi#ssi~P8YO#r%mO~O!P&uO#w!uO#x!vO#y!wO#z!wO#{!xO#|!xO#}!yO$O!yO$P!yO$Q!yO$R!zOuwixwiywizwi#swi!bwi!dwi!fwi!kwi!mwi$Wwi~O#v!tO~P!*_O#vwi~P!*_O!P&uO#y!wO#z!wO#{!xO#|!xO#}!yO$O!yO$P!yO$Q!yO$R!zOuwixwiywizwi#swi#vwi#xwi!bwi!dwi!fwi!kwi!mwi$Wwi~O#wwi~P!,WO#w!uO~P!,WO#}!yO$O!yO$P!yO$Q!yO$R!zOuwixwiywizwi#swi#vwi#wwi#xwi#ywi#zwi!bwi!dwi!fwi!kwi!mwi$Wwi~O!P&uO#{!xO#|!xO~P!.PO!Pwi#{wi#|wi~P!.PO$R!zOxwiywizwi!Pwi#vwi#wwi#xwi#ywi#zwi#{wi#|wi#}wi$Owi$Pwi$Qwi!bwi!dwi!fwi!kwi!mwi$Wwi~Ouwi#swi~P!0OOz&mO!P&uO#v!tO#w!uO#x!vO#y!wO#z!wO#{!xO#|!xO#}!yO$O!yO$P!yO$Q!yO$R!zOuwixwi#swi!bwi!dwi!fwi!kwi!mwi$Wwi~Oy&}O~P!1pOywi~P!1pOu%nO~P'fOc%qOlTO#kPO~Ov%WOm$Ta~O#r%tO!b#oX!d#oX!f#oX!k#oX!m#oX$W#oX~P6eO!b!aa!d!aa!f!aa!k!aa!m!aa$W!aa~P8YO!b!ca!d!ca!f!ca!k!ca!m!ca$W!ca~P8YO!b!ja!d!ja!f!ja!k!ja!m!ja$W!ja~P8YOv#lXx#lXy#lXz#lX!P#lX!b#lX!d#lX!f#lX!k#lX!m#lX#v#lX#w#lX#x#lX#y#lX#z#lX#{#lX#|#lX#}#lX$O#lX$P#lX$Q#lX$R#lX$W#lX!i#lX~P!bOx&|Oy'OOz&nO!P&vO#v!tO#w!uO#x!vO#y!wO#z!wO#{!xO#|!xO#}!yO$O!yO$P!yO$Q!yO$R!zO~Ov%xO!b!la!d!la!f!la!k!la!m!la$W!la~P!7rO]%{O!t%fO!u%|O~Om#^av#^a~P#zO]&RO~O!i&TOv!hX!b!hX!d!hX!f!hX!k!hX!m!hX$W!hX~P!7rOv&UO!b$VX!d$VX!f$VX!k$VX!m$VX$W$VX~Ov!Ta!i!Ta~PBwO!s&cO~P+gO]&dO~Ov&eO[!xy~P+gOury#sry~P8YO!b!aq!d!aq!f!aq!k!aq!m!aq$W!aq~P8YOv&UO!b$Va!d$Va!f$Va!k$Va!m$Va$W$Va~O!P&vO#w!uO#x!vO#y!wO#z!wO#{!xO#|!xO#}!yO$O!yO$P!yO$Q!yO$R!zOvwixwiywizwi!bwi!dwi!fwi!kwi!mwi$Wwi!iwi~O#v!tO~P!<{O#vwi~P!<{O!P&vO#y!wO#z!wO#{!xO#|!xO#}!yO$O!yO$P!yO$Q!yO$R!zOvwixwiywizwi!bwi!dwi!fwi!kwi!mwi#vwi#xwi$Wwi!iwi~O#wwi~P!>tO#w!uO~P!>tO#}!yO$O!yO$P!yO$Q!yO$R!zOvwixwiywizwi!bwi!dwi!fwi!kwi!mwi#vwi#wwi#xwi#ywi#zwi$Wwi!iwi~O!P&vO#{!xO#|!xO~P!@mO!Pwi#{wi#|wi~P!@mOvwi!iwi~P!0OOz&nO!P&vO#v!tO#w!uO#x!vO#y!wO#z!wO#{!xO#|!xO#}!yO$O!yO$P!yO$Q!yO$R!zOvwixwi!bwi!dwi!fwi!kwi!mwi$Wwi!iwi~Oy'OO~P!BvOywi~P!BvO!b!lq!d!lq!f!lq!k!lq!m!lq$W!lq~P8YO]&hO~O[!x!Z~P+gOP#|~", + goto: "CY$[PPP$]P${P%YP${P${PP${PPPPPP'v)_P)_PP*}PP,mP'vP._._._PP'}PPP.e/[0X1RP2R3U4['}P5h5h5h'}P6|7V'}P7Y7^P7^P7^PP7bP7^P7^P${PP${PPPP${P7h7h7kP7n${7z${P${7}${8[8_8e8hP8w9W9^9d9k9q9w9}:T:Z:aPPPP:gP:tP>`@UA|BVPPB_BfPPPPPPPPPPPPPBoPBrPBuBxCVQ_OQ!RaQ!TcQ$d#^Q$f#`Q$k#eQ%s%YQ&O%hQ&b%|R&i&cgZO]ac#^#`#e%Y%h%|&c$OSOT]abcdhlnw{}!k!{!|!}#O#P#Q#R#S#T#V#^#_#`#a#d#e#o#p#t$_$a$b%W%Y%`%f%h%i%m%t%x%|&U&c&e&k&l&m&n&o&p&q&r&s&t&u&v&w&x&y&z&{&|&}'OQiQQ!Q`Q!VeQ!ZfS!]g#lQ!gkW!jm#v%U%oQ!rqQ#n!`Q$h#bQ$l#fQ$o#hQ$p#jQ%]$^R%k$nYoRr!i!r%b#mxTbdhlmnw}!k!{!|!}#O#P#Q#R#S#T#_#a#d#o#p#t#v$^$_$a$b%U%`%f%i%m%o%t%x&U&e&k&l&m&n&o&p&q&r&s&t&u&v&w&x&y&z&{&|&}'O$ZSOT]abcdhlmnw{}!k!{!|!}#O#P#Q#R#S#T#V#^#_#`#a#d#e#o#p#t#v$^$_$a$b%U%W%Y%`%f%h%i%m%o%t%x%|&U&c&e&k&l&m&n&o&p&q&r&s&t&u&v&w&x&y&z&{&|&}'O$ZVOT]abcdhlmnw{}!k!{!|!}#O#P#Q#R#S#T#V#^#_#`#a#d#e#o#p#t#v$^$_$a$b%U%W%Y%`%f%h%i%m%o%t%x%|&U&c&e&k&l&m&n&o&p&q&r&s&t&u&v&w&x&y&z&{&|&}'O#rVTbdhlmnw{}!k!{!|!}#O#P#Q#R#S#T#V#_#a#d#o#p#t#v$^$_$a$b%U%W%`%f%i%m%o%t%x&U&e&k&l&m&n&o&p&q&r&s&t&u&v&w&x&y&z&{&|&}'OgWO]ac#^#`#e%Y%h%|&cX!mm#v%U%on!|s!S!U!b!h#u#|$T$U$e$g$u%}&P&jf&o!l$w$y%Q%R%^%_%a&Q&S&aZ&p%c%u&W&_&`r!}s!S!U!b!h#u#|#}$P$T$U$e$g$u%}&P&jj&q!l$w$y$z$|%Q%R%^%_%a&Q&S&a_&r%c%u&W&X&Z&_&`p#Os!S!U!b!h#u#|#}$T$U$e$g$u%}&P&jh&s!l$w$y$z%Q%R%^%_%a&Q&S&a]&t%c%u&W&X&_&`t#Ps!S!U!b!h#u#|#}$O$P$T$U$e$g$u%}&P&jl&u!l$w$y$z${$|%Q%R%^%_%a&Q&S&aa&v%c%u&W&X&Y&Z&_&`v#Qs!S!U!b!h#u#|#}$O$P$Q$T$U$e$g$u%}&P&jn&w!l$w$y$z${$|$}%Q%R%^%_%a&Q&S&ac&x%c%u&W&X&Y&Z&[&_&`x#Rs!S!U!b!h#u#|#}$O$P$Q$R$T$U$e$g$u%}&P&jp&y!l$w$y$z${$|$}%O%Q%R%^%_%a&Q&S&ae&z%c%u&W&X&Y&Z&[&]&_&`|#Rs!S!U!b!h#U#u#|#}$O$P$Q$R$S$T$U$e$g$u%}&P&jt&y!l#y$w$y$z${$|$}%O%P%Q%R%^%_%a&Q&S&ai&z%c%u%y&W&X&Y&Z&[&]&^&_&`!UwTbdhlnw}!k!{!|!}#O#P#Q#R#S#T#_#a#d#o#p%f%i&ez&km#t#v$^$_$a%U%m%o%t%x&k&m&o&q&s&u&w&y&{&}k&l$b%`&U&l&n&p&r&t&v&x&z&|'OQ#WyQ#i![R$s#nR$Y#VT$[#X$]T$Z#X$]Q%v%`R&f&UR!YeR!XeQ!ehQ#[}Q$j#dR$t#oR![fgYO]ac#^#`#e%Y%h%|&cR!agQ!^gR$q#lR#k!]d^Oac#^#`#e%Y%h%|&cR!P]d]Oac#^#`#e%Y%h%|&cR!O]Q#w!oR%V#wQ#q!bR$v#qS%X$V$WR%r%XQ$]#XR%Z$]Q&V%vR&g&VQ%e$fR%z%eQ#c!VR$i#cQ#g!ZR$m#gQ|UR#Z|Q#m!^R$r#mg[O]ac#^#`#e%Y%h%|&cQsTQ!SbQ!UdY!bhn}#d#oQ!hlW!lm#v%U%oQ#UwQ#u!kQ#y&kQ#|!{Q#}!|Q$O!}Q$P#OQ$Q#PQ$R#QQ$S#RQ$T#SQ$U#TQ$e#_Q$g#aQ$u#pQ$w#tQ$y&mQ$z&oQ${&qQ$|&sQ$}&uQ%O&wQ%P&yQ%Q&{Q%R&}Q%^$^Q%_$_Q%a$aQ%c$bS%u%`&UQ%y&lQ%}%fQ&P%iQ&Q%mQ&S%tQ&W&nQ&X&pQ&Y&rQ&Z&tQ&[&vQ&]&xQ&^&zQ&_&|Q&`'OQ&a%xR&j&elRO]ac{#V#^#`#e%W%Y%h%|&c!UrTbdhlnw}!k!{!|!}#O#P#Q#R#S#T#_#a#d#o#p%f%i&ez!im#t#v$^$_$a%U%m%o%t%x&k&m&o&q&s&u&w&y&{&}k%b$b%`&U&l&n&p&r&t&v&x&z&|'OfUO]ac#^#`#e%Y%h%|&c#lVTbdhlmnw}!k!{!|!}#O#P#Q#R#S#T#_#a#d#o#p#t#v$^$_$a$b%U%`%f%i%m%o%t%x&U&e&k&l&m&n&o&p&q&r&s&t&u&v&w&x&y&z&{&|&}'OQ#Y{Q$V#VR%p%WWpRr!i%bR#{!rQ!omV%S#v%U%oZoRr!i!r%bW!ch}#d#oR!qnR$X#VR%w%`R!WegXO]ac#^#`#e%Y%h%|&cR!_g", nodeNames: "⚠ Comment Chunk Block ; Label :: Name break Goto goto Scope do end WhileStatement while nil true false Ellipsis Number LiteralString Property . MemberExpression [ ] Parens ( ) FunctionCall : TableConstructor { FieldDynamic FieldProp FieldExp } , BinaryExpression or and CompareOp BitOp BitOp BitOp BitOp Concat ArithOp ArithOp ArithOp UnaryExpression not ArithOp BitOp LenOp FunctionDef function FuncBody ArgList Query query QueryClause FromClause from WhereClause where OrderByClause order by OrderBy desc SelectClause select LimitClause limit RepeatStatement repeat until IfStatement if then elseif else ForStatement for ForNumeric ForGeneric NameList in ExpList Function FuncName LocalFunction local Assign VarList Local AttNameList AttName Attrib ReturnStatement return", maxTerm: 151, nodeProps: [ @@ -18,5 +18,5 @@ export const parser = LRParser.deserialize({ topRules: {"Chunk":[0,2]}, dynamicPrecedences: {"128":1}, specialized: [{term: 119, get: (value) => spec_identifier[value] || -1}], - tokenPrec: 3728 + tokenPrec: 3791 }) diff --git a/common/space_lua/parse.test.ts b/common/space_lua/parse.test.ts index 90614546..49606e3e 100644 --- a/common/space_lua/parse.test.ts +++ b/common/space_lua/parse.test.ts @@ -105,7 +105,7 @@ Deno.test("Test comment handling", () => { Deno.test("Test query parsing", () => { parse(`_(query[[from p = tag("page") where p.name == "John" limit 10, 3]])`); - parse(`_(query[[from p = tag("page") select {name="hello", age=10}]])`); + parse(`_(query[[from tag("page") select {name="hello", age=10}]])`); parse( `_(query[[from p = tag("page") order by p.lastModified desc, p.name]])`, ); diff --git a/common/space_lua/parse.ts b/common/space_lua/parse.ts index 5e7bc8a6..ac9a5614 100644 --- a/common/space_lua/parse.ts +++ b/common/space_lua/parse.ts @@ -20,7 +20,6 @@ import type { LuaPrefixExpression, LuaQueryClause, LuaStatement, - LuaTableConstructor, LuaTableField, } from "./ast.ts"; import { tags as t } from "@lezer/highlight"; @@ -486,12 +485,21 @@ function parseQueryClause(t: ParseTree, ctx: ASTCtx): LuaQueryClause { t = t.children![0]; switch (t.type) { case "FromClause": { - return { - type: "From", - name: t.children![1].children![0].text!, - expression: parseExpression(t.children![3], ctx), - ctx: context(t, ctx), - }; + if (t.children!.length === 4) { + // From clause with a name + return { + type: "From", + name: t.children![1].children![0].text!, + expression: parseExpression(t.children![3], ctx), + ctx: context(t, ctx), + }; + } else { + return { + type: "From", + expression: parseExpression(t.children![1], ctx), + ctx: context(t, ctx), + }; + } } case "WhereClause": return { @@ -532,10 +540,7 @@ function parseQueryClause(t: ParseTree, ctx: ASTCtx): LuaQueryClause { case "SelectClause": { return { type: "Select", - tableConstructor: parseExpression( - t.children![1], - ctx, - ) as LuaTableConstructor, + expression: parseExpression(t.children![1], ctx), ctx: context(t, ctx), }; } diff --git a/common/space_lua/query_collection.test.ts b/common/space_lua/query_collection.test.ts index 69f4986e..9fe896ee 100644 --- a/common/space_lua/query_collection.test.ts +++ b/common/space_lua/query_collection.test.ts @@ -114,33 +114,27 @@ Deno.test("ArrayQueryCollection", async () => { const result8 = await collection2.query( { objectVariable: "p", - select: [{ - name: "fullName", - expr: parseExpressionString("p.firstName .. ' ' .. p.lastName"), - }], + select: parseExpressionString("p.firstName .. ' ' .. p.lastName"), }, rootEnv, LuaStackFrame.lostFrame, ); - assertEquals(result8[0].fullName, "John Doe"); - assertEquals(result8[1].fullName, "Alice Johnson"); - assertEquals(result8[2].fullName, "Jane Doe"); - assertEquals(result8[3].fullName, "Bob Johnson"); + assertEquals(result8[0], "John Doe"); + assertEquals(result8[1], "Alice Johnson"); + assertEquals(result8[2], "Jane Doe"); + assertEquals(result8[3], "Bob Johnson"); // Test select with native function const result9 = await collection2.query( { objectVariable: "p", - select: [{ - name: "fullName", - expr: parseExpressionString("build_name(p.firstName, p.lastName)"), - }], + select: parseExpressionString("build_name(p.firstName, p.lastName)"), }, rootEnv, LuaStackFrame.lostFrame, ); - assertEquals(result9[0].fullName, "John Doe"); - assertEquals(result9[1].fullName, "Alice Johnson"); - assertEquals(result9[2].fullName, "Jane Doe"); - assertEquals(result9[3].fullName, "Bob Johnson"); + assertEquals(result9[0], "John Doe"); + assertEquals(result9[1], "Alice Johnson"); + assertEquals(result9[2], "Jane Doe"); + assertEquals(result9[3], "Bob Johnson"); }); diff --git a/common/space_lua/query_collection.ts b/common/space_lua/query_collection.ts index aefa14df..0a967dd8 100644 --- a/common/space_lua/query_collection.ts +++ b/common/space_lua/query_collection.ts @@ -15,11 +15,6 @@ export type LuaOrderBy = { desc: boolean; }; -export type LuaSelect = { - name: string; - expr?: LuaExpression; -}; - /** * Represents a query for a collection */ @@ -30,7 +25,7 @@ export type LuaCollectionQuery = { // The order by expression evaluated with Lua orderBy?: LuaOrderBy[]; // The select expression evaluated with Lua - select?: LuaSelect[]; + select?: LuaExpression; // The limit of the query limit?: number; // The offset of the query @@ -77,28 +72,6 @@ async function applyTransforms( env: LuaEnv, sf: LuaStackFrame, ): Promise { - // Apply the select - if (query.select) { - const newResult = []; - for (const item of result) { - const itemEnv = buildItemEnv(query.objectVariable, item, env); - const newItem: Record = {}; - for (const select of query.select) { - if (select.expr) { - newItem[select.name] = await evalExpression( - select.expr, - itemEnv, - sf, - ); - } else { - newItem[select.name] = item[select.name]; - } - } - newResult.push(newItem); - } - result = newResult; - } - // Apply the order by if (query.orderBy) { result = await asyncQuickSort(result, async (a, b) => { @@ -122,6 +95,16 @@ async function applyTransforms( }); } + // Apply the select + if (query.select) { + const newResult = []; + for (const item of result) { + const itemEnv = buildItemEnv(query.objectVariable, item, env); + newResult.push(await evalExpression(query.select, itemEnv, sf)); + } + result = newResult; + } + // Apply the limit and offset if (query.limit !== undefined && query.offset !== undefined) { result = result.slice(query.offset, query.offset + query.limit); diff --git a/common/space_lua/runtime.ts b/common/space_lua/runtime.ts index 80d49e5d..4a501fad 100644 --- a/common/space_lua/runtime.ts +++ b/common/space_lua/runtime.ts @@ -437,6 +437,14 @@ export class LuaTable implements ILuaSettable, ILuaGettable { return this.arrayPart.map(luaValueToJS); } + asJS(): Record | any[] { + if (this.length > 0) { + return this.asJSArray(); + } else { + return this.asJSObject(); + } + } + async toStringAsync(): Promise { if (this.metatable?.has("__tostring")) { const metaValue = await this.metatable.get("__tostring"); @@ -557,6 +565,10 @@ export function luaCall( return fn.call((sf || LuaStackFrame.lostFrame).withCtx(ctx), ...args); } +export function luaEquals(a: any, b: any): boolean { + return a === b; +} + export function luaKeys(val: any): any[] { if (val instanceof LuaTable) { return val.keys(); diff --git a/common/space_lua/stdlib/table.ts b/common/space_lua/stdlib/table.ts index 843f5ee4..b67e61cd 100644 --- a/common/space_lua/stdlib/table.ts +++ b/common/space_lua/stdlib/table.ts @@ -1,7 +1,10 @@ import { type ILuaFunction, LuaBuiltinFunction, + luaEquals, + LuaRuntimeError, LuaTable, + type LuaValue, } from "$common/space_lua/runtime.ts"; export const tableApi = new LuaTable({ @@ -40,4 +43,24 @@ export const tableApi = new LuaTable({ keys: new LuaBuiltinFunction((_sf, tbl: LuaTable) => { return tbl.keys(); }), + includes: new LuaBuiltinFunction( + (sf, tbl: LuaTable | Record, value: LuaValue) => { + if (tbl instanceof LuaTable) { + // Iterate over the table + for (const key of tbl.keys()) { + if (luaEquals(tbl.get(key), value)) { + return true; + } + } + return false; + } else if (Array.isArray(tbl)) { + return !!tbl.find((item) => luaEquals(item, value)); + } else { + throw new LuaRuntimeError( + `Cannot use includes on a non-table or non-array value`, + sf, + ); + } + }, + ), }); diff --git a/lib/plugos/syscalls/datastore.ts b/lib/plugos/syscalls/datastore.ts index 135e3953..a89c2040 100644 --- a/lib/plugos/syscalls/datastore.ts +++ b/lib/plugos/syscalls/datastore.ts @@ -6,7 +6,7 @@ import type { CommonSystem } from "$common/common_system.ts"; import type { KV, KvKey, KvQuery } from "../../../plug-api/types.ts"; import type { DataStore } from "../../data/datastore.ts"; import type { SysCallMapping } from "../system.ts"; -import { LuaStackFrame } from "$common/space_lua/runtime.ts"; +import { LuaStackFrame, luaValueToJS } from "$common/space_lua/runtime.ts"; /** * Exposes the datastore API to plugs, but scoping everything to a prefix based on the plug's name @@ -37,17 +37,17 @@ export function dataStoreReadSyscalls( return ds.query(query, variables); }, - "datastore.queryLua": ( + "datastore.queryLua": async ( _ctx, prefix: string[], query: LuaCollectionQuery, ): Promise => { const dsQueryCollection = new DataStoreQueryCollection(ds, prefix); - return dsQueryCollection.query( + return (await dsQueryCollection.query( query, commonSystem.spaceLuaEnv.env, LuaStackFrame.lostFrame, - ); + )).map((item) => luaValueToJS(item)); }, "datastore.listFunctions": (): string[] => { diff --git a/web/cm_plugins/lua_widget.ts b/web/cm_plugins/lua_widget.ts index 4b81e275..9ca1c5da 100644 --- a/web/cm_plugins/lua_widget.ts +++ b/web/cm_plugins/lua_widget.ts @@ -45,7 +45,7 @@ export class LuaWidget extends WidgetType { toDOM(): HTMLElement { const div = document.createElement("div"); - div.className = "sb-lua-directive"; + // div.className = "sb-lua-directive-inline"; const cacheItem = this.client.getWidgetCache(this.cacheKey); if (cacheItem) { div.innerHTML = cacheItem.html; @@ -89,9 +89,9 @@ export class LuaWidget extends WidgetType { html = widgetContent.html; div.innerHTML = html; if ((widgetContent as any)?.display === "block") { - div.style.display = "block"; + div.className = "sb-lua-directive-block"; } else { - div.style.display = "inline"; + div.className = "sb-lua-directive-inline"; } attachWidgetEventHandlers(div, this.client, this.from); this.client.setWidgetCache( @@ -128,10 +128,13 @@ export class LuaWidget extends WidgetType { return; } - if ((widgetContent as any)?.display === "block") { - div.style.display = "block"; + if ( + (widgetContent as any)?.display === "block" || + trimmedMarkdown.includes("\n") + ) { + div.className = "sb-lua-directive-block"; } else { - div.style.display = "inline"; + div.className = "sb-lua-directive-inline"; } // Parse the markdown again after trimming diff --git a/web/cm_plugins/smart_quotes.ts b/web/cm_plugins/smart_quotes.ts index ecd8ac46..e502cb46 100644 --- a/web/cm_plugins/smart_quotes.ts +++ b/web/cm_plugins/smart_quotes.ts @@ -6,6 +6,7 @@ import type { Client } from "../client.ts"; const straightQuoteContexts = [ "CommentBlock", "CodeBlock", + "CodeText", "FencedCode", "InlineCode", "FrontMatterCode", diff --git a/web/styles/editor.scss b/web/styles/editor.scss index b4864e61..41dfb84a 100644 --- a/web/styles/editor.scss +++ b/web/styles/editor.scss @@ -334,14 +334,22 @@ cursor: pointer; } - .sb-lua-directive { - background-color: rgb(233, 232, 232, 35%); - border: 1px #d3d3d373 solid; - /* box-shadow: #d1d1d1 0 0 4px; */ + .sb-lua-directive-inline { + display: inline; + border: 1px var(--editor-widget-background-color) solid; border-radius: 8px; padding: 2px; } + .sb-lua-directive-block { + display: block; + border: 1px var(--editor-widget-background-color) solid; + border-radius: 8px; + padding: 5px; + margin: -1em 0; + overflow: auto; + } + a.sb-wiki-link-page-missing, .sb-wiki-link-page-missing > .sb-wiki-link-page { border-radius: 5px; diff --git a/website/Space Lua.md b/website/Space Lua.md index 62ba9c0c..965607f4 100644 --- a/website/Space Lua.md +++ b/website/Space Lua.md @@ -1,18 +1,32 @@ > **warning** Experimental > This is a **highly experimental** feature still under active development. It is documented here primarily for the real early adopters as this feature develops. +> +> If you want to experiment, be sure to use the [edge builds](https://community.silverbullet.md/t/living-on-the-edge-builds/27/5). -Space Lua is a custom implementation of the [Lua programming language](https://lua.org/) embedded in SilverBullet. +Space Lua is a custom implementation of the [Lua programming language](https://lua.org/), embedded in SilverBullet. It aims to be a largely complete Lua implementation, and adds a few non-standard features while remaining syntactically compatible with “real” Lua. # Goals -These are current, long term goals that are subject to change. +The introduction of Lua aims to unify and simplify a few SilverBullet features, specifically: -* Provide a safe, integrated, productive way to extend SilverBullet’s feature set with a low barrier to entry -* Ultimately succeed [[Space Script]] (for most, if not all) use cases -* Ultimately replace [[Expression Language]] with Lua’s expression language, also in [[Query Language]]. -* Ultimately replace [[Template Language]] with a variant using Lua’s control flows (`for`, `if` etc.) +* Scripting: replace [[Space Script]] (JavaScript) with a more controlled, simple and extensible language. +* Replace [[Expression Language]], [[Template Language]] and [[Query Language]] with Lua-based equivalents. +* (Potentially) provide an alternative way to specify [[Space Config]] -# Use -Space Lua functions analogously to [[Space Script]], [[Space Style]] and [[Space Config]] in that it is defined in fenced code blocks, in this case with the `space-lua` language. As follows: +# Introduction approach +This is a big effort. During its development, Space Lua will be offered as a kind of “alternative universe” to the things mentioned above. Existing [[Live Templates]], [[Live Queries]] and [[Space Script]] will continue to work as before, unaltered. + +Once these features stabilize and best practices are ironed out, old mechanisms will likely be deprecated and possibly removed at some point. + +We’re not there yet, though. + +# Basics +In its essence, Space Lua adds two features to its [[Markdown]] language: + +* **Definitions**: Code written in `space-lua` code blocks are enabled across your entire space. +* **Expressions**: The `${expression}` syntax will [[Live Preview]] to its evaluated value. + +## Definitions +Space Lua definitions are defined in fenced code blocks, in this case with the `space-lua` language. As follows: ```space-lua -- adds two numbers @@ -21,14 +35,41 @@ function adder(a, b) end ``` -Each `space-lua` block has its own local scope, however when functions and variables are not explicitly defined as `local` they will be available from anywhere (following regular Lua scoping rule). +Each `space-lua` block has its own local scope. However, following Lua semantics, when functions and variables are not explicitly defined as `local` they will be available globally across your space. This means that the `adder` function above can be used in any other page. -A new syntax introduced with Space Lua is the `${lua expression}` syntax that you can use in your pages, this syntax will [[Live Preview]] to the evaluation of that expression. +Since there is a single global namespace, it is good practice to manually namespace things using the following pattern: -Example: 10 + 2 = ${adder(10, 2)} (Alt-click on this value to see the expression using the just defined `adder` function to calculate this). +```space-lua +-- This initializes the stuff variable with an empty table if it's not already defined +stuff = stuff or {} + +function stuff.adder(a, b) + return a + b +end +``` + +> **note** Tip +> All your space-lua scripts are loaded on boot, to reload them without reloading the page, simply run the {[System: Reload]} command. + +## Expressions +A new syntax introduced with Space Lua is the `${lua expression}` syntax that you can use in your pages. This syntax will [[Live Preview]] to the evaluation of that expression. + +For example: 10 + 2 = ${adder(10, 2)} (Alt-click, or select to see the expression) is using the just defined `adder` function to this rather impressive calculation. Yes, this may as well be written as `${10 + 2}` (${10 + 2}), but... you know. + +## Queries +Space Lua has a feature called [[Space Lua/Lua Integrated Query]], which integrate SQL-like queries into Lua. By using this feature, you can easily replicate [[Live Queries]]. More detail in [[Space Lua/Lua Integrated Query]], but here’s a small example querying the last 3 modifies pages: + +${query[[ + from tag "page" + order by _.lastModified desc + select _.name + limit 3 +]]} ## Widgets -The `${lua expression}` syntax can be used to implement simple widgets. If the lua expression evaluates to a simple string, it will live preview as that string rendered as simple markdown. However, if the expression returns a Lua table with specific keys, you can do some cooler stuff. The following keys are supported: +The `${lua expression}` syntax can be used to implement simple widgets. If the Lua expression evaluates to a simple string, it will live preview as that string rendered as markdown. However, if the expression returns a Lua table with specific keys, you can do some cooler stuff. + +The following keys are supported: * `markdown`: Renders the value as markdown * `html`: Renders the value as HTML @@ -55,7 +96,7 @@ And some [[Space Style]] to style it: } ``` -Now, let’s use it (put your cursor in there to see the code): +Now, let’s use it: ${marquee "Finally, marqeeeeeeee!"} Oh boy, the times we live in! @@ -88,30 +129,17 @@ define_event_listener { } ``` -## Custom functions -Any global function (so not marked with `local`) is automatically exposed to be used in [[Live Queries]] and [[Live Templates]]: +# Space Lua Extensions +Space Lua currently introduces a few new features on top core Lua: -```space-lua --- This is a global function, therefore automatically exposed -function greet_me(name) - return "Hello, " .. name -end +1. [[Space Lua/Lua Integrated Query]], embedding a [[Query Language]]-like language into Lua itself +2. Thread locals --- Whereas this one is not -local function greet_you(name) - error("This is not exposed") -end -``` - -Template: -```template -Here's a greeting: {{greet_me("Pete")}} -``` - -# Thread locals -There’s a magic `_CTX` global variable available from which you can access useful context-specific value. Currently the following keys are available: +## Thread locals +There’s a magic `_CTX` global variable available from which you can access useful context-specific values. Currently the following keys are available: * `_CTX.pageMeta` contains a reference to the loaded page metadata (can be `nil` when not yet loaded) +* `_CTX.GLOBAL` providing access to the global scope # API Lua APIs, which should be (roughly) implemented according to the Lua standard. diff --git a/website/Space Lua/Lua Integrated Query.md b/website/Space Lua/Lua Integrated Query.md new file mode 100644 index 00000000..9a152896 --- /dev/null +++ b/website/Space Lua/Lua Integrated Query.md @@ -0,0 +1,110 @@ +Lua Integrated Query (LIQ) is a SilverBullet specific Lua extension. It adds a convenient query syntax to the language in a backwards compatible way. It does so by overloading Lua’s default function call + single argument syntax when using `query` as the function call. As a result, Lua programs using LIQ are still syntactically valid Lua. + +The syntax for LIQ is `query[[my query]]`. In regular Lua `[[my query]]` is just another way of writing `"my query"` (it is an alternative string syntax). Function calls that only take a string argument can omit parentheses, therefore `query[[my query]]` is equivalent to `query("my query")`. + +However, in [[Space Lua]] it interpreted as an SQL (and [LINQ](https://learn.microsoft.com/en-us/dotnet/csharp/linq/))-inspired integrated query language. + +General syntax: + + query[[ + from = + where + order by + limit , + select + ]] + +Unlike [[Query Language]] which operates on [[Objects]] only, LIQ can operate on any Lua collection. + +For instance, to sort a list of numbers in descending order: +${query[[from n = {1, 2, 3} order by n desc]]} + +However, in most cases you’ll use it in conjunction with [[Space Lua/stdlib#tag(name)]]. Here’s an example querying the 3 pages that were last modified: + +${query[[ + from p = tag "page" + order by p.lastModified desc + select p.name + limit 3 +]]} + +# Clauses +Here are the clauses that are currently supported: + +## `from ` +The `from` clause specifies the source of your data. There are two syntactic variants: + +With explicit variable binding: + + from v = <> + +binding each item to the variable `v`. + +And the shorter: + + from <> + +implicitly binding each item to the variable `_`. + +Example without variable binding: +${query[[from {1, 2, 3} select _]]} + +With variable binding: +${query[[from n = {1, 2, 3} select n]]} + +A more realist example using `tag`: +${query[[from t = tag "page" limit 3 select t.name]]} + +## `where ` +The `where` clause allows you to filter data. When the expression evaluated to a truthy value, the item is included in the result. + +Example: + +${query[[from {1, 2, 3, 4, 5} where _ > 2]]} + +Or to select all pages tagged with `#meta`: + +${query[[from tag "page" where table.includes(_.tags, "meta")]]} + +## `order by [desc]` +The `order by` clause allows you to sort data, when `desc` is specified it reverts the sort order. + +As an example, the last 3 modified pages: +${query[[ + from tag "page" + order by _.lastModified desc + select _.name + limit 3 +]]} + +## `limit [, ]` +The `limit` clause allows you to limit the number of results, optionally with an offset. + +Example: + +${query[[from {1, 2, 3, 4, 5} limit 3]]} + +You can also specify an offset to skip some results: + +${query[[from {1, 2, 3, 4, 5} limit 3, 2]]} + +## `select ` +The `select` clause allows you to transform each item in the result set. If omitted, it defaults to returning the item itself. + +Some examples: + +Double each number: +${query[[from {1, 2, 3} select _ * 2]]} + +Extract just the name from pages: +${query[[from tag "page" select _.name limit 3]]} + +You can also return tables or other complex values: +${query[[ + from tag "page" + select { + name = _.name, + modified = _.lastModified + } + limit 3 +]]} diff --git a/website/Space Lua/stdlib.md b/website/Space Lua/stdlib.md new file mode 100644 index 00000000..a13725a3 --- /dev/null +++ b/website/Space Lua/stdlib.md @@ -0,0 +1,33 @@ +These are Lua functions defined in the global namespace: + +# Standard Lua +## print(...) +Prints to your log (browser or server log). + +## assert(expr) +Asserts `expr` to be true otherwise raises an [[#error]] + +## ipairs +## pairs +## unpack +## type +## tostring +## tonumber +## error(message) +Throw an error. + +Example: `error("FAIL")` + +## pcall +## xpcall +## setmetatable +## getmetatable +## rawset + +# Space Lua specific +## tag(name) +Returns a given [[Objects#Tags]] as a query collection, to be queried using [[Space Lua/Lua Integrated Query]]. + +Example: + +${query[[from tag("page") limit 1]]} \ No newline at end of file