@silverbulletmd/silverbullet 2.8.0 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,18 +5,22 @@
5
5
  [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/silverbulletmd/silverbullet)
6
6
 
7
7
  # SilverBullet
8
- SilverBullet is a Programmable, Private, Browser-based, Open Source, Self Hosted, Personal Knowledge Management Platform.
8
+ SilverBullet is a Programmable, Private, Browser-based, Open Source, Self Hosted, Personal Knowledge Database — a fancy term for a "notes app on steroids".
9
9
 
10
10
  _Yowza!_ That surely is a lot of adjectives to describe a browser-based Markdown editor programmable with Lua.
11
11
 
12
12
  Let’s get more specific.
13
13
 
14
+ SilverBullet combines a clean live-preview editor with wiki-style linking, a built-in database and query language, and a fully integrated Space Lua scripting environment, turning your notes into a programmable system that grows with you.
15
+
14
16
  In SilverBullet you keep your content as a collection of Markdown Pages (called a Space). You navigate your space using the Page Picker like a traditional notes app, or through Links like a wiki (except they are bi-directional).
15
17
 
16
- If you are the **writer** type, you’ll appreciate SilverBullet as a clean Markdown editor with Live Preview. If you have more of an **outliner** personality, SilverBullet has Outlining tools for you. Productivity freak? Have a look at Tasks. More of a **database** person? You will appreciate Objects and Queries.
18
+ If you are the **writer** type, you’ll appreciate SilverBullet as a clean Markdown editor with Live Preview. If you have more of an **outliner** personality, SilverBullet has Outlining tools for you. Productivity freak? Have a look at Tasks. More of a **database** person? You will appreciate Objects and Queries (SLIQ).
17
19
 
18
20
  And if you are comfortable **programming** a little bit — now we’re really talking. You will love _dynamically generating content_ with Space Lua (SilverBullet’s Lua dialect), or to use it to create custom Commands, Page Templates or Widgets.
19
21
 
22
+ You were told there’s no such thing as a silver bullet. You were told wrong.
23
+
20
24
  [Much more detail can be found on silverbullet.md](https://silverbullet.md)
21
25
 
22
26
  ## Installing SilverBullet
@@ -219,10 +219,12 @@ export function editorSyscalls(client: Client): SysCallMapping {
219
219
  );
220
220
  } else if (
221
221
  ref.details &&
222
- !["position", "linecolumn", "header"].includes(ref.details.type)
222
+ !["position", "linecolumn", "header", "anchor"].includes(
223
+ ref.details.type,
224
+ )
223
225
  ) {
224
226
  throw new Error(
225
- "ref.details.type has to be 'position', 'linecolumn' or 'header'",
227
+ "ref.details.type has to be 'position', 'linecolumn', 'header' or 'anchor'",
226
228
  );
227
229
  }
228
230
 
@@ -244,6 +246,11 @@ export function editorSyscalls(client: Client): SysCallMapping {
244
246
  throw new Error(
245
247
  "ref.details.line and ref.details.column has to be of type `number`",
246
248
  );
249
+ } else if (
250
+ ref.details?.type === "anchor" &&
251
+ typeof ref.details.name !== "string"
252
+ ) {
253
+ throw new Error("ref.details.name has to be of type `string`");
247
254
  }
248
255
 
249
256
  await client.navigate(ref, replaceState, newWindow);
@@ -464,20 +471,14 @@ export function editorSyscalls(client: Client): SysCallMapping {
464
471
  }
465
472
  },
466
473
  "editor.moveCursor": (_ctx, pos: number, center = false) => {
474
+ // Always scroll into view so callers don't strand the cursor
475
+ // off-screen; `nearest` (default) is a no-op when already visible.
467
476
  client.editorView.dispatch({
468
- selection: {
469
- anchor: pos,
470
- },
477
+ selection: { anchor: pos },
478
+ effects: [
479
+ EditorView.scrollIntoView(pos, center ? { y: "center" } : {}),
480
+ ],
471
481
  });
472
- if (center) {
473
- client.editorView.dispatch({
474
- effects: [
475
- EditorView.scrollIntoView(pos, {
476
- y: "center",
477
- }),
478
- ],
479
- });
480
- }
481
482
  client.editorView.focus();
482
483
  },
483
484
  "editor.moveCursorToLine": (
@@ -541,8 +542,12 @@ export function editorSyscalls(client: Client): SysCallMapping {
541
542
  ): Promise<string | undefined> => {
542
543
  return client.ui.prompt(message, defaultValue);
543
544
  },
544
- "editor.confirm": (_ctx, message: string): Promise<boolean> => {
545
- return client.ui.confirm(message);
545
+ "editor.confirm": (
546
+ _ctx,
547
+ message: string,
548
+ options?: { destructive?: boolean },
549
+ ): Promise<boolean> => {
550
+ return client.ui.confirm(message, options);
546
551
  },
547
552
  "editor.alert": (_ctx, message: string) => {
548
553
  alert(message);
@@ -17,15 +17,57 @@ export function indexSyscalls(
17
17
  client: Client,
18
18
  ): SysCallMapping {
19
19
  return {
20
+ // Query collection API
20
21
  "index.tag": (_ctx, tagName: string): LuaQueryCollection => {
21
- return objectIndex.tag(tagName);
22
+ return objectIndex.objectsWithTag(tagName);
22
23
  },
23
- "index.contentPages": (): LuaQueryCollection => {
24
- return objectIndex.contentPages();
24
+ // Alias for tag
25
+ "index.objects": (_ctx, tagName: string): LuaQueryCollection => {
26
+ return objectIndex.objectsWithTag(tagName);
27
+ },
28
+ "index.pages": (_ctx, tagName?: string): LuaQueryCollection => {
29
+ return objectIndex.rootTaggedObjects("page", tagName);
30
+ },
31
+ "index.subPages": (_ctx, pageName: string): LuaQueryCollection => {
32
+ return objectIndex.subPages(pageName);
33
+ },
34
+ "index.documents": (): LuaQueryCollection => {
35
+ return objectIndex.objectsWithTag("document");
36
+ },
37
+ "index.links": (): LuaQueryCollection => {
38
+ return objectIndex.objectsWithTag("link");
39
+ },
40
+ "index.relations": (): LuaQueryCollection => {
41
+ return objectIndex.objectsWithTag("relation");
42
+ },
43
+ "index.contentPages": (_ctx, tagName?: string): LuaQueryCollection => {
44
+ return objectIndex.contentPages(tagName);
25
45
  },
26
46
  "index.metaPages": (): LuaQueryCollection => {
27
47
  return objectIndex.metaPages();
28
48
  },
49
+ "index.tasks": (_ctx, tagName?: string): LuaQueryCollection => {
50
+ return objectIndex.rootTaggedObjects("task", tagName);
51
+ },
52
+ "index.headers": (_ctx, tagName?: string): LuaQueryCollection => {
53
+ return objectIndex.rootTaggedObjects("header", tagName);
54
+ },
55
+ "index.items": (_ctx, tagName?: string): LuaQueryCollection => {
56
+ return objectIndex.rootTaggedObjects("item", tagName);
57
+ },
58
+ "index.paragraphs": (_ctx, tagName?: string): LuaQueryCollection => {
59
+ return objectIndex.rootTaggedObjects("paragraph", tagName);
60
+ },
61
+ "index.tables": (_ctx, tagName?: string): LuaQueryCollection => {
62
+ return objectIndex.rootTaggedObjects("table", tagName);
63
+ },
64
+ "index.tags": (): LuaQueryCollection => {
65
+ return objectIndex.objectsWithTag("tag");
66
+ },
67
+ "index.aspiringPages": (): LuaQueryCollection => {
68
+ return objectIndex.aspiringPages();
69
+ },
70
+ // Internals
29
71
  "index.aggregates": (): LuaQueryCollection => {
30
72
  return objectIndex.aggregates();
31
73
  },
@@ -1,6 +1,11 @@
1
1
  import type { SysCallMapping } from "../system.ts";
2
2
  import { evalExpression } from "../../space_lua/eval.ts";
3
- import { parse, parseExpressionString } from "../../space_lua/parse.ts";
3
+ import { parseBlock, parseExpressionString } from "../../space_lua/parse.ts";
4
+ import {
5
+ type PrintOptions,
6
+ prettyPrintBlock,
7
+ prettyPrintExpression,
8
+ } from "../../space_lua/pretty_print.ts";
4
9
  import {
5
10
  LuaStackFrame,
6
11
  luaToString,
@@ -13,12 +18,30 @@ import type { ClientSystem } from "../../client_system.ts";
13
18
 
14
19
  export function luaSyscalls(clientSystem: ClientSystem): SysCallMapping {
15
20
  return {
21
+ "lua.parseBlock": (_ctx, code: string): LuaBlock => {
22
+ return parseBlock(code);
23
+ },
24
+ // Deprecated alias for `lua.parseBlock`, kept for backwards compatibility.
16
25
  "lua.parse": (_ctx, code: string): LuaBlock => {
17
- return parse(code);
26
+ return parseBlock(code);
18
27
  },
19
28
  "lua.parseExpression": (_ctx, expression: string): LuaExpression => {
20
29
  return parseExpressionString(expression);
21
30
  },
31
+ "lua.prettyPrintBlock": (
32
+ _ctx,
33
+ block: LuaBlock,
34
+ opts?: PrintOptions,
35
+ ): string => {
36
+ return prettyPrintBlock(block, opts);
37
+ },
38
+ "lua.prettyPrintExpression": (
39
+ _ctx,
40
+ expression: LuaExpression,
41
+ opts?: PrintOptions,
42
+ ): string => {
43
+ return prettyPrintExpression(expression, opts);
44
+ },
22
45
  /**
23
46
  * Evaluates a Lua expression and returns the result as a JavaScript value
24
47
  * @param _ctx
@@ -67,7 +67,7 @@ function parseChunk(t: ParseTree, ctx: ASTCtx): LuaBlock {
67
67
  if (t.type !== "Chunk") {
68
68
  throw new Error(`Expected Chunk, got ${t.type}`);
69
69
  }
70
- return parseBlock(t.children![0], ctx);
70
+ return parseBlockNode(t.children![0], ctx);
71
71
  }
72
72
 
73
73
  function hasCloseLocal(names: LuaAttName[] | undefined): boolean {
@@ -636,7 +636,7 @@ function exprCapturesNames(e: LuaExpression, names: Set<string>): boolean {
636
636
  }
637
637
  }
638
638
 
639
- function parseBlock(t: ParseTree, ctx: ASTCtx): LuaBlock {
639
+ function parseBlockNode(t: ParseTree, ctx: ASTCtx): LuaBlock {
640
640
  if (t.type !== "Block") {
641
641
  throw new Error(`Expected Block, got ${t.type}`);
642
642
  }
@@ -846,20 +846,20 @@ function parseStatement(t: ParseTree, ctx: ASTCtx): LuaStatement {
846
846
  ctx: context(t, ctx),
847
847
  };
848
848
  case "Scope":
849
- return parseBlock(t.children![1], ctx);
849
+ return parseBlockNode(t.children![1], ctx);
850
850
  case ";":
851
851
  return { type: "Semicolon", ctx: context(t, ctx) };
852
852
  case "WhileStatement":
853
853
  return {
854
854
  type: "While",
855
855
  condition: parseExpression(t.children![1], ctx),
856
- block: parseBlock(t.children![3], ctx),
856
+ block: parseBlockNode(t.children![3], ctx),
857
857
  ctx: context(t, ctx),
858
858
  };
859
859
  case "RepeatStatement":
860
860
  return {
861
861
  type: "Repeat",
862
- block: parseBlock(t.children![1], ctx),
862
+ block: parseBlockNode(t.children![1], ctx),
863
863
  condition: parseExpression(t.children![3], ctx),
864
864
  ctx: context(t, ctx),
865
865
  };
@@ -880,12 +880,12 @@ function parseStatement(t: ParseTree, ctx: ASTCtx): LuaStatement {
880
880
  if (token === "if" || token === "elseif") {
881
881
  conditions.push({
882
882
  condition: parseExpression(t.children![i + 1], ctx),
883
- block: parseBlock(t.children![i + 3], ctx),
883
+ block: parseBlockNode(t.children![i + 3], ctx),
884
884
  from: child.from,
885
885
  to: child.to,
886
886
  });
887
887
  } else if (token === "else") {
888
- elseBlock = parseBlock(t.children![i + 1], ctx);
888
+ elseBlock = parseBlockNode(t.children![i + 1], ctx);
889
889
  } else if (token === "end") {
890
890
  break;
891
891
  } else {
@@ -903,7 +903,7 @@ function parseStatement(t: ParseTree, ctx: ASTCtx): LuaStatement {
903
903
  if (t.children![1].type === "ForNumeric") {
904
904
  const forNumeric = t.children![1];
905
905
  const name = forNumeric.children![0].children![0].text!;
906
- const block = parseBlock(t.children![3], ctx);
906
+ const block = parseBlockNode(t.children![3], ctx);
907
907
  const node: LuaStatement = {
908
908
  type: "For",
909
909
  name,
@@ -923,7 +923,7 @@ function parseStatement(t: ParseTree, ctx: ASTCtx): LuaStatement {
923
923
  }
924
924
  const forGeneric = t.children![1];
925
925
  const names = parseNameList(forGeneric.children![0]);
926
- const block = parseBlock(t.children![3], ctx);
926
+ const block = parseBlockNode(t.children![3], ctx);
927
927
  const node: LuaStatement = {
928
928
  type: "ForIn",
929
929
  names,
@@ -1512,7 +1512,7 @@ function parseFunctionBody(t: ParseTree, ctx: ASTCtx): LuaFunctionBody {
1512
1512
  (c) => c.type && ["Name", "Ellipsis"].includes(c.type),
1513
1513
  )
1514
1514
  .map((c) => c.children![0].text!),
1515
- block: parseBlock(t.children![3], ctx),
1515
+ block: parseBlockNode(t.children![3], ctx),
1516
1516
  ctx: context(t, ctx),
1517
1517
  };
1518
1518
  }
@@ -1686,7 +1686,7 @@ export function stripLuaComments(s: string): string {
1686
1686
  return result;
1687
1687
  }
1688
1688
 
1689
- export function parse(s: string, ctx: ASTCtx = {}): LuaBlock {
1689
+ export function parseBlock(s: string, ctx: ASTCtx = {}): LuaBlock {
1690
1690
  try {
1691
1691
  const t = parseToAST(stripLuaComments(s));
1692
1692
  // console.log("Clean tree", JSON.stringify(t, null, 2));
@@ -1743,6 +1743,6 @@ function luaUnexpectedSymbolMessage(src: string, from: number): string {
1743
1743
  * Helper function to parse a Lua expression string
1744
1744
  */
1745
1745
  export function parseExpressionString(expr: string): LuaExpression {
1746
- const parsedLua = parse(`_(${expr})`) as LuaBlock;
1746
+ const parsedLua = parseBlock(`_(${expr})`) as LuaBlock;
1747
1747
  return (parsedLua.statements[0] as LuaFunctionCallStatement).call.args[0];
1748
1748
  }
@@ -0,0 +1,391 @@
1
+ import type {
2
+ LuaBlock,
3
+ LuaExpression,
4
+ LuaOrderBy,
5
+ LuaQueryClause,
6
+ LuaStatement,
7
+ LuaTableField,
8
+ } from "./ast.ts";
9
+ import { luaFormatNumber } from "./runtime.ts";
10
+
11
+ export type PrintOptions = {
12
+ indentWidth?: number;
13
+ quote?: "double" | "single";
14
+ trailingComma?: boolean;
15
+ };
16
+
17
+ type ResolvedOptions = {
18
+ indentWidth: number;
19
+ quote: "double" | "single";
20
+ trailingComma: boolean;
21
+ };
22
+
23
+ function resolveOptions(opts?: PrintOptions): ResolvedOptions {
24
+ return {
25
+ indentWidth: opts?.indentWidth ?? 2,
26
+ quote: opts?.quote ?? "double",
27
+ trailingComma: opts?.trailingComma ?? true,
28
+ };
29
+ }
30
+
31
+ const RESERVED = new Set([
32
+ "and", "break", "do", "else", "elseif", "end", "false", "for", "function",
33
+ "goto", "if", "in", "local", "nil", "not", "or", "repeat", "return", "then",
34
+ "true", "until", "while",
35
+ ]);
36
+
37
+ function isLuaIdentifier(s: string): boolean {
38
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(s) && !RESERVED.has(s);
39
+ }
40
+
41
+ const BINARY_PREC: Record<string, number> = {
42
+ "or": 1,
43
+ "and": 2,
44
+ "<": 3, ">": 3, "<=": 3, ">=": 3, "~=": 3, "==": 3,
45
+ "|": 4,
46
+ "~": 5, // binary bitwise xor
47
+ "&": 6,
48
+ "<<": 7, ">>": 7,
49
+ "..": 8,
50
+ "+": 9, "-": 9,
51
+ "*": 10, "/": 10, "//": 10, "%": 10,
52
+ "^": 12,
53
+ };
54
+ const UNARY_PREC = 11;
55
+ const RIGHT_ASSOC = new Set(["..", "^"]);
56
+ const ATOM_PREC = 99;
57
+
58
+ function exprPrec(e: LuaExpression): number {
59
+ if (e.type === "Binary") return BINARY_PREC[e.operator] ?? ATOM_PREC;
60
+ if (e.type === "Unary") return UNARY_PREC;
61
+ return ATOM_PREC;
62
+ }
63
+
64
+ class Printer {
65
+ constructor(private opts: ResolvedOptions) { }
66
+
67
+ private indent(depth: number): string {
68
+ return " ".repeat(depth * this.opts.indentWidth);
69
+ }
70
+
71
+ private quoteString(s: string): string {
72
+ const q = this.opts.quote === "double" ? '"' : "'";
73
+ let out = q;
74
+ for (const ch of s) {
75
+ switch (ch) {
76
+ case "\\":
77
+ out += "\\\\";
78
+ break;
79
+ case "\n":
80
+ out += "\\n";
81
+ break;
82
+ case "\r":
83
+ out += "\\r";
84
+ break;
85
+ case "\t":
86
+ out += "\\t";
87
+ break;
88
+ case "\0":
89
+ out += "\\0";
90
+ break;
91
+ default:
92
+ out += ch === q ? `\\${ch}` : ch;
93
+ }
94
+ }
95
+ return out + q;
96
+ }
97
+
98
+ expression(e: LuaExpression, depth = 0): string {
99
+ switch (e.type) {
100
+ case "Nil":
101
+ return "nil";
102
+ case "Boolean":
103
+ return e.value ? "true" : "false";
104
+ case "Number":
105
+ return luaFormatNumber(e.value, e.numericType);
106
+ case "String":
107
+ return this.quoteString(e.value);
108
+ case "Variable":
109
+ return e.name;
110
+ case "PropertyAccess":
111
+ return `${this.expression(e.object, depth)}.${e.property}`;
112
+ case "TableAccess":
113
+ return `${this.expression(e.object, depth)}[${this.expression(e.key, depth)}]`;
114
+ case "Parenthesized":
115
+ return `(${this.expression(e.expression, depth)})`;
116
+ case "Binary": {
117
+ const p = BINARY_PREC[e.operator] ?? ATOM_PREC;
118
+ const rightAssoc = RIGHT_ASSOC.has(e.operator);
119
+ const left = this.operand(e.left, p, false, rightAssoc, depth);
120
+ const right = this.operand(e.right, p, true, rightAssoc, depth);
121
+ return `${left} ${e.operator} ${right}`;
122
+ }
123
+ case "Unary": {
124
+ const arg = this.unaryOperand(e.argument, depth);
125
+ return /^[a-z]+$/.test(e.operator)
126
+ ? `${e.operator} ${arg}`
127
+ : `${e.operator}${arg}`;
128
+ }
129
+ case "TableConstructor":
130
+ return this.tableConstructor(e.fields, depth);
131
+ case "FunctionDefinition":
132
+ return "function" +
133
+ this.functionRest(e.body.parameters, e.body.block, depth);
134
+ case "FunctionCall":
135
+ return this.functionCall(e, depth);
136
+ case "Query": {
137
+ const lines = e.clauses.map((c) =>
138
+ this.indent(depth + 1) + this.queryClause(c, depth + 1)
139
+ );
140
+ return `query[[\n${lines.join("\n")}\n${this.indent(depth)}]]`;
141
+ }
142
+ case "FilteredCall":
143
+ return `${this.expression(e.call, depth)} filter ${this.expression(e.filter, depth)
144
+ }`;
145
+ case "AggregateCall":
146
+ return `${this.expression(e.call, depth)} order by ${this.orderByList(e.orderBy, depth)
147
+ }`;
148
+ default:
149
+ throw new Error(
150
+ `pretty_print: unsupported expression type: ${(e as any).type}`,
151
+ );
152
+ }
153
+ }
154
+
155
+ private operand(
156
+ e: LuaExpression,
157
+ parentPrec: number,
158
+ isRight: boolean,
159
+ parentRightAssoc: boolean,
160
+ depth: number,
161
+ ): string {
162
+ const cp = exprPrec(e);
163
+ let parens = false;
164
+ if (cp < parentPrec) {
165
+ parens = true;
166
+ } else if (cp === parentPrec) {
167
+ // left-assoc parent: the right child of equal precedence needs parens.
168
+ // right-assoc parent: the left child of equal precedence needs parens.
169
+ parens = parentRightAssoc ? !isRight : isRight;
170
+ }
171
+ const s = this.expression(e, depth);
172
+ return parens ? `(${s})` : s;
173
+ }
174
+
175
+ private unaryOperand(e: LuaExpression, depth: number): string {
176
+ const s = this.expression(e, depth);
177
+ return exprPrec(e) < UNARY_PREC ? `(${s})` : s;
178
+ }
179
+
180
+ private field(f: LuaTableField, depth: number): string {
181
+ switch (f.type) {
182
+ case "PropField":
183
+ return isLuaIdentifier(f.key)
184
+ ? `${f.key} = ${this.expression(f.value, depth)}`
185
+ : `[${this.quoteString(f.key)}] = ${this.expression(f.value, depth)}`;
186
+ case "DynamicField":
187
+ return `[${this.expression(f.key, depth)}] = ${this.expression(f.value, depth)}`;
188
+ case "ExpressionField":
189
+ return this.expression(f.value, depth);
190
+ }
191
+ }
192
+
193
+ private tableConstructor(fields: LuaTableField[], depth: number): string {
194
+ if (fields.length === 0) return "{}";
195
+ if (fields.length === 1) return `{${this.field(fields[0], depth)}}`;
196
+ const lines = fields.map((f) =>
197
+ this.indent(depth + 1) + this.field(f, depth + 1)
198
+ );
199
+ const tail = this.opts.trailingComma ? "," : "";
200
+ return `{\n${lines.join(",\n")}${tail}\n${this.indent(depth)}}`;
201
+ }
202
+
203
+ // returns the part of a function after the `function` keyword (+ optional
204
+ // name): "(params) end" or "(params)\n <body>\nend"
205
+ private functionRest(
206
+ params: string[],
207
+ body: LuaBlock,
208
+ depth: number,
209
+ ): string {
210
+ const head = `(${params.join(", ")})`;
211
+ const rendered = this.block(body, depth + 1);
212
+ if (!rendered) return `${head} end`;
213
+ return `${head}\n${rendered}\n${this.indent(depth)}end`;
214
+ }
215
+
216
+ private functionCall(
217
+ e: Extract<LuaExpression, { type: "FunctionCall" }>,
218
+ depth: number,
219
+ ): string {
220
+ let base = this.expression(e.prefix, depth);
221
+ if (e.name) base += `:${e.name}`;
222
+ const args = e.args;
223
+ const orderBy = e.orderBy
224
+ ? ` order by ${this.orderByList(e.orderBy, depth)}`
225
+ : "";
226
+ if (
227
+ !e.orderBy && args.length === 1 &&
228
+ (args[0].type === "TableConstructor" || args[0].type === "String")
229
+ ) {
230
+ return `${base} ${this.expression(args[0], depth)}`;
231
+ }
232
+ return `${base}(${args.map((a) => this.expression(a, depth)).join(", ")})${orderBy}`;
233
+ }
234
+
235
+ private fieldList(fields: LuaTableField[], depth: number): string {
236
+ return fields.map((f) => this.field(f, depth)).join(", ");
237
+ }
238
+
239
+ private orderByList(orders: LuaOrderBy[], depth: number): string {
240
+ return orders
241
+ .map((o) => {
242
+ let s = this.expression(o.expression, depth);
243
+ if (o.direction === "desc") s += " desc";
244
+ if (o.nulls) s += ` nulls ${o.nulls}`;
245
+ return s;
246
+ })
247
+ .join(", ");
248
+ }
249
+
250
+ private queryClause(c: LuaQueryClause, depth: number): string {
251
+ switch (c.type) {
252
+ case "From":
253
+ return `from ${this.fieldList(c.fields, depth)}`;
254
+ case "Where":
255
+ return `where ${this.expression(c.expression, depth)}`;
256
+ case "Limit":
257
+ return `limit ${this.expression(c.limit, depth)}` +
258
+ (c.offset ? ` offset ${this.expression(c.offset, depth)}` : "");
259
+ case "Offset":
260
+ return `offset ${this.expression(c.offset, depth)}`;
261
+ case "OrderBy":
262
+ return `order by ${this.orderByList(c.orderBy, depth)}`;
263
+ case "Select":
264
+ return `select ${this.fieldList(c.fields, depth)}`;
265
+ case "GroupBy":
266
+ return `group by ${this.fieldList(c.fields, depth)}`;
267
+ case "Having":
268
+ return `having ${this.expression(c.expression, depth)}`;
269
+ }
270
+ }
271
+
272
+ block(b: LuaBlock, depth: number): string {
273
+ const stmts = b.statements.filter((s) => s.type !== "Semicolon");
274
+ const isDef = (s: LuaStatement) =>
275
+ s.type === "Function" || s.type === "LocalFunction";
276
+ let out = "";
277
+ for (let i = 0; i < stmts.length; i++) {
278
+ if (i > 0) {
279
+ out += "\n";
280
+ // blank line separating function definitions from neighbours
281
+ if (isDef(stmts[i - 1]) || isDef(stmts[i])) out += "\n";
282
+ }
283
+ out += this.statement(stmts[i], depth);
284
+ }
285
+ return out;
286
+ }
287
+
288
+ private statement(s: LuaStatement, depth: number): string {
289
+ const ind = this.indent(depth);
290
+ switch (s.type) {
291
+ case "Local": {
292
+ const names = s.names
293
+ .map((n) =>
294
+ n.name +
295
+ (n.attribute
296
+ ? ` <${n.attribute}>`
297
+ : (n.attributes?.length ? ` <${n.attributes[0]}>` : ""))
298
+ )
299
+ .join(", ");
300
+ const exprs = s.expressions?.length
301
+ ? ` = ${s.expressions.map((e) => this.expression(e, depth)).join(", ")}`
302
+ : "";
303
+ return `${ind}local ${names}${exprs}`;
304
+ }
305
+ case "Assignment":
306
+ return `${ind}${s.variables.map((v) => this.expression(v, depth)).join(", ")
307
+ } = ${s.expressions.map((e) => this.expression(e, depth)).join(", ")}`;
308
+ case "Return":
309
+ return s.expressions.length
310
+ ? `${ind}return ${s.expressions.map((e) => this.expression(e, depth)).join(", ")
311
+ }`
312
+ : `${ind}return`;
313
+ case "FunctionCallStatement":
314
+ return ind + this.expression(s.call, depth);
315
+ case "Break":
316
+ return `${ind}break`;
317
+ case "Goto":
318
+ return `${ind}goto ${s.name}`;
319
+ case "Label":
320
+ return `${ind}::${s.name}::`;
321
+ case "Block": {
322
+ const body = this.block(s, depth + 1);
323
+ return body ? `${ind}do\n${body}\n${ind}end` : `${ind}do end`;
324
+ }
325
+ case "If": {
326
+ let out = "";
327
+ s.conditions.forEach((c, i) => {
328
+ const kw = i === 0 ? "if" : "elseif";
329
+ const head = i === 0 ? `${ind}${kw} ` : `\n${ind}${kw} `;
330
+ const body = this.block(c.block, depth + 1);
331
+ out += `${head}${this.expression(c.condition, depth)} then` +
332
+ (body ? `\n${body}` : "");
333
+ });
334
+ if (s.elseBlock) {
335
+ const body = this.block(s.elseBlock, depth + 1);
336
+ out += `\n${ind}else${body ? `\n${body}` : ""}`;
337
+ }
338
+ out += `\n${ind}end`;
339
+ return out;
340
+ }
341
+ case "While": {
342
+ const body = this.block(s.block, depth + 1);
343
+ return `${ind}while ${this.expression(s.condition, depth)} do` +
344
+ (body ? `\n${body}` : "") + `\n${ind}end`;
345
+ }
346
+ case "Repeat": {
347
+ const body = this.block(s.block, depth + 1);
348
+ return `${ind}repeat` + (body ? `\n${body}` : "") +
349
+ `\n${ind}until ${this.expression(s.condition, depth)}`;
350
+ }
351
+ case "For": {
352
+ const step = s.step ? `, ${this.expression(s.step, depth)}` : "";
353
+ const body = this.block(s.block, depth + 1);
354
+ return `${ind}for ${s.name} = ${this.expression(s.start, depth)}, ${this.expression(s.end, depth)
355
+ }${step} do${body ? `\n${body}` : ""}\n${ind}end`;
356
+ }
357
+ case "ForIn": {
358
+ const body = this.block(s.block, depth + 1);
359
+ return `${ind}for ${s.names.join(", ")} in ${s.expressions.map((e) => this.expression(e, depth)).join(", ")
360
+ } do${body ? `\n${body}` : ""}\n${ind}end`;
361
+ }
362
+ case "Function": {
363
+ const name = s.name.propNames.join(".") +
364
+ (s.name.colonName ? `:${s.name.colonName}` : "");
365
+ return `${ind}function ${name}` +
366
+ this.functionRest(s.body.parameters, s.body.block, depth);
367
+ }
368
+ case "LocalFunction":
369
+ return `${ind}local function ${s.name}` +
370
+ this.functionRest(s.body.parameters, s.body.block, depth);
371
+ default:
372
+ throw new Error(
373
+ `pretty_print: unsupported statement type: ${(s as any).type}`,
374
+ );
375
+ }
376
+ }
377
+ }
378
+
379
+ export function prettyPrintExpression(
380
+ expr: LuaExpression,
381
+ opts?: PrintOptions,
382
+ ): string {
383
+ return new Printer(resolveOptions(opts)).expression(expr, 0);
384
+ }
385
+
386
+ export function prettyPrintBlock(
387
+ block: LuaBlock,
388
+ opts?: PrintOptions,
389
+ ): string {
390
+ return new Printer(resolveOptions(opts)).block(block, 0);
391
+ }
@@ -193,7 +193,7 @@ export interface LuaQueryCollection {
193
193
  * Implements a query collection for a regular JavaScript array
194
194
  */
195
195
  export class ArrayQueryCollection<T> implements LuaQueryCollection {
196
- constructor(private readonly array: T[]) {}
196
+ constructor(private readonly array: T[]) { }
197
197
  query(
198
198
  query: LuaCollectionQuery,
199
199
  env: LuaEnv,
@@ -842,7 +842,7 @@ export async function applyQuery(
842
842
 
843
843
  const mkEnv = grouped
844
844
  ? (ov: string | undefined, item: any, e: LuaEnv, s: LuaStackFrame) =>
845
- buildGroupItemEnv(ov, groupByNames, item, e, s)
845
+ buildGroupItemEnv(ov, groupByNames, item, e, s)
846
846
  : buildItemEnvLocal;
847
847
 
848
848
  let selectResults: any[] | undefined;
@@ -915,9 +915,7 @@ export async function applyQuery(
915
915
  for (let i = 0; i < violated.length; i++) {
916
916
  if (violated[i]) {
917
917
  throw new LuaRuntimeError(
918
- `order by #${
919
- i + 1
920
- }: 'using' comparator violates strict weak ordering`,
918
+ `order by #${i + 1}: 'using' comparator violates strict weak ordering`,
921
919
  sf,
922
920
  );
923
921
  }
@@ -1004,6 +1002,8 @@ export async function queryLua<T = any>(
1004
1002
  for await (let { key, value } of kv.query({ prefix })) {
1005
1003
  if (enricher) {
1006
1004
  value = enricher(key, value);
1005
+ // Enrichers may return undefined to filter a row out
1006
+ if (value === undefined) continue;
1007
1007
  }
1008
1008
  results.push(value);
1009
1009
  }
@@ -1056,7 +1056,7 @@ export class DataStoreQueryCollection implements LuaQueryCollection {
1056
1056
  constructor(
1057
1057
  private readonly dataStore: DataStore,
1058
1058
  readonly prefix: string[],
1059
- ) {}
1059
+ ) { }
1060
1060
  query(
1061
1061
  query: LuaCollectionQuery,
1062
1062
  env: LuaEnv,
@@ -5,7 +5,7 @@ import {
5
5
  type LuaStackFrame,
6
6
  type LuaValue,
7
7
  } from "../runtime.ts";
8
- import { parse } from "../parse.ts";
8
+ import { parseBlock } from "../parse.ts";
9
9
  import { evalStatement } from "../eval.ts";
10
10
 
11
11
  // Returns a function (callable chunk) or (nil, "error message") pair.
@@ -13,7 +13,7 @@ export function luaLoad(code: LuaValue, sf: LuaStackFrame): LuaValue {
13
13
  const s = typeof code === "string" ? code : String(code);
14
14
 
15
15
  try {
16
- const block = parse(s, sf.astCtx || {});
16
+ const block = parseBlock(s, sf.astCtx || {});
17
17
  const globalEnvMaybe = sf.threadLocal.get("_GLOBAL");
18
18
 
19
19
  // Be vocal when no _GLOBAL is set
@@ -1,6 +1,11 @@
1
- import { parseExpressionString } from "../parse.ts";
2
- import type { LuaExpression } from "../ast.ts";
1
+ import { parseBlock, parseExpressionString } from "../parse.ts";
2
+ import type { LuaBlock, LuaExpression } from "../ast.ts";
3
3
  import { evalExpression } from "../eval.ts";
4
+ import {
5
+ type PrintOptions,
6
+ prettyPrintBlock,
7
+ prettyPrintExpression,
8
+ } from "../pretty_print.ts";
4
9
  import {
5
10
  jsToLuaValue,
6
11
  LuaBuiltinFunction,
@@ -101,6 +106,25 @@ export async function interpolateLuaString(
101
106
  return result;
102
107
  }
103
108
 
109
+ /**
110
+ * Converts an optional Lua options table into a `PrintOptions` object,
111
+ * keeping only the recognised keys with the expected types.
112
+ */
113
+ function toPrintOptions(
114
+ sf: LuaStackFrame,
115
+ opts?: LuaTable,
116
+ ): PrintOptions | undefined {
117
+ if (!opts) return undefined;
118
+ const js = luaValueToJS(opts, sf) as Record<string, unknown>;
119
+ const result: PrintOptions = {};
120
+ if (typeof js.indentWidth === "number") result.indentWidth = js.indentWidth;
121
+ if (js.quote === "double" || js.quote === "single") result.quote = js.quote;
122
+ if (typeof js.trailingComma === "boolean") {
123
+ result.trailingComma = js.trailingComma;
124
+ }
125
+ return result;
126
+ }
127
+
104
128
  export const spaceluaApi = new LuaTable({
105
129
  /**
106
130
  * Parses a lua expression and returns the parsed expression.
@@ -112,6 +136,42 @@ export const spaceluaApi = new LuaTable({
112
136
  parseExpression: new LuaBuiltinFunction((_sf, luaExpression: string) => {
113
137
  return parseExpressionString(luaExpression);
114
138
  }),
139
+ /**
140
+ * Parses a lua chunk (block) and returns the parsed AST block.
141
+ *
142
+ * @param sf - The current space_lua state.
143
+ * @param code - The lua code to parse.
144
+ * @returns The parsed block.
145
+ */
146
+ parseBlock: new LuaBuiltinFunction((_sf, code: string): LuaBlock => {
147
+ return parseBlock(code);
148
+ }),
149
+ /**
150
+ * Pretty-prints a parsed lua block AST back to formatted source.
151
+ *
152
+ * @param sf - The current space_lua state.
153
+ * @param block - The parsed lua block.
154
+ * @param opts - Optional formatting options.
155
+ * @returns The formatted lua source.
156
+ */
157
+ prettyPrintBlock: new LuaBuiltinFunction(
158
+ (sf, block: LuaBlock, opts?: LuaTable): string => {
159
+ return prettyPrintBlock(block, toPrintOptions(sf, opts));
160
+ },
161
+ ),
162
+ /**
163
+ * Pretty-prints a parsed lua expression AST back to formatted source.
164
+ *
165
+ * @param sf - The current space_lua state.
166
+ * @param expr - The parsed lua expression.
167
+ * @param opts - Optional formatting options.
168
+ * @returns The formatted lua source.
169
+ */
170
+ prettyPrintExpression: new LuaBuiltinFunction(
171
+ (sf, expr: LuaExpression, opts?: LuaTable): string => {
172
+ return prettyPrintExpression(expr, toPrintOptions(sf, opts));
173
+ },
174
+ ),
115
175
  /**
116
176
  * Evaluates a parsed lua expression and returns the result.
117
177
  *
@@ -26,7 +26,7 @@ import { osApi } from "./stdlib/os.ts";
26
26
  import { jsApi } from "./stdlib/js.ts";
27
27
  import { spaceluaApi } from "./stdlib/space_lua.ts";
28
28
  import { mathApi } from "./stdlib/math.ts";
29
- import { parse } from "./parse.ts";
29
+ import { parseBlock } from "./parse.ts";
30
30
  import { evalStatement } from "./eval.ts";
31
31
  import { encodingApi } from "./stdlib/encoding.ts";
32
32
  import { luaToNumberDetailed } from "./tonumber.ts";
@@ -350,7 +350,7 @@ const dofileFunction = new LuaBuiltinFunction(async (sf, filename: string) => {
350
350
  )) as Uint8Array;
351
351
  const code = new TextDecoder().decode(file);
352
352
  try {
353
- const parsedExpr = parse(code);
353
+ const parsedExpr = parseBlock(code);
354
354
  const env = new LuaEnv(global);
355
355
  await evalStatement(parsedExpr, env, sf.withCtx(parsedExpr.ctx));
356
356
  } catch (e: any) {
@@ -4,7 +4,7 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // version.ts
7
- var version = "2.8.0";
7
+ var version = "2.9.0";
8
8
 
9
9
  // client/plugos/plug_compile.ts
10
10
  import * as path from "node:path";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@silverbulletmd/silverbullet",
3
- "version": "2.8.0",
3
+ "version": "2.9.0",
4
4
  "type": "module",
5
5
  "description": "A self-hosted, web-based note taking app",
6
6
  "publishConfig": {
@@ -10,6 +10,7 @@
10
10
  "./syscall": "./plug-api/syscall.ts",
11
11
  "./syscalls": "./plug-api/syscalls.ts",
12
12
  "./constants": "./plug-api/constants.ts",
13
+ "./ui": "./plug-api/ui/index.ts",
13
14
  "./lib/json": "./plug-api/lib/json.ts",
14
15
  "./lib/tree": "./plug-api/lib/tree.ts",
15
16
  "./lib/ref": "./plug-api/lib/ref.ts",
@@ -61,9 +62,13 @@
61
62
  "fmt": "biome format --write .",
62
63
  "fmt:check": "biome format .",
63
64
  "bench": "vitest bench",
65
+ "index-stats": "tsx plugs/index/index_stats.ts",
64
66
  "test:e2e": "npx playwright test",
65
67
  "test:e2e:headed": "npx playwright test --headed"
66
68
  },
69
+ "peerDependencies": {
70
+ "preact": "^10.25.0"
71
+ },
67
72
  "dependencies": {
68
73
  "@cfworker/json-schema": "^4.1.1",
69
74
  "@codemirror/autocomplete": "6.20.1",
@@ -91,10 +96,11 @@
91
96
  "@replit/codemirror-vim": "6.3.0",
92
97
  "commander": "^14.0.3",
93
98
  "crelt": "1.0.6",
99
+ "d3-force": "^3.0.0",
94
100
  "esbuild": "^0.27.3",
95
101
  "fast-diff": "1.3.0",
102
+ "force-graph": "^1.49.0",
96
103
  "fast-glob": "^3.3.3",
97
- "fuse.js": "7.1.0",
98
104
  "gitignore-parser": "0.0.2",
99
105
  "idb": "8.0.3",
100
106
  "js-yaml": "^4.1.1",
@@ -110,11 +116,13 @@
110
116
  "@biomejs/biome": "2.4.6",
111
117
  "@playwright/test": "^1.58.2",
112
118
  "@preact/preset-vite": "^2.10.3",
119
+ "@types/d3-force": "^3.0.10",
113
120
  "@types/gitignore-parser": "^0.0.3",
114
121
  "@types/js-yaml": "^4.0.9",
115
122
  "@types/node": "^22.0.0",
116
123
  "@types/turndown": "^5.0.6",
117
124
  "fake-indexeddb": "6.0.1",
125
+ "preact-render-to-string": "^6.7.0",
118
126
  "tsx": "^4.19.0",
119
127
  "typescript": "^5.9.3",
120
128
  "vite": "^7.3.1",
@@ -374,10 +374,15 @@ export function prompt(
374
374
  /**
375
375
  * Prompt the user for confirmation
376
376
  * @param message the message to show in the confirmation dialog
377
+ * @param options optional settings; set `destructive: true` to style the
378
+ * confirm button as a destructive (danger) action
377
379
  * @returns
378
380
  */
379
- export function confirm(message: string): Promise<boolean> {
380
- return syscall("editor.confirm", message);
381
+ export function confirm(
382
+ message: string,
383
+ options?: { destructive?: boolean },
384
+ ): Promise<boolean> {
385
+ return syscall("editor.confirm", message, options);
381
386
  }
382
387
 
383
388
  /**
@@ -1,4 +1,7 @@
1
- import type { LuaCollectionQuery } from "../../client/space_lua/query_collection.ts";
1
+ import type {
2
+ LuaCollectionQuery,
3
+ LuaQueryCollection,
4
+ } from "../../client/space_lua/query_collection.ts";
2
5
  import { syscall } from "@silverbulletmd/silverbullet/syscall";
3
6
  import type { ObjectValue } from "../../plug-api/types/index.ts";
4
7
 
@@ -39,6 +42,98 @@ export function previewProcessedObjects(
39
42
  return syscall("index.previewProcessedObjects", page, objects);
40
43
  }
41
44
 
45
+ /**
46
+ * Returns objects carrying the given tag as a query collection.
47
+ */
48
+ export function objects(tagName: string): Promise<LuaQueryCollection> {
49
+ return syscall("index.objects", tagName);
50
+ }
51
+
52
+ /**
53
+ * Returns all pages (optionally filtered by an additional tag) as a query collection.
54
+ */
55
+ export function pages(tagName?: string): Promise<LuaQueryCollection> {
56
+ return syscall("index.pages", tagName);
57
+ }
58
+
59
+ /**
60
+ * Returns all sub-pages of the given page (pages whose name starts with `<pageName>/`)
61
+ * as a query collection.
62
+ */
63
+ export function subPages(pageName: string): Promise<LuaQueryCollection> {
64
+ return syscall("index.subPages", pageName);
65
+ }
66
+
67
+ /**
68
+ * Returns all documents as a query collection.
69
+ */
70
+ export function documents(): Promise<LuaQueryCollection> {
71
+ return syscall("index.documents");
72
+ }
73
+
74
+ /**
75
+ * Returns all links as a query collection.
76
+ */
77
+ export function links(): Promise<LuaQueryCollection> {
78
+ return syscall("index.links");
79
+ }
80
+
81
+ /**
82
+ * Returns all relation objects as a query collection.
83
+ */
84
+ export function relations(): Promise<LuaQueryCollection> {
85
+ return syscall("index.relations");
86
+ }
87
+
88
+ /**
89
+ * Returns all tasks (optionally filtered by an additional tag) as a query collection.
90
+ */
91
+ export function tasks(tagName?: string): Promise<LuaQueryCollection> {
92
+ return syscall("index.tasks", tagName);
93
+ }
94
+
95
+ /**
96
+ * Returns all headers (optionally filtered by an additional tag) as a query collection.
97
+ */
98
+ export function headers(tagName?: string): Promise<LuaQueryCollection> {
99
+ return syscall("index.headers", tagName);
100
+ }
101
+
102
+ /**
103
+ * Returns all list items (optionally filtered by an additional tag) as a query collection.
104
+ */
105
+ export function items(tagName?: string): Promise<LuaQueryCollection> {
106
+ return syscall("index.items", tagName);
107
+ }
108
+
109
+ /**
110
+ * Returns all paragraphs (optionally filtered by an additional tag) as a query collection.
111
+ */
112
+ export function paragraphs(tagName?: string): Promise<LuaQueryCollection> {
113
+ return syscall("index.paragraphs", tagName);
114
+ }
115
+
116
+ /**
117
+ * Returns all tables (optionally filtered by an additional tag) as a query collection.
118
+ */
119
+ export function tables(tagName?: string): Promise<LuaQueryCollection> {
120
+ return syscall("index.tables", tagName);
121
+ }
122
+
123
+ /**
124
+ * Returns all aspiring (referenced but not yet created) pages as a query collection.
125
+ */
126
+ export function aspiringPages(): Promise<LuaQueryCollection> {
127
+ return syscall("index.aspiringPages");
128
+ }
129
+
130
+ /**
131
+ * Returns all tag objects as a query collection.
132
+ */
133
+ export function tags(): Promise<LuaQueryCollection> {
134
+ return syscall("index.tags");
135
+ }
136
+
42
137
  /**
43
138
  * Queries objects using a Lua-based collection query
44
139
  * @param tag - The tag to filter objects by
@@ -1,8 +1,14 @@
1
1
  import { syscall } from "../syscall.ts";
2
2
  import type { LuaBlock, LuaExpression } from "../../client/space_lua/ast.ts";
3
+ import type { PrintOptions } from "../../client/space_lua/pretty_print.ts";
3
4
 
5
+ export function parseBlock(code: string): Promise<LuaBlock> {
6
+ return syscall("lua.parseBlock", code);
7
+ }
8
+
9
+ /** @deprecated use {@link parseBlock} instead */
4
10
  export function parse(code: string): Promise<LuaBlock> {
5
- return syscall("lua.parse", code);
11
+ return syscall("lua.parseBlock", code);
6
12
  }
7
13
 
8
14
  export function parseExpression(expression: string): Promise<LuaExpression> {
@@ -12,3 +18,17 @@ export function parseExpression(expression: string): Promise<LuaExpression> {
12
18
  export function evalExpression(expression: string): Promise<any> {
13
19
  return syscall("lua.evalExpression", expression);
14
20
  }
21
+
22
+ export function prettyPrintBlock(
23
+ block: LuaBlock,
24
+ opts?: PrintOptions,
25
+ ): Promise<string> {
26
+ return syscall("lua.prettyPrintBlock", block, opts);
27
+ }
28
+
29
+ export function prettyPrintExpression(
30
+ expression: LuaExpression,
31
+ opts?: PrintOptions,
32
+ ): Promise<string> {
33
+ return syscall("lua.prettyPrintExpression", expression, opts);
34
+ }
@@ -7,7 +7,7 @@ export type QueryCollationConfig = {
7
7
  options?: object;
8
8
  };
9
9
 
10
- type vimMode = "normal" | "insert" | "visual";
10
+ type vimMode = "normal" | "insert" | "visual" | "operator";
11
11
 
12
12
  export type VimConfig = {
13
13
  unmap?: (string | { key: string; mode?: vimMode })[];
@@ -0,0 +1,6 @@
1
+ /** Join class-name parts, dropping falsy values. */
2
+ export function cx(
3
+ ...parts: Array<string | false | null | undefined>
4
+ ): string {
5
+ return parts.filter(Boolean).join(" ");
6
+ }
@@ -0,0 +1,19 @@
1
+ export { Button } from "./button.tsx";
2
+ export type { ButtonProps, ButtonVariant } from "./button.tsx";
3
+ export { Input } from "./input.tsx";
4
+ export type { InputProps } from "./input.tsx";
5
+ export { Select } from "./select.tsx";
6
+ export type { SelectProps } from "./select.tsx";
7
+ export { Checkbox } from "./checkbox.tsx";
8
+ export type { CheckboxProps } from "./checkbox.tsx";
9
+ export { Tabs } from "./tabs.tsx";
10
+ export type { TabItem, TabsProps } from "./tabs.tsx";
11
+ export { Alert } from "./alert.tsx";
12
+ export type { AlertProps, AlertVariant } from "./alert.tsx";
13
+ export { Badge } from "./badge.tsx";
14
+ export type { BadgeProps } from "./badge.tsx";
15
+ export { Progress } from "./progress.tsx";
16
+ export type { ProgressProps } from "./progress.tsx";
17
+ export { cx } from "./cx.ts";
18
+ export { panelStyles } from "./panel_styles.ts";
19
+ export type { PanelStylesOptions } from "./panel_styles.ts";
@@ -0,0 +1,32 @@
1
+ import { getUiOption } from "../syscalls/editor.ts";
2
+
3
+ export type PanelStylesOptions = {
4
+ /** Include the shared component stylesheet link (default true). */
5
+ components?: boolean;
6
+ /** Include the user's space-style CSS (default true). */
7
+ spaceStyles?: boolean;
8
+ };
9
+
10
+ /**
11
+ * Build an HTML preamble that styles a plug panel/widget iframe like the rest
12
+ * of SilverBullet: the shared component stylesheet plus the user's space-style
13
+ * CSS. Prepend the result to the HTML you pass to `editor.showPanel`.
14
+ *
15
+ * Order: components first (base), then space styles (so user theming wins),
16
+ * leaving any plug-specific CSS you append afterwards highest-precedence.
17
+ */
18
+ export async function panelStyles(
19
+ { components = true, spaceStyles = true }: PanelStylesOptions = {},
20
+ ): Promise<string> {
21
+ let out = "";
22
+ if (components) {
23
+ out += `<link rel="stylesheet" href=".client/components.css">`;
24
+ }
25
+ if (spaceStyles) {
26
+ const custom = await getUiOption("customStyles");
27
+ if (typeof custom === "string" && custom) {
28
+ out += custom;
29
+ }
30
+ }
31
+ return out;
32
+ }
@@ -7,6 +7,7 @@ export const builtinPlugNames = [
7
7
  "emoji",
8
8
  "image-viewer",
9
9
  "configuration-manager",
10
+ "object-graph",
10
11
  ];
11
12
 
12
13
  export const builtinPlugPaths = builtinPlugNames.map(