@jbarket/loomai 0.8.0 → 0.9.1
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/CHANGELOG.md +52 -1
- package/README.md +33 -16
- package/dist/backends/migrations.js +18 -0
- package/dist/backends/salience.d.ts +38 -4
- package/dist/backends/salience.js +82 -30
- package/dist/backends/sqlite-vec.js +14 -5
- package/dist/transport/http-server.js +5 -5
- package/dist/ui/core.d.ts +15 -3
- package/dist/ui/core.js +414 -90
- package/dist/ui/data.d.ts +53 -0
- package/dist/ui/data.js +129 -0
- package/dist/ui/handler.d.ts +3 -2
- package/dist/ui/handler.js +328 -63
- package/dist/ui/pieces.d.ts +37 -0
- package/dist/ui/pieces.js +191 -0
- package/dist/ui/render.d.ts +47 -10
- package/dist/ui/render.js +220 -48
- package/dist/ui/vocab.d.ts +175 -0
- package/dist/ui/vocab.js +360 -0
- package/package.json +1 -1
- package/dist/ui/piece-sections.d.ts +0 -20
- package/dist/ui/piece-sections.js +0 -112
- package/dist/ui/sections.d.ts +0 -51
- package/dist/ui/sections.js +0 -121
package/dist/ui/sections.d.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The UI section vocabulary (docs/extensions.md, "UI sections").
|
|
3
|
-
*
|
|
4
|
-
* loom's page is built from sections, both its own and the installed pieces'.
|
|
5
|
-
* A piece contributes by answering `loom-<piece> ui --json` with
|
|
6
|
-
* `{ "sections": [...] }` in this vocabulary. Pieces send data, never markup:
|
|
7
|
-
* loom renders every block itself and escapes every string, so a piece can't
|
|
8
|
-
* put HTML or script on loom's page. Anything that doesn't validate is dropped
|
|
9
|
-
* whole (a piece that sends garbage just isn't shown), never half-rendered.
|
|
10
|
-
*/
|
|
11
|
-
export type Tone = 'ok' | 'warn' | 'bad' | 'muted';
|
|
12
|
-
export type Block = {
|
|
13
|
-
type: 'stats';
|
|
14
|
-
items: Array<{
|
|
15
|
-
label: string;
|
|
16
|
-
value: string;
|
|
17
|
-
tone?: Tone;
|
|
18
|
-
}>;
|
|
19
|
-
} | {
|
|
20
|
-
type: 'list';
|
|
21
|
-
items: Array<{
|
|
22
|
-
text: string;
|
|
23
|
-
meta?: string;
|
|
24
|
-
tone?: Tone;
|
|
25
|
-
}>;
|
|
26
|
-
} | {
|
|
27
|
-
type: 'table';
|
|
28
|
-
columns: string[];
|
|
29
|
-
rows: string[][];
|
|
30
|
-
} | {
|
|
31
|
-
type: 'text';
|
|
32
|
-
text: string;
|
|
33
|
-
} | {
|
|
34
|
-
type: 'link';
|
|
35
|
-
text: string;
|
|
36
|
-
href: string;
|
|
37
|
-
};
|
|
38
|
-
export interface Section {
|
|
39
|
-
id: string;
|
|
40
|
-
title: string;
|
|
41
|
-
blocks: Block[];
|
|
42
|
-
}
|
|
43
|
-
export declare const LIMITS: {
|
|
44
|
-
readonly sections: 10;
|
|
45
|
-
readonly blocks: 20;
|
|
46
|
-
readonly items: 200;
|
|
47
|
-
readonly columns: 12;
|
|
48
|
-
readonly string: 2000;
|
|
49
|
-
};
|
|
50
|
-
/** Validate a piece's `ui --json` answer. Returns the sections, or why it was refused. */
|
|
51
|
-
export declare function parseSections(value: unknown): Section[] | string;
|
package/dist/ui/sections.js
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The UI section vocabulary (docs/extensions.md, "UI sections").
|
|
3
|
-
*
|
|
4
|
-
* loom's page is built from sections, both its own and the installed pieces'.
|
|
5
|
-
* A piece contributes by answering `loom-<piece> ui --json` with
|
|
6
|
-
* `{ "sections": [...] }` in this vocabulary. Pieces send data, never markup:
|
|
7
|
-
* loom renders every block itself and escapes every string, so a piece can't
|
|
8
|
-
* put HTML or script on loom's page. Anything that doesn't validate is dropped
|
|
9
|
-
* whole (a piece that sends garbage just isn't shown), never half-rendered.
|
|
10
|
-
*/
|
|
11
|
-
export const LIMITS = {
|
|
12
|
-
sections: 10,
|
|
13
|
-
blocks: 20,
|
|
14
|
-
items: 200,
|
|
15
|
-
columns: 12,
|
|
16
|
-
string: 2000,
|
|
17
|
-
};
|
|
18
|
-
const TONES = new Set(['ok', 'warn', 'bad', 'muted']);
|
|
19
|
-
const ID = /^[a-z][a-z0-9-]{0,39}$/;
|
|
20
|
-
class Invalid extends Error {
|
|
21
|
-
}
|
|
22
|
-
function str(v, what) {
|
|
23
|
-
if (typeof v === 'number' && Number.isFinite(v))
|
|
24
|
-
return String(v);
|
|
25
|
-
if (typeof v !== 'string')
|
|
26
|
-
throw new Invalid(`${what} must be a string`);
|
|
27
|
-
if (v.length > LIMITS.string)
|
|
28
|
-
throw new Invalid(`${what} is longer than ${LIMITS.string}`);
|
|
29
|
-
return v;
|
|
30
|
-
}
|
|
31
|
-
function optStr(v, what) {
|
|
32
|
-
return v === undefined ? undefined : str(v, what);
|
|
33
|
-
}
|
|
34
|
-
function tone(v) {
|
|
35
|
-
if (v === undefined)
|
|
36
|
-
return undefined;
|
|
37
|
-
if (typeof v !== 'string' || !TONES.has(v))
|
|
38
|
-
throw new Invalid(`tone must be one of ${[...TONES].join(', ')}`);
|
|
39
|
-
return v;
|
|
40
|
-
}
|
|
41
|
-
function arr(v, what, max) {
|
|
42
|
-
if (!Array.isArray(v))
|
|
43
|
-
throw new Invalid(`${what} must be an array`);
|
|
44
|
-
if (v.length > max)
|
|
45
|
-
throw new Invalid(`${what} has more than ${max} entries`);
|
|
46
|
-
return v;
|
|
47
|
-
}
|
|
48
|
-
function obj(v, what) {
|
|
49
|
-
if (typeof v !== 'object' || v === null || Array.isArray(v))
|
|
50
|
-
throw new Invalid(`${what} must be an object`);
|
|
51
|
-
return v;
|
|
52
|
-
}
|
|
53
|
-
function block(v) {
|
|
54
|
-
const b = obj(v, 'block');
|
|
55
|
-
switch (b.type) {
|
|
56
|
-
case 'stats':
|
|
57
|
-
return {
|
|
58
|
-
type: 'stats',
|
|
59
|
-
items: arr(b.items, 'stats.items', LIMITS.items).map((i) => {
|
|
60
|
-
const o = obj(i, 'stat');
|
|
61
|
-
return { label: str(o.label, 'stat.label'), value: str(o.value, 'stat.value'), tone: tone(o.tone) };
|
|
62
|
-
}),
|
|
63
|
-
};
|
|
64
|
-
case 'list':
|
|
65
|
-
return {
|
|
66
|
-
type: 'list',
|
|
67
|
-
items: arr(b.items, 'list.items', LIMITS.items).map((i) => {
|
|
68
|
-
const o = obj(i, 'list item');
|
|
69
|
-
return { text: str(o.text, 'list.text'), meta: optStr(o.meta, 'list.meta'), tone: tone(o.tone) };
|
|
70
|
-
}),
|
|
71
|
-
};
|
|
72
|
-
case 'table': {
|
|
73
|
-
const columns = arr(b.columns, 'table.columns', LIMITS.columns).map((c) => str(c, 'table column'));
|
|
74
|
-
const rows = arr(b.rows, 'table.rows', LIMITS.items).map((r) => {
|
|
75
|
-
const row = arr(r, 'table row', columns.length).map((c) => str(c, 'table cell'));
|
|
76
|
-
return row;
|
|
77
|
-
});
|
|
78
|
-
return { type: 'table', columns, rows };
|
|
79
|
-
}
|
|
80
|
-
case 'text':
|
|
81
|
-
return { type: 'text', text: str(b.text, 'text') };
|
|
82
|
-
case 'link': {
|
|
83
|
-
const href = str(b.href, 'link.href');
|
|
84
|
-
// Only http(s): no javascript:, data:, or relative tricks.
|
|
85
|
-
let url;
|
|
86
|
-
try {
|
|
87
|
-
url = new URL(href);
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
throw new Invalid('link.href must be an absolute URL');
|
|
91
|
-
}
|
|
92
|
-
if (url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
93
|
-
throw new Invalid('link.href must be http(s)');
|
|
94
|
-
return { type: 'link', text: str(b.text, 'link.text'), href: url.href };
|
|
95
|
-
}
|
|
96
|
-
default:
|
|
97
|
-
throw new Invalid(`unknown block type ${JSON.stringify(b.type)}`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
/** Validate a piece's `ui --json` answer. Returns the sections, or why it was refused. */
|
|
101
|
-
export function parseSections(value) {
|
|
102
|
-
try {
|
|
103
|
-
const top = obj(value, 'answer');
|
|
104
|
-
return arr(top.sections, 'sections', LIMITS.sections).map((s) => {
|
|
105
|
-
const o = obj(s, 'section');
|
|
106
|
-
const id = str(o.id, 'section.id');
|
|
107
|
-
if (!ID.test(id))
|
|
108
|
-
throw new Invalid(`section.id ${JSON.stringify(id)} must be lowercase letters, digits and dashes`);
|
|
109
|
-
return {
|
|
110
|
-
id,
|
|
111
|
-
title: str(o.title, 'section.title'),
|
|
112
|
-
blocks: arr(o.blocks, 'section.blocks', LIMITS.blocks).map(block),
|
|
113
|
-
};
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
catch (err) {
|
|
117
|
-
if (err instanceof Invalid)
|
|
118
|
-
return err.message;
|
|
119
|
-
throw err;
|
|
120
|
-
}
|
|
121
|
-
}
|