Breaking Lua API changes:
* tpl -> template.new * define_command -> command.define * define_event_listener -> event.listen * tag -> index.tag Updated in the docspull/1224/head
parent
72b4ecdc36
commit
6078452a6c
|
@ -62,7 +62,7 @@ export abstract class CommonSystem {
|
||||||
Object.keys(this.scriptEnv.eventHandlers).length,
|
Object.keys(this.scriptEnv.eventHandlers).length,
|
||||||
"event handlers from space-script",
|
"event handlers from space-script",
|
||||||
);
|
);
|
||||||
await this.spaceLuaEnv.reload(this.system, this.scriptEnv);
|
await this.spaceLuaEnv.reload(this.system);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error("Error loading space-script:", e.message);
|
console.error("Error loading space-script:", e.message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ 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 { 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";
|
||||||
|
|
||||||
|
@ -24,7 +23,6 @@ export class SpaceLuaEnvironment {
|
||||||
*/
|
*/
|
||||||
async reload(
|
async reload(
|
||||||
system: System<any>,
|
system: System<any>,
|
||||||
scriptEnv: ScriptEnvironment,
|
|
||||||
) {
|
) {
|
||||||
const allScripts: ScriptObject[] = await system.invokeFunction(
|
const allScripts: ScriptObject[] = await system.invokeFunction(
|
||||||
"index.queryObjects",
|
"index.queryObjects",
|
||||||
|
@ -36,7 +34,7 @@ export class SpaceLuaEnvironment {
|
||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
this.env = buildLuaEnv(system, scriptEnv);
|
this.env = buildLuaEnv(system);
|
||||||
const tl = new LuaEnv();
|
const tl = new LuaEnv();
|
||||||
tl.setLocal("_GLOBAL", this.env);
|
tl.setLocal("_GLOBAL", this.env);
|
||||||
for (const script of allScripts) {
|
for (const script of allScripts) {
|
||||||
|
|
|
@ -267,6 +267,7 @@ export function evalExpression(
|
||||||
sf.withCtx(e.ctx),
|
sf.withCtx(e.ctx),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
collection = luaValueToJS(collection);
|
||||||
// Check if collection is a queryable collection
|
// Check if collection is a queryable collection
|
||||||
if (!collection.query) {
|
if (!collection.query) {
|
||||||
// If not, try to convert it to JS and see if it's an array
|
// If not, try to convert it to JS and see if it's an array
|
||||||
|
|
|
@ -104,10 +104,12 @@ 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 tag("page") select {name="hello", age=10}]])`);
|
|
||||||
parse(
|
parse(
|
||||||
`_(query[[from p = tag("page") order by p.lastModified desc, p.name]])`,
|
`_(query[[from p = index.tag("page") where p.name == "John" limit 10, 3]])`,
|
||||||
);
|
);
|
||||||
parse(`_(query[[from p = tag("page") order by p.lastModified]])`);
|
parse(`_(query[[from index.tag("page") select {name="hello", age=10}]])`);
|
||||||
|
parse(
|
||||||
|
`_(query[[from p = index.tag("page") order by p.lastModified desc, p.name]])`,
|
||||||
|
);
|
||||||
|
parse(`_(query[[from p = index.tag("page") order by p.lastModified]])`);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,31 +1,21 @@
|
||||||
import {
|
import {
|
||||||
type ILuaFunction,
|
type ILuaFunction,
|
||||||
jsToLuaValue,
|
|
||||||
LuaBuiltinFunction,
|
LuaBuiltinFunction,
|
||||||
luaCall,
|
luaCall,
|
||||||
LuaEnv,
|
LuaEnv,
|
||||||
luaGet,
|
luaGet,
|
||||||
LuaMultiRes,
|
LuaMultiRes,
|
||||||
LuaRuntimeError,
|
LuaRuntimeError,
|
||||||
LuaTable,
|
type LuaTable,
|
||||||
luaToString,
|
luaToString,
|
||||||
luaTypeOf,
|
luaTypeOf,
|
||||||
type LuaValue,
|
type LuaValue,
|
||||||
luaValueToJS,
|
|
||||||
} from "$common/space_lua/runtime.ts";
|
} from "$common/space_lua/runtime.ts";
|
||||||
import { stringApi } from "$common/space_lua/stdlib/string.ts";
|
import { stringApi } from "$common/space_lua/stdlib/string.ts";
|
||||||
import { tableApi } from "$common/space_lua/stdlib/table.ts";
|
import { tableApi } from "$common/space_lua/stdlib/table.ts";
|
||||||
import { osApi } from "$common/space_lua/stdlib/os.ts";
|
import { osApi } from "$common/space_lua/stdlib/os.ts";
|
||||||
import { jsApi } from "$common/space_lua/stdlib/js.ts";
|
import { jsApi } from "$common/space_lua/stdlib/js.ts";
|
||||||
import {
|
import { spaceLuaApi } from "$common/space_lua/stdlib/space_lua.ts";
|
||||||
interpolateLuaString,
|
|
||||||
spaceLuaApi,
|
|
||||||
} from "$common/space_lua/stdlib/space_lua.ts";
|
|
||||||
import {
|
|
||||||
findAllQueryVariables,
|
|
||||||
type LuaCollectionQuery,
|
|
||||||
type LuaQueryCollection,
|
|
||||||
} from "$common/space_lua/query_collection.ts";
|
|
||||||
import { templateApi } from "$common/space_lua/stdlib/template.ts";
|
import { templateApi } from "$common/space_lua/stdlib/template.ts";
|
||||||
import { mathApi } from "$common/space_lua/stdlib/math.ts";
|
import { mathApi } from "$common/space_lua/stdlib/math.ts";
|
||||||
|
|
||||||
|
@ -149,68 +139,6 @@ const getmetatableFunction = new LuaBuiltinFunction((_sf, table: LuaTable) => {
|
||||||
return table.metatable;
|
return table.metatable;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Non-standard
|
|
||||||
const tagFunction = new LuaBuiltinFunction(
|
|
||||||
(sf, tagName: LuaValue): LuaQueryCollection => {
|
|
||||||
const global = sf.threadLocal.get("_GLOBAL");
|
|
||||||
if (!global) {
|
|
||||||
throw new LuaRuntimeError("Global not found", sf);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
query: async (query: LuaCollectionQuery, env: LuaEnv): Promise<any[]> => {
|
|
||||||
const localVars = findAllQueryVariables(query).filter((v) =>
|
|
||||||
!global.has(v) && v !== "_"
|
|
||||||
);
|
|
||||||
const scopedVariables: Record<string, any> = {};
|
|
||||||
for (const v of localVars) {
|
|
||||||
try {
|
|
||||||
const jsonValue = await luaValueToJS(env.get(v));
|
|
||||||
// Ensure this is JSON serializable
|
|
||||||
JSON.stringify(jsonValue);
|
|
||||||
scopedVariables[v] = jsonValue;
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(
|
|
||||||
"Failed to JSON serialize variable",
|
|
||||||
v,
|
|
||||||
e,
|
|
||||||
);
|
|
||||||
throw new LuaRuntimeError(
|
|
||||||
`Failed to JSON serialize variable ${v} in query`,
|
|
||||||
sf,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (await global.get("datastore").get("query_lua").call(
|
|
||||||
sf,
|
|
||||||
[
|
|
||||||
"idx",
|
|
||||||
tagName,
|
|
||||||
],
|
|
||||||
query,
|
|
||||||
scopedVariables,
|
|
||||||
)).toJSArray();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const tplFunction = new LuaBuiltinFunction(
|
|
||||||
(_sf, template: string): ILuaFunction => {
|
|
||||||
const lines = template.split("\n").map((line) =>
|
|
||||||
line.replace(/^\s{4}/, "")
|
|
||||||
);
|
|
||||||
const processed = lines.join("\n");
|
|
||||||
return new LuaBuiltinFunction(
|
|
||||||
async (sf, env: LuaTable | any) => {
|
|
||||||
if (!(env instanceof LuaTable)) {
|
|
||||||
env = jsToLuaValue(env);
|
|
||||||
}
|
|
||||||
return await interpolateLuaString(sf, processed, env);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export function luaBuildStandardEnv() {
|
export function luaBuildStandardEnv() {
|
||||||
const env = new LuaEnv();
|
const env = new LuaEnv();
|
||||||
// Top-level builtins
|
// Top-level builtins
|
||||||
|
@ -231,9 +159,6 @@ export function luaBuildStandardEnv() {
|
||||||
env.set("error", errorFunction);
|
env.set("error", errorFunction);
|
||||||
env.set("pcall", pcallFunction);
|
env.set("pcall", pcallFunction);
|
||||||
env.set("xpcall", xpcallFunction);
|
env.set("xpcall", xpcallFunction);
|
||||||
// Non-standard
|
|
||||||
env.set("tag", tagFunction);
|
|
||||||
env.set("tpl", tplFunction);
|
|
||||||
// APIs
|
// APIs
|
||||||
env.set("string", stringApi);
|
env.set("string", stringApi);
|
||||||
env.set("table", tableApi);
|
env.set("table", tableApi);
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import {
|
import {
|
||||||
type ILuaFunction,
|
type ILuaFunction,
|
||||||
|
jsToLuaValue,
|
||||||
LuaBuiltinFunction,
|
LuaBuiltinFunction,
|
||||||
LuaTable,
|
LuaTable,
|
||||||
} from "$common/space_lua/runtime.ts";
|
} from "$common/space_lua/runtime.ts";
|
||||||
|
import { interpolateLuaString } from "$common/space_lua/stdlib/space_lua.ts";
|
||||||
|
|
||||||
export const templateApi = new LuaTable({
|
export const templateApi = new LuaTable({
|
||||||
each: new LuaBuiltinFunction(
|
each: new LuaBuiltinFunction(
|
||||||
|
@ -17,4 +19,20 @@ export const templateApi = new LuaTable({
|
||||||
return result.join("");
|
return result.join("");
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
new: new LuaBuiltinFunction(
|
||||||
|
(_sf, template: string): ILuaFunction => {
|
||||||
|
const lines = template.split("\n").map((line) =>
|
||||||
|
line.replace(/^\s{4}/, "")
|
||||||
|
);
|
||||||
|
const processed = lines.join("\n");
|
||||||
|
return new LuaBuiltinFunction(
|
||||||
|
async (sf, env: LuaTable | any) => {
|
||||||
|
if (!(env instanceof LuaTable)) {
|
||||||
|
env = jsToLuaValue(env);
|
||||||
|
}
|
||||||
|
return await interpolateLuaString(sf, processed, env);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,24 +1,18 @@
|
||||||
import { luaBuildStandardEnv } from "$common/space_lua/stdlib.ts";
|
import { luaBuildStandardEnv } from "$common/space_lua/stdlib.ts";
|
||||||
import { parsePageRef } from "@silverbulletmd/silverbullet/lib/page_ref";
|
import { parsePageRef } from "@silverbulletmd/silverbullet/lib/page_ref";
|
||||||
import {
|
import {
|
||||||
jsToLuaValue,
|
|
||||||
LuaBuiltinFunction,
|
|
||||||
LuaEnv,
|
LuaEnv,
|
||||||
LuaNativeJSFunction,
|
LuaNativeJSFunction,
|
||||||
LuaStackFrame,
|
LuaStackFrame,
|
||||||
LuaTable,
|
LuaTable,
|
||||||
} from "$common/space_lua/runtime.ts";
|
} from "$common/space_lua/runtime.ts";
|
||||||
import type { System } from "$lib/plugos/system.ts";
|
import type { System } from "$lib/plugos/system.ts";
|
||||||
import type { ScriptEnvironment } from "$common/space_script.ts";
|
|
||||||
import type { CommandDef } from "$lib/command.ts";
|
|
||||||
|
|
||||||
export function buildLuaEnv(system: System<any>, scriptEnv: ScriptEnvironment) {
|
export function buildLuaEnv(system: System<any>) {
|
||||||
const env = new LuaEnv(luaBuildStandardEnv());
|
const env = new LuaEnv(luaBuildStandardEnv());
|
||||||
|
|
||||||
// Expose all syscalls to Lua
|
// Expose all syscalls to Lua
|
||||||
exposeSyscalls(env, system);
|
exposeSyscalls(env, system);
|
||||||
// Support defining commands and subscriptions from Lua
|
|
||||||
exposeDefinitions(env, system, scriptEnv);
|
|
||||||
|
|
||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
@ -45,82 +39,7 @@ function exposeSyscalls(env: LuaEnv, system: System<any>) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function exposeDefinitions(
|
export function buildThreadLocalEnv(_system: System<any>, globalEnv: LuaEnv) {
|
||||||
env: LuaEnv,
|
|
||||||
system: System<any>,
|
|
||||||
scriptEnv: ScriptEnvironment,
|
|
||||||
) {
|
|
||||||
// Expose the command registration function to Lua via define_command({name="foo", function() ... end})
|
|
||||||
env.set(
|
|
||||||
"define_command",
|
|
||||||
new LuaBuiltinFunction(
|
|
||||||
(_sf, def: LuaTable) => {
|
|
||||||
if (def.get(1) === undefined) {
|
|
||||||
throw new Error("Callback is required");
|
|
||||||
}
|
|
||||||
if (!def.get("name")) {
|
|
||||||
throw new Error("Name is required");
|
|
||||||
}
|
|
||||||
const fn = def.get(1);
|
|
||||||
console.log(
|
|
||||||
`[Lua] Registering command '${
|
|
||||||
def.get("name")
|
|
||||||
}' (source: ${fn.body.ctx.ref})`,
|
|
||||||
);
|
|
||||||
scriptEnv.registerCommand(
|
|
||||||
{
|
|
||||||
name: def.get("name"),
|
|
||||||
key: def.get("key"),
|
|
||||||
mac: def.get("mac"),
|
|
||||||
priority: def.get("priority"),
|
|
||||||
requireMode: def.get("require_mode"),
|
|
||||||
hide: def.get("hide"),
|
|
||||||
} as CommandDef,
|
|
||||||
async (...args: any[]) => {
|
|
||||||
const tl = await buildThreadLocalEnv(system, env);
|
|
||||||
const sf = new LuaStackFrame(tl, null);
|
|
||||||
try {
|
|
||||||
return await fn.call(sf, ...args.map(jsToLuaValue));
|
|
||||||
} catch (e: any) {
|
|
||||||
await handleLuaError(e, system);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
env.set(
|
|
||||||
"define_event_listener",
|
|
||||||
new LuaBuiltinFunction((_sf, def: LuaTable) => {
|
|
||||||
if (def.get(1) === undefined) {
|
|
||||||
throw new Error("Callback is required");
|
|
||||||
}
|
|
||||||
if (!def.get("event")) {
|
|
||||||
throw new Error("Event is required");
|
|
||||||
}
|
|
||||||
const fn = def.get(1);
|
|
||||||
console.log(
|
|
||||||
`[Lua] Subscribing to event '${
|
|
||||||
def.get("event")
|
|
||||||
}' (source: ${fn.body.ctx.ref})`,
|
|
||||||
);
|
|
||||||
scriptEnv.registerEventListener(
|
|
||||||
{ name: def.get("event") },
|
|
||||||
async (...args: any[]) => {
|
|
||||||
const tl = await buildThreadLocalEnv(system, env);
|
|
||||||
const sf = new LuaStackFrame(tl, null);
|
|
||||||
try {
|
|
||||||
return await fn.call(sf, ...args.map(jsToLuaValue));
|
|
||||||
} catch (e: any) {
|
|
||||||
await handleLuaError(e, system);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildThreadLocalEnv(_system: System<any>, globalEnv: LuaEnv) {
|
|
||||||
const tl = new LuaEnv();
|
const tl = new LuaEnv();
|
||||||
// const currentPageMeta = await system.localSyscall(
|
// const currentPageMeta = await system.localSyscall(
|
||||||
// "editor.getCurrentPageMeta",
|
// "editor.getCurrentPageMeta",
|
||||||
|
@ -131,7 +50,7 @@ function buildThreadLocalEnv(_system: System<any>, globalEnv: LuaEnv) {
|
||||||
return Promise.resolve(tl);
|
return Promise.resolve(tl);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleLuaError(e: any, system: System<any>) {
|
export async function handleLuaError(e: any, system: System<any>) {
|
||||||
console.error(
|
console.error(
|
||||||
"Lua eval exception",
|
"Lua eval exception",
|
||||||
e.message,
|
e.message,
|
||||||
|
|
|
@ -20,7 +20,7 @@ type AttributeExtractorDef = {
|
||||||
tags: string[];
|
tags: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type EventListenerDef = {
|
export type EventListenerDef = {
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import type { SysCallMapping } from "$lib/plugos/system.ts";
|
||||||
|
import type { CommandDef } from "$lib/command.ts";
|
||||||
|
import { buildThreadLocalEnv, handleLuaError } from "$common/space_lua_api.ts";
|
||||||
|
import {
|
||||||
|
type ILuaFunction,
|
||||||
|
jsToLuaValue,
|
||||||
|
luaCall,
|
||||||
|
LuaStackFrame,
|
||||||
|
luaValueToJS,
|
||||||
|
} from "$common/space_lua/runtime.ts";
|
||||||
|
import type { CommonSystem } from "$common/common_system.ts";
|
||||||
|
|
||||||
|
export type CallbackCommandDef = CommandDef & {
|
||||||
|
run: ILuaFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function commandSyscalls(
|
||||||
|
commonSystem: CommonSystem,
|
||||||
|
): SysCallMapping {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Define a Lua command
|
||||||
|
* @param def - The command definition
|
||||||
|
* @param luaCallback - The Lua callback
|
||||||
|
*/
|
||||||
|
"command.define": (
|
||||||
|
_ctx,
|
||||||
|
def: CallbackCommandDef,
|
||||||
|
) => {
|
||||||
|
console.log("Registering Lua command: ", def.name);
|
||||||
|
commonSystem.scriptEnv.registerCommand(
|
||||||
|
def,
|
||||||
|
async (...args: any[]) => {
|
||||||
|
const tl = await buildThreadLocalEnv(
|
||||||
|
commonSystem.system,
|
||||||
|
commonSystem.spaceLuaEnv.env,
|
||||||
|
);
|
||||||
|
const sf = new LuaStackFrame(tl, null);
|
||||||
|
try {
|
||||||
|
return luaValueToJS(
|
||||||
|
await luaCall(def.run, args.map(jsToLuaValue), sf),
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
await handleLuaError(e, commonSystem.system);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
import type { SysCallMapping } from "$lib/plugos/system.ts";
|
||||||
|
import type { EventListenerDef } from "$common/space_script.ts";
|
||||||
|
import { buildThreadLocalEnv, handleLuaError } from "$common/space_lua_api.ts";
|
||||||
|
import {
|
||||||
|
type ILuaFunction,
|
||||||
|
jsToLuaValue,
|
||||||
|
luaCall,
|
||||||
|
LuaStackFrame,
|
||||||
|
luaValueToJS,
|
||||||
|
} from "$common/space_lua/runtime.ts";
|
||||||
|
import type { CommonSystem } from "$common/common_system.ts";
|
||||||
|
|
||||||
|
export type CallbackEventListener = EventListenerDef & {
|
||||||
|
run: ILuaFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function eventListenerSyscalls(
|
||||||
|
commonSystem: CommonSystem,
|
||||||
|
): SysCallMapping {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Define a Lua event listener
|
||||||
|
*/
|
||||||
|
"event.listen": (
|
||||||
|
_ctx,
|
||||||
|
def: CallbackEventListener,
|
||||||
|
) => {
|
||||||
|
console.log("Registering Lua event listener: ", def.name);
|
||||||
|
commonSystem.scriptEnv.registerEventListener(
|
||||||
|
def,
|
||||||
|
async (...args: any[]) => {
|
||||||
|
const tl = await buildThreadLocalEnv(
|
||||||
|
commonSystem.system,
|
||||||
|
commonSystem.spaceLuaEnv.env,
|
||||||
|
);
|
||||||
|
const sf = new LuaStackFrame(tl, null);
|
||||||
|
try {
|
||||||
|
return luaValueToJS(
|
||||||
|
await luaCall(def.run, args.map(jsToLuaValue), sf),
|
||||||
|
);
|
||||||
|
} catch (e: any) {
|
||||||
|
await handleLuaError(e, commonSystem.system);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -3,15 +3,26 @@ import type {
|
||||||
ObjectQuery,
|
ObjectQuery,
|
||||||
ObjectValue,
|
ObjectValue,
|
||||||
} from "@silverbulletmd/silverbullet/types";
|
} from "@silverbulletmd/silverbullet/types";
|
||||||
import type { SysCallMapping, System } from "$lib/plugos/system.ts";
|
import type { SysCallMapping } from "$lib/plugos/system.ts";
|
||||||
import type { LuaCollectionQuery } from "$common/space_lua/query_collection.ts";
|
import {
|
||||||
|
findAllQueryVariables,
|
||||||
|
type LuaCollectionQuery,
|
||||||
|
type LuaQueryCollection,
|
||||||
|
} from "$common/space_lua/query_collection.ts";
|
||||||
|
import {
|
||||||
|
type LuaEnv,
|
||||||
|
LuaRuntimeError,
|
||||||
|
type LuaStackFrame,
|
||||||
|
luaValueToJS,
|
||||||
|
} from "$common/space_lua/runtime.ts";
|
||||||
|
import type { CommonSystem } from "$common/common_system.ts";
|
||||||
|
|
||||||
// These are just wrappers around the system.invokeFunction calls, but they make it easier to use the index
|
// These are just wrappers around the system.invokeFunction calls, but they make it easier to use the index
|
||||||
|
|
||||||
export function indexSyscalls(system: System<any>): SysCallMapping {
|
export function indexSyscalls(commonSystem: CommonSystem): SysCallMapping {
|
||||||
return {
|
return {
|
||||||
"index.indexObjects": (ctx, page: string, objects: ObjectValue<any>[]) => {
|
"index.indexObjects": (ctx, page: string, objects: ObjectValue<any>[]) => {
|
||||||
return system.syscall(ctx, "system.invokeFunction", [
|
return commonSystem.system.syscall(ctx, "system.invokeFunction", [
|
||||||
"index.indexObjects",
|
"index.indexObjects",
|
||||||
page,
|
page,
|
||||||
objects,
|
objects,
|
||||||
|
@ -23,7 +34,7 @@ export function indexSyscalls(system: System<any>): SysCallMapping {
|
||||||
query: ObjectQuery,
|
query: ObjectQuery,
|
||||||
ttlSecs?: number,
|
ttlSecs?: number,
|
||||||
) => {
|
) => {
|
||||||
return system.syscall(ctx, "system.invokeFunction", [
|
return commonSystem.system.syscall(ctx, "system.invokeFunction", [
|
||||||
"index.queryObjects",
|
"index.queryObjects",
|
||||||
tag,
|
tag,
|
||||||
query,
|
query,
|
||||||
|
@ -36,7 +47,7 @@ export function indexSyscalls(system: System<any>): SysCallMapping {
|
||||||
query: LuaCollectionQuery,
|
query: LuaCollectionQuery,
|
||||||
scopedVariables?: Record<string, any>,
|
scopedVariables?: Record<string, any>,
|
||||||
) => {
|
) => {
|
||||||
return system.syscall(ctx, "system.invokeFunction", [
|
return commonSystem.system.syscall(ctx, "system.invokeFunction", [
|
||||||
"index.queryLuaObjects",
|
"index.queryLuaObjects",
|
||||||
tag,
|
tag,
|
||||||
query,
|
query,
|
||||||
|
@ -44,26 +55,68 @@ export function indexSyscalls(system: System<any>): SysCallMapping {
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
"index.queryDeleteObjects": (ctx, tag: string, query: ObjectQuery) => {
|
"index.queryDeleteObjects": (ctx, tag: string, query: ObjectQuery) => {
|
||||||
return system.syscall(ctx, "system.invokeFunction", [
|
return commonSystem.system.syscall(ctx, "system.invokeFunction", [
|
||||||
"index.queryDeleteObjects",
|
"index.queryDeleteObjects",
|
||||||
tag,
|
tag,
|
||||||
query,
|
query,
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
"index.query": (ctx, query: KvQuery, variables?: Record<string, any>) => {
|
"index.query": (ctx, query: KvQuery, variables?: Record<string, any>) => {
|
||||||
return system.syscall(ctx, "system.invokeFunction", [
|
return commonSystem.system.syscall(ctx, "system.invokeFunction", [
|
||||||
"index.query",
|
"index.query",
|
||||||
query,
|
query,
|
||||||
variables,
|
variables,
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
"index.getObjectByRef": (ctx, page: string, tag: string, ref: string) => {
|
"index.getObjectByRef": (ctx, page: string, tag: string, ref: string) => {
|
||||||
return system.syscall(ctx, "system.invokeFunction", [
|
return commonSystem.system.syscall(ctx, "system.invokeFunction", [
|
||||||
"index.getObjectByRef",
|
"index.getObjectByRef",
|
||||||
page,
|
page,
|
||||||
tag,
|
tag,
|
||||||
ref,
|
ref,
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
"index.tag": (_ctx, tagName: string): LuaQueryCollection => {
|
||||||
|
return {
|
||||||
|
query: async (
|
||||||
|
query: LuaCollectionQuery,
|
||||||
|
env: LuaEnv,
|
||||||
|
sf: LuaStackFrame,
|
||||||
|
): Promise<any[]> => {
|
||||||
|
const global = commonSystem.spaceLuaEnv.env;
|
||||||
|
const localVars = findAllQueryVariables(query).filter((v) =>
|
||||||
|
!global.has(v) && v !== "_"
|
||||||
|
);
|
||||||
|
const scopedVariables: Record<string, any> = {};
|
||||||
|
for (const v of localVars) {
|
||||||
|
try {
|
||||||
|
const jsonValue = await luaValueToJS(env.get(v));
|
||||||
|
// Ensure this is JSON serializable
|
||||||
|
JSON.stringify(jsonValue);
|
||||||
|
scopedVariables[v] = jsonValue;
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(
|
||||||
|
"Failed to JSON serialize variable",
|
||||||
|
v,
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
throw new LuaRuntimeError(
|
||||||
|
`Failed to JSON serialize variable ${v} in query`,
|
||||||
|
sf,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (await global.get("datastore").get("query_lua").call(
|
||||||
|
sf,
|
||||||
|
[
|
||||||
|
"idx",
|
||||||
|
tagName,
|
||||||
|
],
|
||||||
|
query,
|
||||||
|
scopedVariables,
|
||||||
|
)).toJSArray();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,8 @@ import { plugPrefix } from "$common/spaces/constants.ts";
|
||||||
import { base64EncodedDataUrl } from "$lib/crypto.ts";
|
import { base64EncodedDataUrl } from "$lib/crypto.ts";
|
||||||
import type { ConfigContainer } from "../type/config.ts";
|
import type { ConfigContainer } from "../type/config.ts";
|
||||||
import { indexSyscalls } from "$common/syscalls/index.ts";
|
import { indexSyscalls } from "$common/syscalls/index.ts";
|
||||||
|
import { commandSyscalls } from "$common/syscalls/command.ts";
|
||||||
|
import { eventListenerSyscalls } from "$common/syscalls/event.ts";
|
||||||
|
|
||||||
const fileListInterval = 30 * 1000; // 30s
|
const fileListInterval = 30 * 1000; // 30s
|
||||||
|
|
||||||
|
@ -122,6 +124,7 @@ export class ServerSystem extends CommonSystem {
|
||||||
this.system.registerSyscalls(
|
this.system.registerSyscalls(
|
||||||
[],
|
[],
|
||||||
eventSyscalls(this.eventHook),
|
eventSyscalls(this.eventHook),
|
||||||
|
eventListenerSyscalls(this),
|
||||||
spaceReadSyscalls(space, this.allKnownFiles),
|
spaceReadSyscalls(space, this.allKnownFiles),
|
||||||
assetSyscalls(this.system),
|
assetSyscalls(this.system),
|
||||||
yamlSyscalls(),
|
yamlSyscalls(),
|
||||||
|
@ -134,7 +137,8 @@ export class ServerSystem extends CommonSystem {
|
||||||
mqSyscalls(this.mq),
|
mqSyscalls(this.mq),
|
||||||
languageSyscalls(),
|
languageSyscalls(),
|
||||||
jsonschemaSyscalls(),
|
jsonschemaSyscalls(),
|
||||||
indexSyscalls(this.system),
|
indexSyscalls(this),
|
||||||
|
commandSyscalls(this),
|
||||||
luaSyscalls(),
|
luaSyscalls(),
|
||||||
templateSyscalls(this.ds),
|
templateSyscalls(this.ds),
|
||||||
dataStoreReadSyscalls(this.ds, this),
|
dataStoreReadSyscalls(this.ds, this),
|
||||||
|
|
|
@ -45,6 +45,8 @@ import { plugPrefix } from "$common/spaces/constants.ts";
|
||||||
import { jsonschemaSyscalls } from "$common/syscalls/jsonschema.ts";
|
import { jsonschemaSyscalls } from "$common/syscalls/jsonschema.ts";
|
||||||
import { luaSyscalls } from "$common/syscalls/lua.ts";
|
import { luaSyscalls } from "$common/syscalls/lua.ts";
|
||||||
import { indexSyscalls } from "$common/syscalls/index.ts";
|
import { indexSyscalls } from "$common/syscalls/index.ts";
|
||||||
|
import { commandSyscalls } from "$common/syscalls/command.ts";
|
||||||
|
import { eventListenerSyscalls } from "$common/syscalls/event.ts";
|
||||||
|
|
||||||
const plugNameExtractRegex = /\/(.+)\.plug\.js$/;
|
const plugNameExtractRegex = /\/(.+)\.plug\.js$/;
|
||||||
|
|
||||||
|
@ -152,6 +154,7 @@ export class ClientSystem extends CommonSystem {
|
||||||
this.system.registerSyscalls(
|
this.system.registerSyscalls(
|
||||||
[],
|
[],
|
||||||
eventSyscalls(this.eventHook),
|
eventSyscalls(this.eventHook),
|
||||||
|
eventListenerSyscalls(this),
|
||||||
editorSyscalls(this.client),
|
editorSyscalls(this.client),
|
||||||
spaceReadSyscalls(this.client),
|
spaceReadSyscalls(this.client),
|
||||||
systemSyscalls(this.system, false, this, this.client, this.client),
|
systemSyscalls(this.system, false, this, this.client, this.client),
|
||||||
|
@ -163,7 +166,8 @@ export class ClientSystem extends CommonSystem {
|
||||||
clientCodeWidgetSyscalls(),
|
clientCodeWidgetSyscalls(),
|
||||||
languageSyscalls(),
|
languageSyscalls(),
|
||||||
jsonschemaSyscalls(),
|
jsonschemaSyscalls(),
|
||||||
indexSyscalls(this.system),
|
indexSyscalls(this),
|
||||||
|
commandSyscalls(this),
|
||||||
luaSyscalls(),
|
luaSyscalls(),
|
||||||
this.client.syncMode
|
this.client.syncMode
|
||||||
// In sync mode handle locally
|
// In sync mode handle locally
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
This describes the APIs available in [[Space Lua]]
|
This describes the APIs available in [[Space Lua]]
|
||||||
|
|
||||||
${template.each(query[[
|
${template.each(query[[
|
||||||
from tag("page") where string.startswith(name, "API/")
|
from index.tag("page") where string.startswith(name, "API/")
|
||||||
]], render.page)}
|
]], render.page)}
|
|
@ -0,0 +1,14 @@
|
||||||
|
APIs related to editor commands
|
||||||
|
|
||||||
|
### command.define(command_def)
|
||||||
|
Registers a command.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```lua
|
||||||
|
command.define {
|
||||||
|
name = "My custom command",
|
||||||
|
run = function()
|
||||||
|
editor.flash_notification "Triggered my custom command"
|
||||||
|
end
|
||||||
|
}
|
||||||
|
```
|
|
@ -4,13 +4,25 @@ The Event API provides functions for working with SilverBullet's event bus syste
|
||||||
|
|
||||||
## Event Operations
|
## Event Operations
|
||||||
|
|
||||||
### event.dispatch_event(event_name, data, timeout)
|
### event.listen(listener_def)
|
||||||
|
Register an event listener.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
event.listen {
|
||||||
|
name = "my-event",
|
||||||
|
run = function(e)
|
||||||
|
print("Data", e.data)
|
||||||
|
end
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### event.dispatch(event_name, data, timeout)
|
||||||
Triggers an event on the SilverBullet event bus. Event handlers can return values, which are accumulated and returned to the caller.
|
Triggers an event on the SilverBullet event bus. Event handlers can return values, which are accumulated and returned to the caller.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
```lua
|
```lua
|
||||||
-- Simple event dispatch
|
-- Simple event dispatch
|
||||||
event.dispatch_event("custom.event", {message = "Hello"})
|
event.dispatch("custom.event", {message = "Hello"})
|
||||||
|
|
||||||
-- Event dispatch with timeout and response handling
|
-- Event dispatch with timeout and response handling
|
||||||
local responses = event.dispatch_event("data.request", {id = 123}, 5000)
|
local responses = event.dispatch_event("data.request", {id = 123}, 5000)
|
||||||
|
|
|
@ -201,23 +201,3 @@ rawset(t, "foo", "bar") -- bypasses the metamethod
|
||||||
print(t.foo) -- prints: "bar"
|
print(t.foo) -- prints: "bar"
|
||||||
```
|
```
|
||||||
|
|
||||||
# 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]]}
|
|
||||||
|
|
||||||
## tpl(template)
|
|
||||||
Returns a template function that can be used to render a template. Conventionally, a template string is put between `[==[` and `]==]` as string delimiters.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```space-lua
|
|
||||||
examples = examples or {}
|
|
||||||
|
|
||||||
examples.say_hello = tpl[==[Hello ${name}!]==]
|
|
||||||
```
|
|
||||||
|
|
||||||
And its use: ${examples.say_hello {name="Pete"}}
|
|
||||||
|
|
|
@ -2,7 +2,14 @@ The `index` API provides functions for interacting with SilverBullet's [[Objects
|
||||||
|
|
||||||
## Object Operations
|
## Object Operations
|
||||||
|
|
||||||
### index.index_objects(page, objects)
|
## index.tag(name)
|
||||||
|
Returns a given [[Objects#Tags]] as a query collection, to be queried using [[Space Lua/Lua Integrated Query]].
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
${query[[from index.tag("page") limit 1]]}
|
||||||
|
|
||||||
|
## index.index_objects(page, objects)
|
||||||
Indexes an array of objects for a specific page.
|
Indexes an array of objects for a specific page.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
@ -14,7 +21,7 @@ local objects = {
|
||||||
index.index_objects("my page", objects)
|
index.index_objects("my page", objects)
|
||||||
```
|
```
|
||||||
|
|
||||||
### index.query_lua_objects(tag, query, scoped_variables?)
|
## index.query_lua_objects(tag, query, scoped_variables?)
|
||||||
Queries objects using a Lua-based collection query.
|
Queries objects using a Lua-based collection query.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
@ -22,7 +29,7 @@ Example:
|
||||||
local tasks = index.query_lua_objects("mytask", {limit=3})
|
local tasks = index.query_lua_objects("mytask", {limit=3})
|
||||||
```
|
```
|
||||||
|
|
||||||
### index.get_object_by_ref(page, tag, ref)
|
## index.get_object_by_ref(page, tag, ref)
|
||||||
Retrieves a specific object by its reference.
|
Retrieves a specific object by its reference.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
|
@ -1,10 +1,23 @@
|
||||||
Template functions that use the [[API/global#tpl(template)]] function.
|
Template functions that use the [[API/template#template.new(template)]] function.
|
||||||
|
|
||||||
|
## template.new(template)
|
||||||
|
Returns a template function that can be used to render a template. Conventionally, a template string is put between `[==[` and `]==]` as string delimiters.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```space-lua
|
||||||
|
examples = examples or {}
|
||||||
|
|
||||||
|
examples.say_hello = template.new[==[Hello ${name}!]==]
|
||||||
|
```
|
||||||
|
|
||||||
|
And its use: ${examples.say_hello {name="Pete"}}
|
||||||
|
|
||||||
## template.each(collection, template)
|
## template.each(collection, template)
|
||||||
Iterates over a collection and renders a template for each item.
|
Iterates over a collection and renders a template for each item.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
${template.each(query[[from tag "page" limit 3]], tpl[==[
|
${template.each(query[[from index.tag "page" limit 3]], template.new[==[
|
||||||
* ${name}
|
* ${name}
|
||||||
]==])}
|
]==])}
|
|
@ -1,8 +0,0 @@
|
||||||
Defines some core useful templates for use in [[Space Lua]]
|
|
||||||
```space-lua
|
|
||||||
render = render or {}
|
|
||||||
|
|
||||||
render.page = tpl[==[
|
|
||||||
* [[${name}]]
|
|
||||||
]==]
|
|
||||||
```
|
|
|
@ -60,7 +60,7 @@ For example: 10 + 2 = ${adder(10, 2)} (Alt-click, or select to see the expressio
|
||||||
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:
|
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[[
|
${query[[
|
||||||
from tag "page"
|
from index.tag "page"
|
||||||
order by lastModified desc
|
order by lastModified desc
|
||||||
select name
|
select name
|
||||||
limit 3
|
limit 3
|
||||||
|
@ -101,12 +101,12 @@ ${marquee "Finally, marqeeeeeeee!"}
|
||||||
Oh boy, the times we live in!
|
Oh boy, the times we live in!
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
Custom commands can be defined using `define_command`:
|
Custom commands can be defined using `command.define`:
|
||||||
|
|
||||||
```space-lua
|
```space-lua
|
||||||
define_command {
|
command.define {
|
||||||
name = "Hello World";
|
name = "Hello World",
|
||||||
function()
|
run = function()
|
||||||
editor.flash_notification "Hello world!"
|
editor.flash_notification "Hello world!"
|
||||||
event.dispatch("my-custom-event", {name="Pete"})
|
event.dispatch("my-custom-event", {name="Pete"})
|
||||||
end
|
end
|
||||||
|
@ -116,15 +116,14 @@ define_command {
|
||||||
Try it: {[Hello World]}
|
Try it: {[Hello World]}
|
||||||
|
|
||||||
## Event listeners
|
## Event listeners
|
||||||
You can listen to events using `define_event_listener`:
|
You can listen to events using `event.listen`:
|
||||||
|
|
||||||
```space-lua
|
```space-lua
|
||||||
define_event_listener {
|
event.listen {
|
||||||
event = "my-custom-event";
|
name = "my-custom-event";
|
||||||
function(e)
|
run = function(e)
|
||||||
editor.flash_notification("Custom triggered: "
|
editor.flash_notification("Custom triggered: "
|
||||||
.. e.data.name
|
.. e.data.name)
|
||||||
.. " on page " .. _CTX.pageMeta.name)
|
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -138,7 +137,6 @@ Space Lua currently introduces a few new features on top core Lua:
|
||||||
## Thread locals
|
## 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:
|
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
|
* `_CTX.GLOBAL` providing access to the global scope
|
||||||
|
|
||||||
# API
|
# API
|
||||||
|
|
|
@ -19,10 +19,10 @@ Unlike [[Query Language]] which operates on [[Objects]] only, LIQ can operate on
|
||||||
For instance, to sort a list of numbers in descending order:
|
For instance, to sort a list of numbers in descending order:
|
||||||
${query[[from n = {1, 2, 3} order by n desc]]}
|
${query[[from n = {1, 2, 3} order by n desc]]}
|
||||||
|
|
||||||
However, in most cases you’ll use it in conjunction with [[../API/global#tag(name)]]. Here’s an example querying the 3 pages that were last modified:
|
However, in most cases you’ll use it in conjunction with [[API/index#index.tag(name)]]. Here’s an example querying the 3 pages that were last modified:
|
||||||
|
|
||||||
${query[[
|
${query[[
|
||||||
from p = tag "page"
|
from p = index.tag "page"
|
||||||
order by p.lastModified desc
|
order by p.lastModified desc
|
||||||
select p.name
|
select p.name
|
||||||
limit 3
|
limit 3
|
||||||
|
@ -52,8 +52,8 @@ ${query[[from {1, 2, 3} select _]]}
|
||||||
With variable binding:
|
With variable binding:
|
||||||
${query[[from n = {1, 2, 3} select n]]}
|
${query[[from n = {1, 2, 3} select n]]}
|
||||||
|
|
||||||
A more realistic example using `tag`:
|
A more realistic example using `index.tag`:
|
||||||
${query[[from tag "page" order by lastModified select name limit 3]]}
|
${query[[from index.tag "page" order by lastModified select name limit 3]]}
|
||||||
|
|
||||||
## `where <expression>`
|
## `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.
|
The `where` clause allows you to filter data. When the expression evaluated to a truthy value, the item is included in the result.
|
||||||
|
@ -64,14 +64,14 @@ ${query[[from {1, 2, 3, 4, 5} where _ > 2]]}
|
||||||
|
|
||||||
Or to select all pages tagged with `#meta`:
|
Or to select all pages tagged with `#meta`:
|
||||||
|
|
||||||
${query[[from tag "page" where table.includes(tags, "meta")]]}
|
${query[[from index.tag "page" where table.includes(tags, "meta")]]}
|
||||||
|
|
||||||
## `order by <expression> [desc]`
|
## `order by <expression> [desc]`
|
||||||
The `order by` clause allows you to sort data, when `desc` is specified it reverts the sort order.
|
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:
|
As an example, the last 3 modified pages:
|
||||||
${query[[
|
${query[[
|
||||||
from tag "page"
|
from index.tag "page"
|
||||||
order by lastModified desc
|
order by lastModified desc
|
||||||
select name
|
select name
|
||||||
limit 3
|
limit 3
|
||||||
|
@ -97,11 +97,11 @@ Double each number:
|
||||||
${query[[from {1, 2, 3} select _ * 2]]}
|
${query[[from {1, 2, 3} select _ * 2]]}
|
||||||
|
|
||||||
Extract just the name from pages:
|
Extract just the name from pages:
|
||||||
${query[[from tag "page" select _.name limit 3]]}
|
${query[[from index.tag "page" select _.name limit 3]]}
|
||||||
|
|
||||||
You can also return tables or other complex values:
|
You can also return tables or other complex values:
|
||||||
${query[[
|
${query[[
|
||||||
from p = tag "page"
|
from p = index.tag "page"
|
||||||
select {
|
select {
|
||||||
name = p.name,
|
name = p.name,
|
||||||
modified = p.lastModified
|
modified = p.lastModified
|
||||||
|
|
Loading…
Reference in New Issue