Lua Integrated Query
parent
2283d16d09
commit
bf6a34f82c
|
@ -2,19 +2,16 @@ import type { System } from "../lib/plugos/system.ts";
|
||||||
import type { ScriptObject } from "../plugs/index/script.ts";
|
import type { ScriptObject } from "../plugs/index/script.ts";
|
||||||
import {
|
import {
|
||||||
LuaEnv,
|
LuaEnv,
|
||||||
LuaFunction,
|
|
||||||
LuaRuntimeError,
|
LuaRuntimeError,
|
||||||
LuaStackFrame,
|
LuaStackFrame,
|
||||||
} from "$common/space_lua/runtime.ts";
|
} from "$common/space_lua/runtime.ts";
|
||||||
import { parse as parseLua } from "$common/space_lua/parse.ts";
|
import { parse as parseLua } from "$common/space_lua/parse.ts";
|
||||||
import { evalStatement } from "$common/space_lua/eval.ts";
|
import { evalStatement } from "$common/space_lua/eval.ts";
|
||||||
import { jsToLuaValue } from "$common/space_lua/runtime.ts";
|
|
||||||
import {
|
import {
|
||||||
type PageRef,
|
type PageRef,
|
||||||
parsePageRef,
|
parsePageRef,
|
||||||
} from "@silverbulletmd/silverbullet/lib/page_ref";
|
} from "@silverbulletmd/silverbullet/lib/page_ref";
|
||||||
import type { ScriptEnvironment } from "$common/space_script.ts";
|
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 type { ASTCtx } from "$common/space_lua/ast.ts";
|
||||||
import { buildLuaEnv } from "$common/space_lua_api.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");
|
console.log("[Lua] Loaded", allScripts.length, "scripts");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -271,7 +271,7 @@ export type LuaQueryClause =
|
||||||
|
|
||||||
export type LuaFromClause = {
|
export type LuaFromClause = {
|
||||||
type: "From";
|
type: "From";
|
||||||
name: string;
|
name?: string;
|
||||||
expression: LuaExpression;
|
expression: LuaExpression;
|
||||||
} & ASTContext;
|
} & ASTContext;
|
||||||
|
|
||||||
|
@ -299,5 +299,5 @@ export type LuaOrderBy = {
|
||||||
|
|
||||||
export type LuaSelectClause = {
|
export type LuaSelectClause = {
|
||||||
type: "Select";
|
type: "Select";
|
||||||
tableConstructor: LuaTableConstructor;
|
expression: LuaExpression;
|
||||||
} & ASTContext;
|
} & ASTContext;
|
||||||
|
|
|
@ -7,6 +7,7 @@ import type {
|
||||||
import { evalPromiseValues } from "$common/space_lua/util.ts";
|
import { evalPromiseValues } from "$common/space_lua/util.ts";
|
||||||
import {
|
import {
|
||||||
luaCall,
|
luaCall,
|
||||||
|
luaEquals,
|
||||||
luaSet,
|
luaSet,
|
||||||
type LuaStackFrame,
|
type LuaStackFrame,
|
||||||
} from "$common/space_lua/runtime.ts";
|
} from "$common/space_lua/runtime.ts";
|
||||||
|
@ -256,7 +257,7 @@ export function evalExpression(
|
||||||
if (!findFromClause) {
|
if (!findFromClause) {
|
||||||
throw new LuaRuntimeError("No from clause found", sf.withCtx(e.ctx));
|
throw new LuaRuntimeError("No from clause found", sf.withCtx(e.ctx));
|
||||||
}
|
}
|
||||||
const objectVariable = findFromClause.name;
|
const objectVariable = findFromClause.name || "_";
|
||||||
const objectExpression = findFromClause.expression;
|
const objectExpression = findFromClause.expression;
|
||||||
return Promise.resolve(evalExpression(objectExpression, env, sf)).then(
|
return Promise.resolve(evalExpression(objectExpression, env, sf)).then(
|
||||||
async (collection: LuaValue) => {
|
async (collection: LuaValue) => {
|
||||||
|
@ -298,20 +299,7 @@ export function evalExpression(
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "Select": {
|
case "Select": {
|
||||||
query.select = clause.tableConstructor.fields.map((f) => {
|
query.select = clause.expression;
|
||||||
if (f.type === "PropField") {
|
|
||||||
return {
|
|
||||||
name: f.key,
|
|
||||||
expr: f.value,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
throw new LuaRuntimeError(
|
|
||||||
"Select fields must be named",
|
|
||||||
sf.withCtx(f.ctx),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "Limit": {
|
case "Limit": {
|
||||||
|
@ -524,15 +512,15 @@ const operatorsMetaMethods: Record<string, {
|
||||||
},
|
},
|
||||||
"==": {
|
"==": {
|
||||||
metaMethod: "__eq",
|
metaMethod: "__eq",
|
||||||
nativeImplementation: (a, b) => a === b,
|
nativeImplementation: (a, b) => luaEquals(a, b),
|
||||||
},
|
},
|
||||||
"~=": {
|
"~=": {
|
||||||
metaMethod: "__ne",
|
metaMethod: "__ne",
|
||||||
nativeImplementation: (a, b) => a !== b,
|
nativeImplementation: (a, b) => !luaEquals(a, b),
|
||||||
},
|
},
|
||||||
"!=": {
|
"!=": {
|
||||||
metaMethod: "__ne",
|
metaMethod: "__ne",
|
||||||
nativeImplementation: (a, b) => a !== b,
|
nativeImplementation: (a, b) => !luaEquals(a, b),
|
||||||
},
|
},
|
||||||
"<": { metaMethod: "__lt", nativeImplementation: (a, b) => a < b },
|
"<": { metaMethod: "__lt", nativeImplementation: (a, b) => a < b },
|
||||||
"<=": { metaMethod: "__le", nativeImplementation: (a, b) => a <= b },
|
"<=": { metaMethod: "__le", nativeImplementation: (a, b) => a <= b },
|
||||||
|
|
|
@ -730,3 +730,10 @@ assert_equal(r[2].name, "Jane")
|
||||||
assert_equal(r[2].age, 21)
|
assert_equal(r[2].age, 21)
|
||||||
assert_equal(r[1].lastModified, nil)
|
assert_equal(r[1].lastModified, nil)
|
||||||
assert_equal(r[2].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)
|
||||||
|
|
|
@ -97,12 +97,12 @@ QueryClause {
|
||||||
LimitClause
|
LimitClause
|
||||||
}
|
}
|
||||||
|
|
||||||
FromClause { ckw<"from"> Name "=" exp }
|
FromClause { ckw<"from"> (Name "=")? exp }
|
||||||
WhereClause { ckw<"where"> exp }
|
WhereClause { ckw<"where"> exp }
|
||||||
LimitClause { ckw<"limit"> exp ("," exp)? }
|
LimitClause { ckw<"limit"> exp ("," exp)? }
|
||||||
OrderByClause { ckw<"order"> ckw<"by"> list<OrderBy> }
|
OrderByClause { ckw<"order"> ckw<"by"> list<OrderBy> }
|
||||||
OrderBy { exp ckw<"desc">? }
|
OrderBy { exp ckw<"desc">? }
|
||||||
SelectClause { ckw<"select"> TableConstructor }
|
SelectClause { ckw<"select"> exp }
|
||||||
|
|
||||||
|
|
||||||
field[@isGroup=Field] {
|
field[@isGroup=Field] {
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -105,7 +105,7 @@ Deno.test("Test comment handling", () => {
|
||||||
|
|
||||||
Deno.test("Test query parsing", () => {
|
Deno.test("Test query parsing", () => {
|
||||||
parse(`_(query[[from p = tag("page") where p.name == "John" limit 10, 3]])`);
|
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(
|
parse(
|
||||||
`_(query[[from p = tag("page") order by p.lastModified desc, p.name]])`,
|
`_(query[[from p = tag("page") order by p.lastModified desc, p.name]])`,
|
||||||
);
|
);
|
||||||
|
|
|
@ -20,7 +20,6 @@ import type {
|
||||||
LuaPrefixExpression,
|
LuaPrefixExpression,
|
||||||
LuaQueryClause,
|
LuaQueryClause,
|
||||||
LuaStatement,
|
LuaStatement,
|
||||||
LuaTableConstructor,
|
|
||||||
LuaTableField,
|
LuaTableField,
|
||||||
} from "./ast.ts";
|
} from "./ast.ts";
|
||||||
import { tags as t } from "@lezer/highlight";
|
import { tags as t } from "@lezer/highlight";
|
||||||
|
@ -486,12 +485,21 @@ function parseQueryClause(t: ParseTree, ctx: ASTCtx): LuaQueryClause {
|
||||||
t = t.children![0];
|
t = t.children![0];
|
||||||
switch (t.type) {
|
switch (t.type) {
|
||||||
case "FromClause": {
|
case "FromClause": {
|
||||||
return {
|
if (t.children!.length === 4) {
|
||||||
type: "From",
|
// From clause with a name
|
||||||
name: t.children![1].children![0].text!,
|
return {
|
||||||
expression: parseExpression(t.children![3], ctx),
|
type: "From",
|
||||||
ctx: context(t, ctx),
|
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":
|
case "WhereClause":
|
||||||
return {
|
return {
|
||||||
|
@ -532,10 +540,7 @@ function parseQueryClause(t: ParseTree, ctx: ASTCtx): LuaQueryClause {
|
||||||
case "SelectClause": {
|
case "SelectClause": {
|
||||||
return {
|
return {
|
||||||
type: "Select",
|
type: "Select",
|
||||||
tableConstructor: parseExpression(
|
expression: parseExpression(t.children![1], ctx),
|
||||||
t.children![1],
|
|
||||||
ctx,
|
|
||||||
) as LuaTableConstructor,
|
|
||||||
ctx: context(t, ctx),
|
ctx: context(t, ctx),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,33 +114,27 @@ Deno.test("ArrayQueryCollection", async () => {
|
||||||
const result8 = await collection2.query(
|
const result8 = await collection2.query(
|
||||||
{
|
{
|
||||||
objectVariable: "p",
|
objectVariable: "p",
|
||||||
select: [{
|
select: parseExpressionString("p.firstName .. ' ' .. p.lastName"),
|
||||||
name: "fullName",
|
|
||||||
expr: parseExpressionString("p.firstName .. ' ' .. p.lastName"),
|
|
||||||
}],
|
|
||||||
},
|
},
|
||||||
rootEnv,
|
rootEnv,
|
||||||
LuaStackFrame.lostFrame,
|
LuaStackFrame.lostFrame,
|
||||||
);
|
);
|
||||||
assertEquals(result8[0].fullName, "John Doe");
|
assertEquals(result8[0], "John Doe");
|
||||||
assertEquals(result8[1].fullName, "Alice Johnson");
|
assertEquals(result8[1], "Alice Johnson");
|
||||||
assertEquals(result8[2].fullName, "Jane Doe");
|
assertEquals(result8[2], "Jane Doe");
|
||||||
assertEquals(result8[3].fullName, "Bob Johnson");
|
assertEquals(result8[3], "Bob Johnson");
|
||||||
|
|
||||||
// Test select with native function
|
// Test select with native function
|
||||||
const result9 = await collection2.query(
|
const result9 = await collection2.query(
|
||||||
{
|
{
|
||||||
objectVariable: "p",
|
objectVariable: "p",
|
||||||
select: [{
|
select: parseExpressionString("build_name(p.firstName, p.lastName)"),
|
||||||
name: "fullName",
|
|
||||||
expr: parseExpressionString("build_name(p.firstName, p.lastName)"),
|
|
||||||
}],
|
|
||||||
},
|
},
|
||||||
rootEnv,
|
rootEnv,
|
||||||
LuaStackFrame.lostFrame,
|
LuaStackFrame.lostFrame,
|
||||||
);
|
);
|
||||||
assertEquals(result9[0].fullName, "John Doe");
|
assertEquals(result9[0], "John Doe");
|
||||||
assertEquals(result9[1].fullName, "Alice Johnson");
|
assertEquals(result9[1], "Alice Johnson");
|
||||||
assertEquals(result9[2].fullName, "Jane Doe");
|
assertEquals(result9[2], "Jane Doe");
|
||||||
assertEquals(result9[3].fullName, "Bob Johnson");
|
assertEquals(result9[3], "Bob Johnson");
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,11 +15,6 @@ export type LuaOrderBy = {
|
||||||
desc: boolean;
|
desc: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LuaSelect = {
|
|
||||||
name: string;
|
|
||||||
expr?: LuaExpression;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a query for a collection
|
* Represents a query for a collection
|
||||||
*/
|
*/
|
||||||
|
@ -30,7 +25,7 @@ export type LuaCollectionQuery = {
|
||||||
// The order by expression evaluated with Lua
|
// The order by expression evaluated with Lua
|
||||||
orderBy?: LuaOrderBy[];
|
orderBy?: LuaOrderBy[];
|
||||||
// The select expression evaluated with Lua
|
// The select expression evaluated with Lua
|
||||||
select?: LuaSelect[];
|
select?: LuaExpression;
|
||||||
// The limit of the query
|
// The limit of the query
|
||||||
limit?: number;
|
limit?: number;
|
||||||
// The offset of the query
|
// The offset of the query
|
||||||
|
@ -77,28 +72,6 @@ async function applyTransforms(
|
||||||
env: LuaEnv,
|
env: LuaEnv,
|
||||||
sf: LuaStackFrame,
|
sf: LuaStackFrame,
|
||||||
): Promise<any[]> {
|
): Promise<any[]> {
|
||||||
// Apply the select
|
|
||||||
if (query.select) {
|
|
||||||
const newResult = [];
|
|
||||||
for (const item of result) {
|
|
||||||
const itemEnv = buildItemEnv(query.objectVariable, item, env);
|
|
||||||
const newItem: Record<string, any> = {};
|
|
||||||
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
|
// Apply the order by
|
||||||
if (query.orderBy) {
|
if (query.orderBy) {
|
||||||
result = await asyncQuickSort(result, async (a, b) => {
|
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
|
// Apply the limit and offset
|
||||||
if (query.limit !== undefined && query.offset !== undefined) {
|
if (query.limit !== undefined && query.offset !== undefined) {
|
||||||
result = result.slice(query.offset, query.offset + query.limit);
|
result = result.slice(query.offset, query.offset + query.limit);
|
||||||
|
|
|
@ -437,6 +437,14 @@ export class LuaTable implements ILuaSettable, ILuaGettable {
|
||||||
return this.arrayPart.map(luaValueToJS);
|
return this.arrayPart.map(luaValueToJS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
asJS(): Record<string, any> | any[] {
|
||||||
|
if (this.length > 0) {
|
||||||
|
return this.asJSArray();
|
||||||
|
} else {
|
||||||
|
return this.asJSObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async toStringAsync(): Promise<string> {
|
async toStringAsync(): Promise<string> {
|
||||||
if (this.metatable?.has("__tostring")) {
|
if (this.metatable?.has("__tostring")) {
|
||||||
const metaValue = await this.metatable.get("__tostring");
|
const metaValue = await this.metatable.get("__tostring");
|
||||||
|
@ -557,6 +565,10 @@ export function luaCall(
|
||||||
return fn.call((sf || LuaStackFrame.lostFrame).withCtx(ctx), ...args);
|
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[] {
|
export function luaKeys(val: any): any[] {
|
||||||
if (val instanceof LuaTable) {
|
if (val instanceof LuaTable) {
|
||||||
return val.keys();
|
return val.keys();
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import {
|
import {
|
||||||
type ILuaFunction,
|
type ILuaFunction,
|
||||||
LuaBuiltinFunction,
|
LuaBuiltinFunction,
|
||||||
|
luaEquals,
|
||||||
|
LuaRuntimeError,
|
||||||
LuaTable,
|
LuaTable,
|
||||||
|
type LuaValue,
|
||||||
} from "$common/space_lua/runtime.ts";
|
} from "$common/space_lua/runtime.ts";
|
||||||
|
|
||||||
export const tableApi = new LuaTable({
|
export const tableApi = new LuaTable({
|
||||||
|
@ -40,4 +43,24 @@ export const tableApi = new LuaTable({
|
||||||
keys: new LuaBuiltinFunction((_sf, tbl: LuaTable) => {
|
keys: new LuaBuiltinFunction((_sf, tbl: LuaTable) => {
|
||||||
return tbl.keys();
|
return tbl.keys();
|
||||||
}),
|
}),
|
||||||
|
includes: new LuaBuiltinFunction(
|
||||||
|
(sf, tbl: LuaTable | Record<string, any>, 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,7 +6,7 @@ import type { CommonSystem } from "$common/common_system.ts";
|
||||||
import type { KV, KvKey, KvQuery } from "../../../plug-api/types.ts";
|
import type { KV, KvKey, KvQuery } from "../../../plug-api/types.ts";
|
||||||
import type { DataStore } from "../../data/datastore.ts";
|
import type { DataStore } from "../../data/datastore.ts";
|
||||||
import type { SysCallMapping } from "../system.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
|
* 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);
|
return ds.query(query, variables);
|
||||||
},
|
},
|
||||||
|
|
||||||
"datastore.queryLua": (
|
"datastore.queryLua": async (
|
||||||
_ctx,
|
_ctx,
|
||||||
prefix: string[],
|
prefix: string[],
|
||||||
query: LuaCollectionQuery,
|
query: LuaCollectionQuery,
|
||||||
): Promise<KV[]> => {
|
): Promise<KV[]> => {
|
||||||
const dsQueryCollection = new DataStoreQueryCollection(ds, prefix);
|
const dsQueryCollection = new DataStoreQueryCollection(ds, prefix);
|
||||||
return dsQueryCollection.query(
|
return (await dsQueryCollection.query(
|
||||||
query,
|
query,
|
||||||
commonSystem.spaceLuaEnv.env,
|
commonSystem.spaceLuaEnv.env,
|
||||||
LuaStackFrame.lostFrame,
|
LuaStackFrame.lostFrame,
|
||||||
);
|
)).map((item) => luaValueToJS(item));
|
||||||
},
|
},
|
||||||
|
|
||||||
"datastore.listFunctions": (): string[] => {
|
"datastore.listFunctions": (): string[] => {
|
||||||
|
|
|
@ -45,7 +45,7 @@ export class LuaWidget extends WidgetType {
|
||||||
|
|
||||||
toDOM(): HTMLElement {
|
toDOM(): HTMLElement {
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.className = "sb-lua-directive";
|
// div.className = "sb-lua-directive-inline";
|
||||||
const cacheItem = this.client.getWidgetCache(this.cacheKey);
|
const cacheItem = this.client.getWidgetCache(this.cacheKey);
|
||||||
if (cacheItem) {
|
if (cacheItem) {
|
||||||
div.innerHTML = cacheItem.html;
|
div.innerHTML = cacheItem.html;
|
||||||
|
@ -89,9 +89,9 @@ export class LuaWidget extends WidgetType {
|
||||||
html = widgetContent.html;
|
html = widgetContent.html;
|
||||||
div.innerHTML = html;
|
div.innerHTML = html;
|
||||||
if ((widgetContent as any)?.display === "block") {
|
if ((widgetContent as any)?.display === "block") {
|
||||||
div.style.display = "block";
|
div.className = "sb-lua-directive-block";
|
||||||
} else {
|
} else {
|
||||||
div.style.display = "inline";
|
div.className = "sb-lua-directive-inline";
|
||||||
}
|
}
|
||||||
attachWidgetEventHandlers(div, this.client, this.from);
|
attachWidgetEventHandlers(div, this.client, this.from);
|
||||||
this.client.setWidgetCache(
|
this.client.setWidgetCache(
|
||||||
|
@ -128,10 +128,13 @@ export class LuaWidget extends WidgetType {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((widgetContent as any)?.display === "block") {
|
if (
|
||||||
div.style.display = "block";
|
(widgetContent as any)?.display === "block" ||
|
||||||
|
trimmedMarkdown.includes("\n")
|
||||||
|
) {
|
||||||
|
div.className = "sb-lua-directive-block";
|
||||||
} else {
|
} else {
|
||||||
div.style.display = "inline";
|
div.className = "sb-lua-directive-inline";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the markdown again after trimming
|
// Parse the markdown again after trimming
|
||||||
|
|
|
@ -6,6 +6,7 @@ import type { Client } from "../client.ts";
|
||||||
const straightQuoteContexts = [
|
const straightQuoteContexts = [
|
||||||
"CommentBlock",
|
"CommentBlock",
|
||||||
"CodeBlock",
|
"CodeBlock",
|
||||||
|
"CodeText",
|
||||||
"FencedCode",
|
"FencedCode",
|
||||||
"InlineCode",
|
"InlineCode",
|
||||||
"FrontMatterCode",
|
"FrontMatterCode",
|
||||||
|
|
|
@ -334,14 +334,22 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sb-lua-directive {
|
.sb-lua-directive-inline {
|
||||||
background-color: rgb(233, 232, 232, 35%);
|
display: inline;
|
||||||
border: 1px #d3d3d373 solid;
|
border: 1px var(--editor-widget-background-color) solid;
|
||||||
/* box-shadow: #d1d1d1 0 0 4px; */
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 2px;
|
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,
|
a.sb-wiki-link-page-missing,
|
||||||
.sb-wiki-link-page-missing > .sb-wiki-link-page {
|
.sb-wiki-link-page-missing > .sb-wiki-link-page {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
|
|
@ -1,18 +1,32 @@
|
||||||
> **warning** Experimental
|
> **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.
|
> 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
|
# 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
|
* Scripting: replace [[Space Script]] (JavaScript) with a more controlled, simple and extensible language.
|
||||||
* Ultimately succeed [[Space Script]] (for most, if not all) use cases
|
* Replace [[Expression Language]], [[Template Language]] and [[Query Language]] with Lua-based equivalents.
|
||||||
* Ultimately replace [[Expression Language]] with Lua’s expression language, also in [[Query Language]].
|
* (Potentially) provide an alternative way to specify [[Space Config]]
|
||||||
* Ultimately replace [[Template Language]] with a variant using Lua’s control flows (`for`, `if` etc.)
|
|
||||||
|
|
||||||
# Use
|
# Introduction approach
|
||||||
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:
|
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
|
```space-lua
|
||||||
-- adds two numbers
|
-- adds two numbers
|
||||||
|
@ -21,14 +35,41 @@ function adder(a, b)
|
||||||
end
|
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
|
## 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
|
* `markdown`: Renders the value as markdown
|
||||||
* `html`: Renders the value as HTML
|
* `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!"}
|
${marquee "Finally, marqeeeeeeee!"}
|
||||||
Oh boy, the times we live in!
|
Oh boy, the times we live in!
|
||||||
|
|
||||||
|
@ -88,30 +129,17 @@ define_event_listener {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Custom functions
|
# Space Lua Extensions
|
||||||
Any global function (so not marked with `local`) is automatically exposed to be used in [[Live Queries]] and [[Live Templates]]:
|
Space Lua currently introduces a few new features on top core Lua:
|
||||||
|
|
||||||
```space-lua
|
1. [[Space Lua/Lua Integrated Query]], embedding a [[Query Language]]-like language into Lua itself
|
||||||
-- This is a global function, therefore automatically exposed
|
2. Thread locals
|
||||||
function greet_me(name)
|
|
||||||
return "Hello, " .. name
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Whereas this one is not
|
## Thread locals
|
||||||
local function greet_you(name)
|
There’s a magic `_CTX` global variable available from which you can access useful context-specific values. Currently the following keys are available:
|
||||||
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:
|
|
||||||
|
|
||||||
* `_CTX.pageMeta` contains a reference to the loaded page metadata (can be `nil` when not yet loaded)
|
* `_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
|
# API
|
||||||
Lua APIs, which should be (roughly) implemented according to the Lua standard.
|
Lua APIs, which should be (roughly) implemented according to the Lua standard.
|
||||||
|
|
|
@ -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 <var> = <expression>
|
||||||
|
where <expression>
|
||||||
|
order by <expression>
|
||||||
|
limit <expression>, <expression>
|
||||||
|
select <expression>
|
||||||
|
]]
|
||||||
|
|
||||||
|
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 <expression>`
|
||||||
|
The `from` clause specifies the source of your data. There are two syntactic variants:
|
||||||
|
|
||||||
|
With explicit variable binding:
|
||||||
|
|
||||||
|
from v = <<expression>>
|
||||||
|
|
||||||
|
binding each item to the variable `v`.
|
||||||
|
|
||||||
|
And the shorter:
|
||||||
|
|
||||||
|
from <<expression>>
|
||||||
|
|
||||||
|
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 <expression>`
|
||||||
|
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 <expression> [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 <expression>[, <expression>]`
|
||||||
|
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 <expression>`
|
||||||
|
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
|
||||||
|
]]}
|
|
@ -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]]}
|
Loading…
Reference in New Issue