@beeuni/sdk 0.2.0 → 0.3.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 +19 -10
- package/dist/beeuni.esm.min.js +37 -35
- package/dist/beeuni.global.js +37 -35
- package/dist/browser/cache/idb-sync-cache.js +15 -5
- package/dist/browser/session-sync/server-transcript-mirror.d.ts +7 -1
- package/dist/browser/session-sync/server-transcript-mirror.js +30 -24
- package/dist/browser/session-sync/session-sync-registry.d.ts +3 -1
- package/dist/browser/session-sync/session-sync-registry.js +48 -3
- package/dist/browser/stores/sync-store/keep-unchanged.d.ts +4 -0
- package/dist/browser/stores/sync-store/keep-unchanged.js +34 -0
- package/dist/browser/stores/sync-store.d.ts +1 -0
- package/dist/browser/stores/sync-store.js +72 -16
- package/dist/core/attachments/prompt-attachments.d.ts +4 -0
- package/dist/core/attachments/prompt-attachments.js +34 -0
- package/dist/core/capture/rules.d.ts +17 -0
- package/dist/core/capture/rules.js +27 -0
- package/dist/core/client/beeuni.d.ts +92 -5
- package/dist/core/client/beeuni.js +2 -0
- package/dist/core/client/capture.d.ts +105 -0
- package/dist/core/client/capture.js +48 -0
- package/dist/core/client/facade-identity.d.ts +1 -0
- package/dist/core/client/facade-identity.js +1 -0
- package/dist/core/client/project-access-resources.d.ts +3 -1
- package/dist/core/client/project-access-resources.js +1 -1
- package/dist/core/client/project-operations-admin.d.ts +4 -4
- package/dist/core/client/project-operations-review.d.ts +2 -3
- package/dist/core/client/project-operations-review.js +1 -0
- package/dist/core/client/project-operations-workspace.d.ts +4 -4
- package/dist/core/client/project-operations-workspace.js +1 -1
- package/dist/core/client/project-operations.d.ts +5 -4
- package/dist/core/client/session-context.d.ts +2 -0
- package/dist/core/client/session-handle.d.ts +13 -0
- package/dist/core/client/session-lifecycle-resources.d.ts +11 -0
- package/dist/core/client/session-lifecycle-resources.js +4 -0
- package/dist/core/client/session-lifecycle-security.d.ts +2 -0
- package/dist/core/client/session-lifecycle-security.js +1 -0
- package/dist/core/client/session-readiness.d.ts +2 -0
- package/dist/core/client/session-readiness.js +20 -1
- package/dist/core/http/api-client.d.ts +1 -0
- package/dist/core/http/api-client.js +1 -0
- package/dist/core/http/config.d.ts +2 -0
- package/dist/core/http/response-error.js +26 -11
- package/dist/core/http/transport.js +6 -1
- package/dist/core/models/composer-model.d.ts +14 -0
- package/dist/core/models/composer-model.js +21 -1
- package/dist/core/rest/platform-client/capture.d.ts +363 -0
- package/dist/core/rest/platform-client/capture.js +116 -0
- package/dist/core/rest/platform-client/index.d.ts +1 -0
- package/dist/core/rest/platform-client/index.js +1 -0
- package/dist/core/rest/platform-client/lifecycle.js +18 -12
- package/dist/core/rest/platform-client/shared.d.ts +5 -0
- package/dist/core/rest/platform-client/shared.js +18 -0
- package/dist/core/rest/projects-client/account-secret-resources.d.ts +2 -1
- package/dist/core/rest/projects-client/accounts.d.ts +34 -0
- package/dist/core/rest/projects-client/accounts.js +10 -0
- package/dist/core/rest/projects-client/admin-providers.d.ts +0 -61
- package/dist/core/rest/projects-client/admin-providers.js +0 -6
- package/dist/core/rest/projects-client/agent-config.d.ts +3 -0
- package/dist/core/rest/projects-client/apps-fleet.d.ts +5 -1
- package/dist/core/rest/projects-client/apps-fleet.js +2 -2
- package/dist/core/rest/projects-client/apps.d.ts +3 -0
- package/dist/core/rest/projects-client/change-requests.d.ts +2 -0
- package/dist/core/rest/projects-client/change-requests.js +2 -0
- package/dist/core/rest/projects-client/connectors.d.ts +7 -0
- package/dist/core/rest/projects-client/connectors.js +3 -0
- package/dist/core/rest/projects-client/files.d.ts +3 -0
- package/dist/core/rest/projects-client/files.js +4 -0
- package/dist/core/rest/projects-client/gateway.d.ts +5 -0
- package/dist/core/rest/projects-client/iam-actions.d.ts +76 -0
- package/dist/core/rest/projects-client/iam-actions.js +101 -0
- package/dist/core/rest/projects-client/iam.d.ts +1 -0
- package/dist/core/rest/projects-client/iam.js +1 -0
- package/dist/core/rest/projects-client/notifications.d.ts +1 -1
- package/dist/core/rest/projects-client/notifications.js +4 -0
- package/dist/core/rest/projects-client/projects.d.ts +3 -2
- package/dist/core/rest/projects-client/projects.js +2 -3
- package/dist/core/rest/projects-client/review-items.d.ts +2 -0
- package/dist/core/rest/projects-client/review-items.js +4 -0
- package/dist/core/rest/projects-client/review.d.ts +17 -3
- package/dist/core/rest/projects-client/review.js +11 -0
- package/dist/core/rest/projects-client/secrets.d.ts +3 -0
- package/dist/core/rest/projects-client/session-attachments.js +14 -1
- package/dist/core/rest/projects-client/sessions.d.ts +29 -2
- package/dist/core/rest/projects-client/sessions.js +34 -4
- package/dist/core/runtime/runtime-rest-client.js +1 -1
- package/dist/core/runtime/runtime-types.d.ts +1 -0
- package/dist/core/session/open-bundle.d.ts +4 -4
- package/dist/core/session/open-bundle.js +29 -46
- package/dist/core/session/turn-end-watch.d.ts +47 -0
- package/dist/core/session/turn-end-watch.js +141 -0
- package/dist/core/session-log/index.d.ts +4 -0
- package/dist/core/session-log/index.js +3 -0
- package/dist/core/session-log/tool-call.d.ts +7 -2
- package/dist/core/session-log/tool-call.js +25 -6
- package/dist/core/session-log/turns/blocks.d.ts +60 -0
- package/dist/core/session-log/turns/blocks.js +124 -0
- package/dist/core/session-log/turns/history.d.ts +32 -0
- package/dist/core/session-log/turns/history.js +99 -0
- package/dist/core/session-log/turns/index.d.ts +4 -0
- package/dist/core/session-log/turns/index.js +4 -0
- package/dist/core/session-log/turns/metrics.d.ts +21 -0
- package/dist/core/session-log/turns/metrics.js +29 -0
- package/dist/core/session-log/turns/turn.d.ts +55 -0
- package/dist/core/session-log/turns/turn.js +279 -0
- package/dist/core/session-log/turns/v1-parts.d.ts +8 -0
- package/dist/core/session-log/turns/v1-parts.js +43 -0
- package/dist/core/session-log/v1-view.d.ts +5 -2
- package/dist/core/session-log/v1-view.js +86 -22
- package/dist/core/session-log/v1.js +10 -3
- package/dist/core/session-sync/saved-transcript.d.ts +15 -1
- package/dist/core/session-sync/saved-transcript.js +55 -0
- package/dist/core/session-sync/session-sync-controller.js +3 -0
- package/dist/core/stream/event-stream.js +3 -3
- package/dist/core/turns/classify.js +4 -2
- package/dist/core/turns/edit-blocks.d.ts +1 -0
- package/dist/core/turns/edit-blocks.js +15 -0
- package/dist/core/turns/segments/burst-summary.d.ts +1 -0
- package/dist/core/turns/segments/burst-summary.js +24 -0
- package/dist/core/turns/segments/group-steps.d.ts +4 -3
- package/dist/core/turns/segments/group-steps.js +9 -6
- package/dist/core/turns/segments/narration.d.ts +5 -4
- package/dist/core/turns/segments/narration.js +12 -3
- package/dist/core/turns/segments/session-activity-groups.d.ts +2 -1
- package/dist/core/turns/segments/session-activity-groups.js +3 -1
- package/dist/core/turns/segments/step-label.d.ts +2 -1
- package/dist/core/turns/segments/step-label.js +3 -1
- package/dist/core/turns/segments/working-turn.d.ts +4 -2
- package/dist/core/turns/segments/working-turn.js +9 -8
- package/dist/core/turns/tool-kind.d.ts +1 -1
- package/dist/core/turns/tool-kind.js +3 -2
- package/dist/core/turns/tool-registry.js +1 -0
- package/dist/core/turns/tools/tool-meta.d.ts +1 -1
- package/dist/core/turns/tools/tool-meta.js +3 -2
- package/dist/core/turns/view-model.js +1 -13
- package/dist/genui/catalog.d.ts +28 -0
- package/dist/genui/catalog.js +396 -0
- package/dist/genui/expansion.d.ts +3 -0
- package/dist/genui/expansion.js +295 -0
- package/dist/genui/fence-entry.d.ts +2 -0
- package/dist/genui/fence-entry.js +1 -0
- package/dist/genui/fence.d.ts +21 -0
- package/dist/genui/fence.js +138 -0
- package/dist/genui/index.d.ts +6 -0
- package/dist/genui/index.js +6 -0
- package/dist/genui/markdown.d.ts +12 -0
- package/dist/genui/markdown.js +104 -0
- package/dist/genui/parse.d.ts +6 -0
- package/dist/genui/parse.js +75 -0
- package/dist/genui/prompt.d.ts +2 -0
- package/dist/genui/prompt.js +206 -0
- package/dist/genui/react/default-components.d.ts +2 -0
- package/dist/genui/react/default-components.js +72 -0
- package/dist/genui/react/default-components.test.d.ts +1 -0
- package/dist/genui/react/default-components.test.js +94 -0
- package/dist/genui/react/genui-block-disabled.test.d.ts +1 -0
- package/dist/genui/react/genui-block-disabled.test.js +50 -0
- package/dist/genui/react/genui-block.d.ts +28 -0
- package/dist/genui/react/genui-block.js +91 -0
- package/dist/genui/react/genui-block.test.d.ts +1 -0
- package/dist/genui/react/genui-block.test.js +205 -0
- package/dist/genui/react/index.d.ts +2 -0
- package/dist/genui/react/index.js +2 -0
- package/dist/genui/share.d.ts +5 -0
- package/dist/genui/share.js +30 -0
- package/dist/genui/test-fixtures.d.ts +2 -0
- package/dist/genui/test-fixtures.js +16 -0
- package/dist/genui/types.d.ts +29 -0
- package/dist/genui/types.js +1 -0
- package/dist/genui/urls.d.ts +1 -0
- package/dist/genui/urls.js +18 -0
- package/dist/genui/validate.d.ts +9 -0
- package/dist/genui/validate.js +116 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/internal/connector-catalog.d.ts +1 -0
- package/dist/platform/ui.d.ts +8 -2
- package/dist/platform/ui.js +19 -4
- package/dist/react/chat/use-chat-turns.d.ts +1 -1
- package/dist/react/human-presence.d.ts +9 -0
- package/dist/react/human-presence.js +12 -0
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.js +4 -0
- package/dist/react/opencode.d.ts +1 -1
- package/dist/react/opencode.js +1 -1
- package/dist/react/query-contracts.d.ts +6 -0
- package/dist/react/query-contracts.js +6 -0
- package/dist/react/query-keys.d.ts +17 -1
- package/dist/react/query-keys.js +24 -1
- package/dist/react/reset-identity-state.js +2 -0
- package/dist/react/session-cache-write.d.ts +1 -0
- package/dist/react/session-cache-write.js +20 -0
- package/dist/react/session-transcript-subscription.js +1 -1
- package/dist/react/sign-in-with-beeuni.d.ts +5 -5
- package/dist/react/turn-end-handoff.d.ts +13 -0
- package/dist/react/turn-end-handoff.js +101 -0
- package/dist/react/use-admin-providers.d.ts +0 -3
- package/dist/react/use-admin-providers.js +1 -11
- package/dist/react/use-capture.d.ts +47 -0
- package/dist/react/use-capture.js +173 -0
- package/dist/react/use-feature-flag.js +1 -0
- package/dist/react/use-opencode-events/handle-interaction-event.js +8 -3
- package/dist/react/use-opencode-events/handle-session-event.js +13 -8
- package/dist/react/use-opencode-events/handler-context.d.ts +1 -0
- package/dist/react/use-opencode-events/helpers.d.ts +1 -0
- package/dist/react/use-opencode-events/helpers.js +11 -1
- package/dist/react/use-opencode-local.d.ts +5 -1
- package/dist/react/use-opencode-local.js +15 -2
- package/dist/react/use-opencode-sessions/commands.d.ts +11 -1
- package/dist/react/use-opencode-sessions/commands.js +21 -1
- package/dist/react/use-opencode-sessions/permissions.js +28 -1
- package/dist/react/use-project-sessions.d.ts +1 -1
- package/dist/react/use-project-sessions.js +6 -5
- package/dist/react/use-prompt-attachments.js +1 -0
- package/dist/react/use-review-summary.d.ts +13 -0
- package/dist/react/use-review-summary.js +32 -0
- package/dist/react/use-runtime-reconnect.js +2 -2
- package/dist/react/use-session-list-state.d.ts +6 -0
- package/dist/react/use-session-list-state.js +33 -0
- package/dist/react/use-session-log.d.ts +3 -2
- package/dist/react/use-session-log.js +26 -17
- package/dist/react/use-session-messages.d.ts +2 -1
- package/dist/react/use-session-messages.js +15 -14
- package/dist/react/use-session-stream.js +12 -1
- package/dist/react/use-session-sync.d.ts +2 -2
- package/dist/react/use-session-sync.js +10 -8
- package/dist/react/use-session-transcript-history.js +2 -12
- package/dist/react/use-session-working.d.ts +2 -0
- package/dist/react/use-session-working.js +10 -0
- package/dist/react/use-session.d.ts +6 -0
- package/dist/react/use-session.js +87 -7
- package/package.json +49 -3
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { createLibrary, defineComponent } from '@openuidev/lang-core';
|
|
2
|
+
import { z } from 'zod/v4';
|
|
3
|
+
const isSlot = (field) => 'accepts' in field;
|
|
4
|
+
const defined = {};
|
|
5
|
+
const specs = {};
|
|
6
|
+
function refsOf(names) {
|
|
7
|
+
const refs = names.map((name) => {
|
|
8
|
+
const component = defined[name];
|
|
9
|
+
if (!component)
|
|
10
|
+
throw new Error(`genui catalog: ${name} is used before it is defined`);
|
|
11
|
+
return component.ref;
|
|
12
|
+
});
|
|
13
|
+
return refs.length === 1 ? refs[0] : z.union(refs);
|
|
14
|
+
}
|
|
15
|
+
function component(input) {
|
|
16
|
+
const openui = {};
|
|
17
|
+
const strict = {};
|
|
18
|
+
const streaming = {};
|
|
19
|
+
const slots = {};
|
|
20
|
+
for (const [key, field] of Object.entries(input.fields)) {
|
|
21
|
+
if (!isSlot(field)) {
|
|
22
|
+
openui[key] = field;
|
|
23
|
+
strict[key] = field;
|
|
24
|
+
streaming[key] = field;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
slots[key] = field;
|
|
28
|
+
const item = field.lazy ? z.lazy(() => refsOf(field.accepts)) : refsOf(field.accepts);
|
|
29
|
+
const strictArray = z.array(z.unknown()).min(field.min).max(field.max);
|
|
30
|
+
const streamingArray = z.array(z.unknown()).max(field.max);
|
|
31
|
+
openui[key] = field.optional ? z.array(item).optional() : z.array(item);
|
|
32
|
+
strict[key] = field.optional ? strictArray.optional() : strictArray;
|
|
33
|
+
streaming[key] = streamingArray.optional();
|
|
34
|
+
}
|
|
35
|
+
defined[input.name] = defineComponent({
|
|
36
|
+
name: input.name,
|
|
37
|
+
description: input.description,
|
|
38
|
+
props: z.object(openui),
|
|
39
|
+
component: input.name,
|
|
40
|
+
});
|
|
41
|
+
specs[input.name] = {
|
|
42
|
+
name: input.name,
|
|
43
|
+
block: input.block ?? true,
|
|
44
|
+
container: input.container ?? false,
|
|
45
|
+
slots,
|
|
46
|
+
urls: input.urls ?? {},
|
|
47
|
+
strict: z.object(strict),
|
|
48
|
+
streaming: z.object(streaming),
|
|
49
|
+
toMarkdown: input.toMarkdown,
|
|
50
|
+
a11y: input.a11y,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const escapeLine = (value) => value.replace(/[\\`*_[\]()<|]/g, '\\$&');
|
|
54
|
+
const inline = (value) => escapeLine(String(value ?? '').replace(/\s*[\r\n]+\s*/g, ' '));
|
|
55
|
+
const target = (value) => String(value ?? '').replace(/\(/g, '%28').replace(/\)/g, '%29');
|
|
56
|
+
const cell = (value) => (typeof value === 'number' ? String(value) : inline(value));
|
|
57
|
+
const mdTable = (head, rows) => [
|
|
58
|
+
`| ${head.join(' | ')} |`,
|
|
59
|
+
`| ${head.map(() => '---').join(' | ')} |`,
|
|
60
|
+
...rows.map((row) => `| ${head.map((_, i) => cell(row[i] ?? '')).join(' | ')} |`),
|
|
61
|
+
].join('\n');
|
|
62
|
+
const nodes = (value) => (Array.isArray(value) ? value : []);
|
|
63
|
+
const joinChildren = (value, child, separator = '\n\n') => nodes(value)
|
|
64
|
+
.map(child)
|
|
65
|
+
.filter((text) => text.length > 0)
|
|
66
|
+
.join(separator);
|
|
67
|
+
const formatNumber = (value) => typeof value === 'number' && Number.isFinite(value) ? value.toLocaleString('en-US') : '';
|
|
68
|
+
const withUnit = (value, unit) => (unit ? `${value} ${inline(unit)}` : value);
|
|
69
|
+
const sourceLine = (source) => `Source: ${inline(source)}`;
|
|
70
|
+
function seriesTable(labels, series, unit) {
|
|
71
|
+
const head = ['', ...series.map((s) => `${inline(s.props.name)}${unit ? ` \\(${inline(unit)}\\)` : ''}`)];
|
|
72
|
+
const rows = labels.map((label, i) => [
|
|
73
|
+
label,
|
|
74
|
+
...series.map((s) => formatNumber(s.props.values?.[i])),
|
|
75
|
+
]);
|
|
76
|
+
return mdTable(head, rows);
|
|
77
|
+
}
|
|
78
|
+
const osmLink = (lat, lng) => `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lng}#map=15/${lat}/${lng}`;
|
|
79
|
+
const text = (max) => z.string().min(1).max(max);
|
|
80
|
+
const optText = (max) => z.string().max(max).optional();
|
|
81
|
+
const url = () => z.string().max(2048);
|
|
82
|
+
component({
|
|
83
|
+
name: 'Badge',
|
|
84
|
+
description: 'Short status tag. tone: neutral, good, warn, bad',
|
|
85
|
+
fields: { label: text(24), tone: z.enum(['neutral', 'good', 'warn', 'bad']).optional() },
|
|
86
|
+
toMarkdown: (p) => `[${inline(p.label)}]`,
|
|
87
|
+
});
|
|
88
|
+
component({
|
|
89
|
+
name: 'Link',
|
|
90
|
+
description: 'A link to a source, booking page, or document',
|
|
91
|
+
fields: { label: text(80), href: url() },
|
|
92
|
+
urls: { href: 'required' },
|
|
93
|
+
toMarkdown: (p) => `[${inline(p.label)}](${target(p.href)})`,
|
|
94
|
+
});
|
|
95
|
+
component({
|
|
96
|
+
name: 'Image',
|
|
97
|
+
description: 'A picture. Use only URLs from tool results or files. With text, the picture sits beside 1 to 3 sentences',
|
|
98
|
+
fields: { src: url(), alt: text(200), caption: optText(200), text: optText(400) },
|
|
99
|
+
urls: { src: 'required' },
|
|
100
|
+
toMarkdown: (p) => [`})`, p.caption ? `*${inline(p.caption)}*` : '', p.text ? inline(p.text) : '']
|
|
101
|
+
.filter(Boolean)
|
|
102
|
+
.join('\n\n'),
|
|
103
|
+
});
|
|
104
|
+
component({
|
|
105
|
+
name: 'Callout',
|
|
106
|
+
description: 'A key takeaway or warning. tone: info, warn, success',
|
|
107
|
+
fields: { tone: z.enum(['info', 'warn', 'success']), body: text(400), title: optText(80) },
|
|
108
|
+
toMarkdown: (p) => String(p.body ?? '')
|
|
109
|
+
.split(/\r?\n/)
|
|
110
|
+
.map((line, i) => `> ${i === 0 && p.title ? `**${inline(p.title)}** ` : ''}${escapeLine(line.trim())}`.trimEnd())
|
|
111
|
+
.join('\n'),
|
|
112
|
+
});
|
|
113
|
+
component({
|
|
114
|
+
name: 'Stat',
|
|
115
|
+
description: 'One key number. trend: up, down, flat',
|
|
116
|
+
fields: {
|
|
117
|
+
label: text(40),
|
|
118
|
+
value: text(24),
|
|
119
|
+
delta: optText(24),
|
|
120
|
+
trend: z.enum(['up', 'down', 'flat']).optional(),
|
|
121
|
+
unit: optText(12),
|
|
122
|
+
},
|
|
123
|
+
toMarkdown: (p) => `**${inline(p.label)}:** ${withUnit(inline(p.value), p.unit)}${p.delta ? ` (${inline(p.delta)})` : ''}`,
|
|
124
|
+
});
|
|
125
|
+
component({
|
|
126
|
+
name: 'StatRow',
|
|
127
|
+
description: '2 to 4 Stats in a row, for the key numbers of an answer',
|
|
128
|
+
fields: { stats: { accepts: ['Stat'], min: 2, max: 4 } },
|
|
129
|
+
toMarkdown: (p, child) => nodes(p.stats).map((stat) => `- ${child(stat)}`).join('\n'),
|
|
130
|
+
});
|
|
131
|
+
component({
|
|
132
|
+
name: 'Table',
|
|
133
|
+
description: 'Rows a user scans or sorts. Up to 8 columns and 50 rows',
|
|
134
|
+
fields: {
|
|
135
|
+
columns: z.array(text(40)).min(1).max(8),
|
|
136
|
+
rows: z.array(z.array(z.union([z.string().max(200), z.number()])).max(8)).max(50),
|
|
137
|
+
caption: optText(120),
|
|
138
|
+
},
|
|
139
|
+
toMarkdown: (p) => [p.caption ? `*${inline(p.caption)}*` : '', mdTable(p.columns.map(inline), p.rows)]
|
|
140
|
+
.filter(Boolean)
|
|
141
|
+
.join('\n\n'),
|
|
142
|
+
});
|
|
143
|
+
component({
|
|
144
|
+
name: 'CompareItem',
|
|
145
|
+
description: 'One option inside Compare. values line up with Compare specs',
|
|
146
|
+
block: false,
|
|
147
|
+
fields: {
|
|
148
|
+
name: text(60),
|
|
149
|
+
values: z.array(z.string().max(120)).max(12),
|
|
150
|
+
pros: z.array(z.string().max(120)).max(5).optional(),
|
|
151
|
+
cons: z.array(z.string().max(120)).max(5).optional(),
|
|
152
|
+
},
|
|
153
|
+
toMarkdown: (p) => [
|
|
154
|
+
`**${inline(p.name)}**`,
|
|
155
|
+
...(p.pros ?? []).map((pro) => `- + ${inline(pro)}`),
|
|
156
|
+
...(p.cons ?? []).map((con) => `- − ${inline(con)}`),
|
|
157
|
+
].join('\n'),
|
|
158
|
+
});
|
|
159
|
+
component({
|
|
160
|
+
name: 'Compare',
|
|
161
|
+
description: 'Side-by-side comparison of 2 to 4 options. specs are the row labels; winner names the pick',
|
|
162
|
+
fields: {
|
|
163
|
+
items: { accepts: ['CompareItem'], min: 2, max: 4 },
|
|
164
|
+
specs: z.array(text(40)).max(12).optional(),
|
|
165
|
+
winner: optText(60),
|
|
166
|
+
},
|
|
167
|
+
toMarkdown: (p, child) => {
|
|
168
|
+
const items = nodes(p.items);
|
|
169
|
+
const specsList = p.specs ?? [];
|
|
170
|
+
const parts = [];
|
|
171
|
+
if (specsList.length > 0) {
|
|
172
|
+
parts.push(mdTable(['', ...items.map((item) => inline(item.props.name))], specsList.map((spec, i) => [spec, ...items.map((item) => item.props.values[i] ?? '—')])));
|
|
173
|
+
}
|
|
174
|
+
parts.push(...items.map(child));
|
|
175
|
+
if (p.winner)
|
|
176
|
+
parts.push(`**Pick:** ${inline(p.winner)}`);
|
|
177
|
+
return parts.join('\n\n');
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
component({
|
|
181
|
+
name: 'RankedItem',
|
|
182
|
+
description: 'One entry inside RankedList. Leave image empty: options with photos are Cards',
|
|
183
|
+
block: false,
|
|
184
|
+
fields: { title: text(80), reason: text(240), meta: optText(80), image: url().optional(), href: url().optional() },
|
|
185
|
+
urls: { image: 'optional', href: 'optional' },
|
|
186
|
+
toMarkdown: (p) => `**${inline(p.title)}** — ${inline(p.reason)}${p.meta ? ` (${inline(p.meta)})` : ''}${p.href ? ` [Open](${target(p.href)})` : ''}`,
|
|
187
|
+
});
|
|
188
|
+
component({
|
|
189
|
+
name: 'RankedList',
|
|
190
|
+
description: 'A ranked recommendation, best first. Up to 10 items',
|
|
191
|
+
fields: { items: { accepts: ['RankedItem'], min: 1, max: 10 } },
|
|
192
|
+
toMarkdown: (p, child) => nodes(p.items).map((item, i) => `${i + 1}. ${child(item)}`).join('\n'),
|
|
193
|
+
});
|
|
194
|
+
component({
|
|
195
|
+
name: 'Series',
|
|
196
|
+
description: 'One named series of numbers inside a chart',
|
|
197
|
+
block: false,
|
|
198
|
+
fields: { name: text(40), values: z.array(z.number()).max(365) },
|
|
199
|
+
toMarkdown: (p) => `${inline(p.name)}: ${p.values.map(formatNumber).join(', ')}`,
|
|
200
|
+
});
|
|
201
|
+
const chartA11y = (kind) => (p) => `${kind}: ${nodes(p.series)
|
|
202
|
+
.map((s) => s.props.name)
|
|
203
|
+
.join(', ')}. Source: ${p.source}`;
|
|
204
|
+
component({
|
|
205
|
+
name: 'BarChart',
|
|
206
|
+
description: 'Amounts across up to 24 categories, up to 4 series. source names where the data came from',
|
|
207
|
+
fields: {
|
|
208
|
+
categories: z.array(text(40)).min(1).max(24),
|
|
209
|
+
series: { accepts: ['Series'], min: 1, max: 4 },
|
|
210
|
+
source: text(200),
|
|
211
|
+
unit: optText(12),
|
|
212
|
+
},
|
|
213
|
+
toMarkdown: (p) => `${seriesTable(p.categories, nodes(p.series), p.unit)}\n\n${sourceLine(p.source)}`,
|
|
214
|
+
a11y: chartA11y('Bar chart'),
|
|
215
|
+
});
|
|
216
|
+
component({
|
|
217
|
+
name: 'LineChart',
|
|
218
|
+
description: 'A trend over time: x labels, up to 4 series. source names where the data came from',
|
|
219
|
+
fields: {
|
|
220
|
+
x: z.array(text(40)).min(2).max(365),
|
|
221
|
+
series: { accepts: ['Series'], min: 1, max: 4 },
|
|
222
|
+
source: text(200),
|
|
223
|
+
unit: optText(12),
|
|
224
|
+
},
|
|
225
|
+
toMarkdown: (p) => `${seriesTable(p.x, nodes(p.series), p.unit)}\n\n${sourceLine(p.source)}`,
|
|
226
|
+
a11y: chartA11y('Line chart'),
|
|
227
|
+
});
|
|
228
|
+
component({
|
|
229
|
+
name: 'Slice',
|
|
230
|
+
description: 'One part of a PieChart',
|
|
231
|
+
block: false,
|
|
232
|
+
fields: { label: text(40), value: z.number().nonnegative() },
|
|
233
|
+
toMarkdown: (p) => `${inline(p.label)}: ${formatNumber(p.value)}`,
|
|
234
|
+
});
|
|
235
|
+
component({
|
|
236
|
+
name: 'PieChart',
|
|
237
|
+
description: 'Shares of a whole, 2 to 6 slices. source names where the data came from. variant picks the layout: ' +
|
|
238
|
+
'donut: the total and every share (default). focus: the answer is about the first slice. ' +
|
|
239
|
+
'ring: one share dominates and is the headline. treemap: 4 to 6 slices, sizes matter more than the total. ' +
|
|
240
|
+
'gauge: 2 or 3 slices, like used against left. stack: a receipt, exact values to read',
|
|
241
|
+
fields: {
|
|
242
|
+
slices: { accepts: ['Slice'], min: 2, max: 6 },
|
|
243
|
+
source: text(200),
|
|
244
|
+
unit: optText(12),
|
|
245
|
+
variant: z.enum(['donut', 'focus', 'ring', 'treemap', 'gauge', 'stack']).optional(),
|
|
246
|
+
},
|
|
247
|
+
toMarkdown: (p) => {
|
|
248
|
+
const slices = nodes(p.slices);
|
|
249
|
+
const total = slices.reduce((sum, s) => sum + (Number(s.props.value) || 0), 0);
|
|
250
|
+
const lines = slices.map((s) => {
|
|
251
|
+
const value = Number(s.props.value) || 0;
|
|
252
|
+
const percent = total > 0 ? Math.round((value / total) * 100) : 0;
|
|
253
|
+
return `- ${inline(s.props.label)}: ${withUnit(formatNumber(value), p.unit)} (${percent}%)`;
|
|
254
|
+
});
|
|
255
|
+
return `${lines.join('\n')}\n\n${sourceLine(p.source)}`;
|
|
256
|
+
},
|
|
257
|
+
a11y: (p) => `Pie chart: ${nodes(p.slices)
|
|
258
|
+
.map((s) => `${s.props.label} ${formatNumber(s.props.value)}`)
|
|
259
|
+
.join(', ')}. Source: ${p.source}`,
|
|
260
|
+
});
|
|
261
|
+
component({
|
|
262
|
+
name: 'Marker',
|
|
263
|
+
description: 'One place on a Map. Coordinates only from tool results or files. description is its address or a short note. ' +
|
|
264
|
+
'value is the short figure in its map pin: a rating, a price, a population. href is its own page, image its photo, ' +
|
|
265
|
+
'phone its phone number, kind what it is (Hotel, Cafe, Museum)',
|
|
266
|
+
block: false,
|
|
267
|
+
fields: {
|
|
268
|
+
lat: z.number().min(-90).max(90),
|
|
269
|
+
lng: z.number().min(-180).max(180),
|
|
270
|
+
label: text(60),
|
|
271
|
+
description: optText(200),
|
|
272
|
+
value: optText(24),
|
|
273
|
+
href: url().optional(),
|
|
274
|
+
image: url().optional(),
|
|
275
|
+
phone: optText(30),
|
|
276
|
+
kind: optText(30),
|
|
277
|
+
},
|
|
278
|
+
urls: { href: 'optional', image: 'optional' },
|
|
279
|
+
toMarkdown: (p) => `**${inline(p.label)}**${p.value ? ` (${inline(p.value)})` : ''}${p.description ? ` — ${inline(p.description)}` : ''} ([map](${osmLink(p.lat, p.lng)}))`,
|
|
280
|
+
});
|
|
281
|
+
component({
|
|
282
|
+
name: 'Map',
|
|
283
|
+
description: 'Up to 25 places, zoom 1-18, then an optional route as [lat, lng] pairs. source names where the coordinates came from',
|
|
284
|
+
fields: {
|
|
285
|
+
markers: { accepts: ['Marker'], min: 1, max: 25 },
|
|
286
|
+
source: text(200),
|
|
287
|
+
zoom: z.number().int().min(1).max(18).optional(),
|
|
288
|
+
route: z.array(z.array(z.number()).length(2)).max(500).optional(),
|
|
289
|
+
},
|
|
290
|
+
toMarkdown: (p, child) => `${nodes(p.markers).map((m) => `- ${child(m)}`).join('\n')}\n\n${sourceLine(p.source)}`,
|
|
291
|
+
a11y: (p) => `Map with ${nodes(p.markers).length} places: ${nodes(p.markers)
|
|
292
|
+
.map((m) => m.props.label)
|
|
293
|
+
.join(', ')}. Source: ${p.source}`,
|
|
294
|
+
});
|
|
295
|
+
component({
|
|
296
|
+
name: 'Card',
|
|
297
|
+
description: 'One item: a hotel, product, place, or person. subtitle is a short location or kind line. ' +
|
|
298
|
+
'rating is 0 to 5, reviews is how many, price is a short text. 2 or more Cards in a Stack show as a grid or a list',
|
|
299
|
+
fields: {
|
|
300
|
+
title: text(80),
|
|
301
|
+
body: optText(600),
|
|
302
|
+
subtitle: optText(120),
|
|
303
|
+
image: url().optional(),
|
|
304
|
+
href: url().optional(),
|
|
305
|
+
badges: { accepts: ['Badge'], min: 0, max: 4, optional: true },
|
|
306
|
+
rating: z.number().min(0).max(5).optional(),
|
|
307
|
+
reviews: z.number().int().nonnegative().optional(),
|
|
308
|
+
price: optText(40),
|
|
309
|
+
},
|
|
310
|
+
urls: { image: 'optional', href: 'optional' },
|
|
311
|
+
toMarkdown: (p, child) => [
|
|
312
|
+
`### ${inline(p.title)}`,
|
|
313
|
+
p.rating != null ? `★ ${p.rating}${p.reviews != null ? ` (${formatNumber(p.reviews)})` : ''}` : '',
|
|
314
|
+
p.subtitle ? `*${inline(p.subtitle)}*` : '',
|
|
315
|
+
p.price ? inline(p.price) : '',
|
|
316
|
+
p.body ? inline(p.body) : '',
|
|
317
|
+
joinChildren(p.badges, child, ' '),
|
|
318
|
+
p.image ? `})` : '',
|
|
319
|
+
p.href ? `[Open](${target(p.href)})` : '',
|
|
320
|
+
]
|
|
321
|
+
.filter(Boolean)
|
|
322
|
+
.join('\n\n'),
|
|
323
|
+
});
|
|
324
|
+
component({
|
|
325
|
+
name: 'Step',
|
|
326
|
+
description: 'One row inside Steps: a leg or action, what to do, its photo, and a time or cost',
|
|
327
|
+
block: false,
|
|
328
|
+
fields: { title: text(80), text: optText(500), image: url().optional(), meta: optText(40) },
|
|
329
|
+
urls: { image: 'optional' },
|
|
330
|
+
toMarkdown: (p) => `**${inline(p.title)}**${p.text ? ` — ${inline(p.text)}` : ''}${p.meta ? ` (${inline(p.meta)})` : ''}`,
|
|
331
|
+
});
|
|
332
|
+
component({
|
|
333
|
+
name: 'Steps',
|
|
334
|
+
description: 'A journey or a process in order: up to 12 rows, each with its photo, joined by a line',
|
|
335
|
+
fields: { steps: { accepts: ['Step'], min: 1, max: 12 } },
|
|
336
|
+
toMarkdown: (p, child) => nodes(p.steps).map((step, i) => `${i + 1}. ${child(step)}`).join('\n'),
|
|
337
|
+
});
|
|
338
|
+
component({
|
|
339
|
+
name: 'Checklist',
|
|
340
|
+
description: 'Things to pack or gather: a title and up to 20 items, each under 120 characters, the reader ticks off',
|
|
341
|
+
fields: { title: text(80), items: z.array(text(120)).min(1).max(20) },
|
|
342
|
+
toMarkdown: (p) => `**${inline(p.title)}**\n\n${p.items.map((item) => `- [ ] ${inline(item)}`).join('\n')}`,
|
|
343
|
+
});
|
|
344
|
+
export const GENUI_BLOCKS = [
|
|
345
|
+
'Stack', 'Card', 'Stat', 'StatRow', 'Table', 'Compare', 'RankedList', 'BarChart', 'LineChart',
|
|
346
|
+
'PieChart', 'Map', 'Steps', 'Checklist', 'Tabs', 'Accordion', 'Badge', 'Callout', 'Image', 'Link',
|
|
347
|
+
];
|
|
348
|
+
const INNER_BLOCKS = GENUI_BLOCKS.filter((name) => name !== 'Tabs' && name !== 'Accordion');
|
|
349
|
+
component({
|
|
350
|
+
name: 'Tab',
|
|
351
|
+
description: 'One tab inside Tabs: a label and its blocks',
|
|
352
|
+
block: false,
|
|
353
|
+
fields: { label: text(30), children: { accepts: INNER_BLOCKS, min: 1, max: 8, lazy: true } },
|
|
354
|
+
toMarkdown: (p, child) => `#### ${inline(p.label)}\n\n${joinChildren(p.children, child)}`,
|
|
355
|
+
});
|
|
356
|
+
component({
|
|
357
|
+
name: 'Tabs',
|
|
358
|
+
description: '2 to 5 tabs. Use when content splits into views the user switches between',
|
|
359
|
+
container: true,
|
|
360
|
+
fields: { tabs: { accepts: ['Tab'], min: 2, max: 5 } },
|
|
361
|
+
toMarkdown: (p, child) => joinChildren(p.tabs, child),
|
|
362
|
+
});
|
|
363
|
+
component({
|
|
364
|
+
name: 'AccordionItem',
|
|
365
|
+
description: 'One collapsible section inside Accordion',
|
|
366
|
+
block: false,
|
|
367
|
+
fields: { title: text(80), children: { accepts: INNER_BLOCKS, min: 1, max: 8, lazy: true } },
|
|
368
|
+
toMarkdown: (p, child) => `#### ${inline(p.title)}\n\n${joinChildren(p.children, child)}`,
|
|
369
|
+
});
|
|
370
|
+
component({
|
|
371
|
+
name: 'Accordion',
|
|
372
|
+
description: 'Up to 10 collapsible sections, for details a reader may skip',
|
|
373
|
+
container: true,
|
|
374
|
+
fields: { items: { accepts: ['AccordionItem'], min: 1, max: 10 } },
|
|
375
|
+
toMarkdown: (p, child) => joinChildren(p.items, child),
|
|
376
|
+
});
|
|
377
|
+
component({
|
|
378
|
+
name: 'Stack',
|
|
379
|
+
description: 'Layout: 2 or more blocks in one block. children: 2 to 12 blocks. direction: col (default) or row. ' +
|
|
380
|
+
'layout, for 2 or more Cards: grid (big images) or list (small thumbnails); empty lets the app choose from the images',
|
|
381
|
+
container: true,
|
|
382
|
+
fields: {
|
|
383
|
+
children: { accepts: GENUI_BLOCKS, min: 1, max: 12, lazy: true },
|
|
384
|
+
direction: z.enum(['col', 'row']).optional(),
|
|
385
|
+
layout: z.enum(['list', 'grid']).optional(),
|
|
386
|
+
},
|
|
387
|
+
toMarkdown: (p, child) => joinChildren(p.children, child),
|
|
388
|
+
});
|
|
389
|
+
export const GENUI_SPECS = specs;
|
|
390
|
+
export const GENUI_LIBRARY = createLibrary({
|
|
391
|
+
components: Object.values(defined),
|
|
392
|
+
root: 'Stack',
|
|
393
|
+
});
|
|
394
|
+
export const GENUI_MAX_DEPTH = 4;
|
|
395
|
+
export const GENUI_MAX_NODES = 500;
|
|
396
|
+
export const GENUI_MAX_SOURCE_CHARS = 64 * 1024;
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { autoClose, parseExpression, split, tokenize, walkAST } from '@openuidev/lang-core';
|
|
2
|
+
import { GENUI_MAX_NODES, GENUI_MAX_SOURCE_CHARS } from './catalog.js';
|
|
3
|
+
const MAX_NESTING = 64;
|
|
4
|
+
const MAX_EXPANDED_VALUES = 50_000;
|
|
5
|
+
function skipString(input, start) {
|
|
6
|
+
if (input[start] !== '"')
|
|
7
|
+
return start;
|
|
8
|
+
let i = start + 1;
|
|
9
|
+
while (i < input.length) {
|
|
10
|
+
const c = input[i];
|
|
11
|
+
if (c === '\\')
|
|
12
|
+
i += 2;
|
|
13
|
+
else if (c === '"')
|
|
14
|
+
return i + 1;
|
|
15
|
+
else
|
|
16
|
+
i++;
|
|
17
|
+
}
|
|
18
|
+
return i;
|
|
19
|
+
}
|
|
20
|
+
const isFence = (input, i) => input[i] === '`' && input[i + 1] === '`' && input[i + 2] === '`';
|
|
21
|
+
function stripFences(input) {
|
|
22
|
+
const blocks = [];
|
|
23
|
+
let i = 0;
|
|
24
|
+
while (i < input.length) {
|
|
25
|
+
let fenceStart = -1;
|
|
26
|
+
while (i < input.length) {
|
|
27
|
+
const next = skipString(input, i);
|
|
28
|
+
if (next > i) {
|
|
29
|
+
i = next;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (isFence(input, i)) {
|
|
33
|
+
fenceStart = i;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
i++;
|
|
37
|
+
}
|
|
38
|
+
if (fenceStart === -1)
|
|
39
|
+
break;
|
|
40
|
+
let j = fenceStart + 3;
|
|
41
|
+
while (j < input.length && input[j] !== '\n')
|
|
42
|
+
j++;
|
|
43
|
+
if (j >= input.length) {
|
|
44
|
+
blocks.push(input.slice(fenceStart + 3).replace(/^[^\n]*\n?/, ''));
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
j++;
|
|
48
|
+
let closePos = -1;
|
|
49
|
+
let k = j;
|
|
50
|
+
while (k < input.length) {
|
|
51
|
+
const next = skipString(input, k);
|
|
52
|
+
if (next > k) {
|
|
53
|
+
k = next;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (isFence(input, k)) {
|
|
57
|
+
closePos = k;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
k++;
|
|
61
|
+
}
|
|
62
|
+
if (closePos === -1) {
|
|
63
|
+
blocks.push(input.slice(j));
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
blocks.push(input.slice(j, closePos));
|
|
67
|
+
i = closePos + 3;
|
|
68
|
+
}
|
|
69
|
+
if (blocks.length > 0)
|
|
70
|
+
return blocks.join('\n');
|
|
71
|
+
if (input.startsWith('```')) {
|
|
72
|
+
let j = 3;
|
|
73
|
+
while (j < input.length && input[j] !== '\n')
|
|
74
|
+
j++;
|
|
75
|
+
const body = input.slice(j < input.length ? j + 1 : 3);
|
|
76
|
+
const trailing = body.lastIndexOf('```');
|
|
77
|
+
return trailing === -1 ? body : body.slice(0, trailing);
|
|
78
|
+
}
|
|
79
|
+
return input;
|
|
80
|
+
}
|
|
81
|
+
function stripComments(input) {
|
|
82
|
+
let inStr = false;
|
|
83
|
+
return input
|
|
84
|
+
.split('\n')
|
|
85
|
+
.map((line) => {
|
|
86
|
+
for (let i = 0; i < line.length; i++) {
|
|
87
|
+
const c = line[i];
|
|
88
|
+
if (inStr) {
|
|
89
|
+
if (c === '\\' && i + 1 < line.length) {
|
|
90
|
+
i++;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (c === inStr)
|
|
94
|
+
inStr = false;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (c === '"' || c === "'") {
|
|
98
|
+
inStr = c;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (c === '/' && line[i + 1] === '/')
|
|
102
|
+
return line.substring(0, i).trimEnd();
|
|
103
|
+
if (c === '#')
|
|
104
|
+
return line.substring(0, i).trimEnd();
|
|
105
|
+
}
|
|
106
|
+
return line;
|
|
107
|
+
})
|
|
108
|
+
.join('\n');
|
|
109
|
+
}
|
|
110
|
+
const preprocess = (input) => stripComments(stripFences(input.trim())).trim();
|
|
111
|
+
function bracketDepth(text) {
|
|
112
|
+
let depth = 0;
|
|
113
|
+
let max = 0;
|
|
114
|
+
let quote = '';
|
|
115
|
+
let escaped = false;
|
|
116
|
+
for (const c of text) {
|
|
117
|
+
if (quote) {
|
|
118
|
+
if (escaped)
|
|
119
|
+
escaped = false;
|
|
120
|
+
else if (c === '\\')
|
|
121
|
+
escaped = true;
|
|
122
|
+
else if (c === quote)
|
|
123
|
+
quote = '';
|
|
124
|
+
}
|
|
125
|
+
else if (c === '"' || c === "'")
|
|
126
|
+
quote = c;
|
|
127
|
+
else if (c === '(' || c === '[' || c === '{') {
|
|
128
|
+
max = Math.max(max, ++depth);
|
|
129
|
+
if (max > MAX_NESTING)
|
|
130
|
+
return max;
|
|
131
|
+
}
|
|
132
|
+
else if (c === ')' || c === ']' || c === '}')
|
|
133
|
+
depth = Math.max(0, depth - 1);
|
|
134
|
+
}
|
|
135
|
+
return max;
|
|
136
|
+
}
|
|
137
|
+
function splitStatements(cleaned) {
|
|
138
|
+
const completed = [];
|
|
139
|
+
let depth = 0;
|
|
140
|
+
let ternary = 0;
|
|
141
|
+
let quote = '';
|
|
142
|
+
let escaped = false;
|
|
143
|
+
let start = 0;
|
|
144
|
+
for (let i = 0; i < cleaned.length; i++) {
|
|
145
|
+
const c = cleaned[i];
|
|
146
|
+
if (escaped) {
|
|
147
|
+
escaped = false;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (c === '\\' && quote) {
|
|
151
|
+
escaped = true;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (quote) {
|
|
155
|
+
if (c === quote)
|
|
156
|
+
quote = '';
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (c === '"' || c === "'")
|
|
160
|
+
quote = c;
|
|
161
|
+
else if (c === '(' || c === '[' || c === '{')
|
|
162
|
+
depth++;
|
|
163
|
+
else if (c === ')' || c === ']' || c === '}')
|
|
164
|
+
depth = Math.max(0, depth - 1);
|
|
165
|
+
else if (c === '?' && depth === 0)
|
|
166
|
+
ternary++;
|
|
167
|
+
else if (c === ':' && depth === 0 && ternary > 0)
|
|
168
|
+
ternary--;
|
|
169
|
+
else if (c === '\n' && depth <= 0 && ternary <= 0) {
|
|
170
|
+
let peek = i + 1;
|
|
171
|
+
while (peek < cleaned.length && ' \t\r\n'.includes(cleaned[peek]))
|
|
172
|
+
peek++;
|
|
173
|
+
if (peek < cleaned.length && (cleaned[peek] === '?' || (cleaned[peek] === ':' && ternary > 0)))
|
|
174
|
+
continue;
|
|
175
|
+
const text = cleaned.slice(start, i).trim();
|
|
176
|
+
if (text)
|
|
177
|
+
completed.push(text);
|
|
178
|
+
start = i + 1;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return { completed, pending: cleaned.slice(start).trim() };
|
|
182
|
+
}
|
|
183
|
+
function statementsOf(cleaned) {
|
|
184
|
+
const statements = new Map();
|
|
185
|
+
const add = (text, skipCompleted) => {
|
|
186
|
+
const completed = skipCompleted ? new Set(statements.keys()) : null;
|
|
187
|
+
for (const raw of split(tokenize(text))) {
|
|
188
|
+
if (completed?.has(raw.id))
|
|
189
|
+
continue;
|
|
190
|
+
statements.set(raw.id, { ast: parseExpression(raw.tokens), tokens: raw.tokens });
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
const { completed, pending } = splitStatements(cleaned);
|
|
194
|
+
for (const text of completed)
|
|
195
|
+
add(text, false);
|
|
196
|
+
if (pending) {
|
|
197
|
+
const { text, wasIncomplete } = autoClose(pending);
|
|
198
|
+
add(text, wasIncomplete);
|
|
199
|
+
}
|
|
200
|
+
return statements;
|
|
201
|
+
}
|
|
202
|
+
export function pendingStatementId(code) {
|
|
203
|
+
const { pending } = splitStatements(preprocess(code));
|
|
204
|
+
if (!pending)
|
|
205
|
+
return null;
|
|
206
|
+
return split(tokenize(autoClose(pending).text)).at(-1)?.id ?? null;
|
|
207
|
+
}
|
|
208
|
+
const OPEN = new Set([1, 3, 5]);
|
|
209
|
+
const CLOSE = new Set([2, 4, 6]);
|
|
210
|
+
export function expansionIssue(code) {
|
|
211
|
+
if (code.length > GENUI_MAX_SOURCE_CHARS) {
|
|
212
|
+
return { code: 'too-large', message: `Block source is longer than ${GENUI_MAX_SOURCE_CHARS} characters` };
|
|
213
|
+
}
|
|
214
|
+
const tooDeep = { code: 'depth', message: `Brackets or references nest deeper than ${MAX_NESTING}` };
|
|
215
|
+
const tooMany = (what) => ({ code: 'too-many-nodes', message: `${what}; the block is not rendered` });
|
|
216
|
+
const cleaned = preprocess(code);
|
|
217
|
+
if (bracketDepth(cleaned) > MAX_NESTING)
|
|
218
|
+
return tooDeep;
|
|
219
|
+
const own = new Map();
|
|
220
|
+
for (const [id, { ast, tokens }] of statementsOf(cleaned)) {
|
|
221
|
+
let elements = 0;
|
|
222
|
+
let values = 0;
|
|
223
|
+
const refs = [];
|
|
224
|
+
walkAST(ast, (node) => {
|
|
225
|
+
values++;
|
|
226
|
+
if (node.k === 'Comp')
|
|
227
|
+
elements++;
|
|
228
|
+
else if (node.k === 'Ref' || node.k === 'RuntimeRef')
|
|
229
|
+
refs.push(node.n);
|
|
230
|
+
});
|
|
231
|
+
let depth = 0;
|
|
232
|
+
let max = 0;
|
|
233
|
+
for (const token of tokens) {
|
|
234
|
+
if (OPEN.has(token.t))
|
|
235
|
+
max = Math.max(max, ++depth);
|
|
236
|
+
else if (CLOSE.has(token.t))
|
|
237
|
+
depth--;
|
|
238
|
+
}
|
|
239
|
+
own.set(id, { elements, values, depth: max, refs });
|
|
240
|
+
}
|
|
241
|
+
const nodeCap = GENUI_MAX_NODES + 1;
|
|
242
|
+
const valueCap = MAX_EXPANDED_VALUES + 1;
|
|
243
|
+
const sizes = new Map();
|
|
244
|
+
const path = new Set();
|
|
245
|
+
let cycle = false;
|
|
246
|
+
const measure = (id) => {
|
|
247
|
+
const known = sizes.get(id);
|
|
248
|
+
if (known)
|
|
249
|
+
return known;
|
|
250
|
+
if (path.size > MAX_NESTING)
|
|
251
|
+
return { elements: 0, values: 0, depth: MAX_NESTING + 1 };
|
|
252
|
+
path.add(id);
|
|
253
|
+
const statement = own.get(id);
|
|
254
|
+
let { elements, values } = statement;
|
|
255
|
+
let below = 0;
|
|
256
|
+
for (const ref of statement.refs) {
|
|
257
|
+
if (!own.has(ref))
|
|
258
|
+
continue;
|
|
259
|
+
if (path.has(ref)) {
|
|
260
|
+
cycle = true;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const child = measure(ref);
|
|
264
|
+
elements = Math.min(nodeCap, elements + child.elements);
|
|
265
|
+
values = Math.min(valueCap, values + child.values);
|
|
266
|
+
below = Math.max(below, child.depth);
|
|
267
|
+
}
|
|
268
|
+
path.delete(id);
|
|
269
|
+
const size = { elements, values, depth: statement.depth + 1 + below };
|
|
270
|
+
sizes.set(id, size);
|
|
271
|
+
return size;
|
|
272
|
+
};
|
|
273
|
+
let stateElements = 0;
|
|
274
|
+
let stateValues = 0;
|
|
275
|
+
for (const id of own.keys()) {
|
|
276
|
+
const size = measure(id);
|
|
277
|
+
if (id.startsWith('$')) {
|
|
278
|
+
stateElements = Math.min(nodeCap, stateElements + size.elements);
|
|
279
|
+
stateValues = Math.min(valueCap, stateValues + size.values);
|
|
280
|
+
}
|
|
281
|
+
if (cycle)
|
|
282
|
+
return tooMany('Block references form a cycle');
|
|
283
|
+
if (stateElements >= nodeCap)
|
|
284
|
+
return tooMany(`$state statements expand to more than ${GENUI_MAX_NODES} components`);
|
|
285
|
+
if (stateValues >= valueCap)
|
|
286
|
+
return tooMany(`$state statements expand to more than ${MAX_EXPANDED_VALUES} values`);
|
|
287
|
+
if (size.depth > MAX_NESTING)
|
|
288
|
+
return tooDeep;
|
|
289
|
+
if (size.elements >= nodeCap)
|
|
290
|
+
return tooMany(`Block expands to more than ${GENUI_MAX_NODES} components`);
|
|
291
|
+
if (size.values >= valueCap)
|
|
292
|
+
return tooMany(`Block expands to more than ${MAX_EXPANDED_VALUES} values`);
|
|
293
|
+
}
|
|
294
|
+
return null;
|
|
295
|
+
}
|