@artkit-ai/format-openui 0.0.0-stage → 0.2.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/LICENSE +202 -0
- package/README.md +4 -2
- package/dist/diff.d.ts +4 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +24 -0
- package/dist/diff.js.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +202 -0
- package/dist/index.js.map +1 -0
- package/dist/lint.d.ts +4 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +81 -0
- package/dist/lint.js.map +1 -0
- package/dist/operations.d.ts +41 -0
- package/dist/operations.d.ts.map +1 -0
- package/dist/operations.js +140 -0
- package/dist/operations.js.map +1 -0
- package/dist/read.d.ts +4 -0
- package/dist/read.d.ts.map +1 -0
- package/dist/read.js +58 -0
- package/dist/read.js.map +1 -0
- package/dist/syntax.d.ts +68 -0
- package/dist/syntax.d.ts.map +1 -0
- package/dist/syntax.js +204 -0
- package/dist/syntax.js.map +1 -0
- package/package.json +46 -3
- package/src/diff.ts +22 -0
- package/src/index.ts +221 -0
- package/src/lint.ts +86 -0
- package/src/operations.ts +172 -0
- package/src/read.ts +59 -0
- package/src/syntax.ts +226 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AdapterContext,
|
|
3
|
+
type ApplyResult,
|
|
4
|
+
type ChangeSummary,
|
|
5
|
+
defineFormat,
|
|
6
|
+
type FormatAdapter,
|
|
7
|
+
type FormatInstructions,
|
|
8
|
+
type LintIssue,
|
|
9
|
+
type ReadRequest,
|
|
10
|
+
type ReadResult,
|
|
11
|
+
type ValidationResult,
|
|
12
|
+
} from '@artkit-ai/core';
|
|
13
|
+
import { uiComponents, uiLibrary, VISUAL_ROOTS } from '@artkit-ai/openui-components';
|
|
14
|
+
import { createParser } from '@openuidev/lang-core';
|
|
15
|
+
import { diffOpenUI } from './diff.js';
|
|
16
|
+
import { lintOpenUI } from './lint.js';
|
|
17
|
+
import { applyOpenUIOperation, type OpenUIOperation, openuiOperationSchema } from './operations.js';
|
|
18
|
+
import { readOpenUI } from './read.js';
|
|
19
|
+
import { componentOf, parseStatements, references, type Statement, serializeStatements } from './syntax.js';
|
|
20
|
+
|
|
21
|
+
export { diffOpenUI } from './diff.js';
|
|
22
|
+
export { lintOpenUI } from './lint.js';
|
|
23
|
+
export * from './operations.js';
|
|
24
|
+
export { readOpenUI } from './read.js';
|
|
25
|
+
export * from './syntax.js';
|
|
26
|
+
|
|
27
|
+
export const OPENUI_LIMITS = { declarations: 2000, slides: 100 };
|
|
28
|
+
export const OPENUI_FORMAT_VERSION = 'openui-lang@1.1.0';
|
|
29
|
+
|
|
30
|
+
export const OPENUI_INSTRUCTIONS: FormatInstructions = {
|
|
31
|
+
summary:
|
|
32
|
+
'OpenUI Lang for visuals: slide decks, dashboards, reports, prototypes and diagrams built from the artkit component library. One declaration per line; the first declaration is root = <Visual>(...).',
|
|
33
|
+
rules: [
|
|
34
|
+
'Start with root = Deck(...) for presentations, Dashboard, Report, Prototype or Diagram for the other visuals.',
|
|
35
|
+
'Declare each component on its own line with a lowercase name, then reference it from a parent by that name.',
|
|
36
|
+
'Use only components listed in the catalog with the positional arguments of their schema.',
|
|
37
|
+
'Give every slide a Title first; keep Bullets to six items or fewer.',
|
|
38
|
+
'Give every Image an alternative text as its second argument and an asset://<id> source.',
|
|
39
|
+
'Edit visuals by declaration: set_statement, insert_child, move_child, rename_statement; never rewrite the whole text.',
|
|
40
|
+
],
|
|
41
|
+
examples: [
|
|
42
|
+
{
|
|
43
|
+
title: 'Two-slide deck',
|
|
44
|
+
operations: [
|
|
45
|
+
{ op: 'set_statement', name: 'root', expr: 'Deck([capa, escopo])' },
|
|
46
|
+
{
|
|
47
|
+
op: 'set_statement',
|
|
48
|
+
name: 'capa',
|
|
49
|
+
expr: 'Slide("title", [Title("Portal de Fornecedores"), Subtitle("Grupo Meridiano")])',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
op: 'set_statement',
|
|
53
|
+
name: 'escopo',
|
|
54
|
+
expr: 'Slide("title-content", [Title("Escopo em 13 módulos"), Bullets(["Cadastro e homologação", "Integração Protheus"])])',
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const COMPONENT_NAMES = new Set(uiComponents.map((component) => component.name));
|
|
62
|
+
|
|
63
|
+
function cycles(statements: readonly Statement[]): string[] {
|
|
64
|
+
const byName = new Map(statements.map((statement) => [statement.name, statement]));
|
|
65
|
+
const state = new Map<string, 'visiting' | 'done'>();
|
|
66
|
+
const found: string[] = [];
|
|
67
|
+
const visit = (name: string) => {
|
|
68
|
+
if (state.get(name) === 'done') return;
|
|
69
|
+
if (state.get(name) === 'visiting') {
|
|
70
|
+
found.push(name);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
state.set(name, 'visiting');
|
|
74
|
+
for (const ref of references(byName.get(name)?.expr ?? '')) if (byName.has(ref)) visit(ref);
|
|
75
|
+
state.set(name, 'done');
|
|
76
|
+
};
|
|
77
|
+
for (const statement of statements) visit(statement.name);
|
|
78
|
+
return found;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Validates OpenUI text: syntax, root, component names, references, cycles and the parser of OpenUI itself. */
|
|
82
|
+
export function validateOpenUI(text: string): ValidationResult<string> {
|
|
83
|
+
const { statements, invalidLines } = parseStatements(text);
|
|
84
|
+
const issues: { path: string; message: string; hint?: string }[] = [];
|
|
85
|
+
for (const line of invalidLines) {
|
|
86
|
+
issues.push({
|
|
87
|
+
path: `content.line${line}`,
|
|
88
|
+
message: `A linha ${line} não é uma declaração.`,
|
|
89
|
+
hint: 'Use o formato nome = Componente(argumentos), uma por linha.',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const seen = new Set<string>();
|
|
93
|
+
for (const statement of statements) {
|
|
94
|
+
if (seen.has(statement.name)) {
|
|
95
|
+
issues.push({
|
|
96
|
+
path: `content.${statement.name}`,
|
|
97
|
+
message: `A declaração "${statement.name}" está repetida.`,
|
|
98
|
+
hint: 'Cada nome precisa ser único.',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
seen.add(statement.name);
|
|
102
|
+
const component = componentOf(statement.expr);
|
|
103
|
+
if (!component || !COMPONENT_NAMES.has(component)) {
|
|
104
|
+
issues.push({
|
|
105
|
+
path: `content.${statement.name}`,
|
|
106
|
+
message: `"${component ?? statement.expr}" não é um componente da biblioteca.`,
|
|
107
|
+
hint: `Use um destes: ${[...COMPONENT_NAMES].slice(0, 12).join(', ')}, …`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const root = statements.find((statement) => statement.name === 'root');
|
|
112
|
+
if (!root) {
|
|
113
|
+
issues.push({
|
|
114
|
+
path: 'content.root',
|
|
115
|
+
message: 'Falta a declaração root.',
|
|
116
|
+
hint: 'Comece com root = Deck(...) ou outro visual.',
|
|
117
|
+
});
|
|
118
|
+
} else if (!VISUAL_ROOTS.includes(componentOf(root.expr) as never)) {
|
|
119
|
+
issues.push({
|
|
120
|
+
path: 'content.root',
|
|
121
|
+
message: `A raiz "${componentOf(root.expr)}" não é um visual.`,
|
|
122
|
+
hint: `Use uma destas raízes: ${VISUAL_ROOTS.join(', ')}.`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
for (const name of cycles(statements)) {
|
|
126
|
+
issues.push({
|
|
127
|
+
path: `content.${name}`,
|
|
128
|
+
message: `A declaração "${name}" referencia a si mesma em um ciclo.`,
|
|
129
|
+
hint: 'Remova a referência circular.',
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (issues.length === 0 && root) {
|
|
133
|
+
const result = createParser(uiLibrary.toJSONSchema(), uiLibrary.root).parse(
|
|
134
|
+
serializeStatements(statements),
|
|
135
|
+
);
|
|
136
|
+
for (const error of result.meta.errors) {
|
|
137
|
+
issues.push({
|
|
138
|
+
path: `content.${error.statementId ?? 'root'}`,
|
|
139
|
+
message: error.message,
|
|
140
|
+
hint: 'Confira o componente e os argumentos no catálogo.',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
for (const name of result.meta.unresolved) {
|
|
144
|
+
issues.push({
|
|
145
|
+
path: `content.${name}`,
|
|
146
|
+
message: `"${name}" é usada mas não foi declarada.`,
|
|
147
|
+
hint: 'Declare a referência em uma linha própria.',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (issues.length > 0) return { ok: false, issues };
|
|
152
|
+
return { ok: true, content: serializeStatements(statements) };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** OpenUI format adapter (spec §12), validated against the artkit component library. */
|
|
156
|
+
export function createOpenUIFormat(): FormatAdapter<string, OpenUIOperation> {
|
|
157
|
+
return defineFormat({
|
|
158
|
+
format: 'openui',
|
|
159
|
+
formatVersion: OPENUI_FORMAT_VERSION,
|
|
160
|
+
readableVersions: [OPENUI_FORMAT_VERSION],
|
|
161
|
+
label: 'Visual',
|
|
162
|
+
empty: () => 'root = Deck([])\n',
|
|
163
|
+
validate(content: unknown, _ctx: AdapterContext): ValidationResult<string> {
|
|
164
|
+
if (typeof content !== 'string') {
|
|
165
|
+
return {
|
|
166
|
+
ok: false,
|
|
167
|
+
issues: [
|
|
168
|
+
{
|
|
169
|
+
path: 'content',
|
|
170
|
+
message: 'O visual deve ser texto OpenUI Lang.',
|
|
171
|
+
hint: 'Envie as declarações como texto.',
|
|
172
|
+
},
|
|
173
|
+
],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return validateOpenUI(content);
|
|
177
|
+
},
|
|
178
|
+
operationSchema: openuiOperationSchema,
|
|
179
|
+
async apply(
|
|
180
|
+
content: string,
|
|
181
|
+
operations: OpenUIOperation[],
|
|
182
|
+
ctx: AdapterContext,
|
|
183
|
+
): Promise<ApplyResult<string>> {
|
|
184
|
+
const state = { statements: parseStatements(content).statements, createdRefs: [] as string[] };
|
|
185
|
+
for (const op of operations) applyOpenUIOperation(state, op, ctx);
|
|
186
|
+
const text = serializeStatements(state.statements);
|
|
187
|
+
const validation = validateOpenUI(text);
|
|
188
|
+
if (!validation.ok)
|
|
189
|
+
ctx.fail(validation.issues[0]?.message ?? 'Visual inválido.', { hint: validation.issues[0]?.hint });
|
|
190
|
+
return {
|
|
191
|
+
content: validation.content,
|
|
192
|
+
createdRefs: state.createdRefs,
|
|
193
|
+
warnings: lintOpenUI(validation.content),
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
read(content: string, request: ReadRequest): ReadResult {
|
|
197
|
+
return readOpenUI(content, request);
|
|
198
|
+
},
|
|
199
|
+
diff: (before: string, after: string): ChangeSummary[] => diffOpenUI(before, after),
|
|
200
|
+
lint: (content: string): LintIssue[] => lintOpenUI(content),
|
|
201
|
+
parsePartial(raw: string): string | undefined {
|
|
202
|
+
if (raw.trim() === '') return undefined;
|
|
203
|
+
// Keep only complete lines: the last line may still be streaming.
|
|
204
|
+
const lines = raw.split('\n');
|
|
205
|
+
const complete = raw.endsWith('\n') ? lines : lines.slice(0, -1);
|
|
206
|
+
return `${complete.filter((line) => line.trim() !== '').join('\n')}\n`;
|
|
207
|
+
},
|
|
208
|
+
migrations: [],
|
|
209
|
+
limits: OPENUI_LIMITS,
|
|
210
|
+
measure: (content: string) => ({
|
|
211
|
+
declarations: parseStatements(content).statements.length,
|
|
212
|
+
slides: parseStatements(content).statements.filter(
|
|
213
|
+
(statement) => componentOf(statement.expr) === 'Slide',
|
|
214
|
+
).length,
|
|
215
|
+
}),
|
|
216
|
+
instructions: OPENUI_INSTRUCTIONS,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Default OpenUI format adapter with the artkit component library. */
|
|
221
|
+
export const openuiFormat = createOpenUIFormat();
|
package/src/lint.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { LintIssue } from '@artkit-ai/core';
|
|
2
|
+
import { argsOf, callsOf, childrenOf, componentOf, parseStatements, references, tokenize } from './syntax.js';
|
|
3
|
+
|
|
4
|
+
const MAX_BULLETS = 6;
|
|
5
|
+
const MAX_TEXT_CHARS = 400;
|
|
6
|
+
|
|
7
|
+
function stringArgs(expr: string): string[] {
|
|
8
|
+
return tokenize(expr)
|
|
9
|
+
.filter((token) => token.kind === 'string')
|
|
10
|
+
.map((token) => token.text.slice(1, -1));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Lint (spec §12.6): slides without title, long bullet lists, long text, images without alt, unused declarations. */
|
|
14
|
+
export function lintOpenUI(content: string): LintIssue[] {
|
|
15
|
+
const { statements } = parseStatements(content);
|
|
16
|
+
const byName = new Map(statements.map((statement) => [statement.name, statement]));
|
|
17
|
+
const referenced = new Set(statements.flatMap((statement) => references(statement.expr)));
|
|
18
|
+
const issues: LintIssue[] = [];
|
|
19
|
+
for (const statement of statements) {
|
|
20
|
+
const target = `node:${statement.name}`;
|
|
21
|
+
if (statement.name !== 'root' && !referenced.has(statement.name)) {
|
|
22
|
+
issues.push({
|
|
23
|
+
code: 'unused-declaration',
|
|
24
|
+
severity: 'info',
|
|
25
|
+
target,
|
|
26
|
+
message: `"${statement.name}" não é usada por nenhuma outra declaração.`,
|
|
27
|
+
hint: 'Remova a declaração ou inclua-a na lista de filhos.',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
for (const call of callsOf(statement.expr)) {
|
|
31
|
+
const args = argsOf(call.inner);
|
|
32
|
+
if (call.component === 'Slide') {
|
|
33
|
+
const children = childrenOf(`X(${call.inner})`);
|
|
34
|
+
const hasTitle = children.some(
|
|
35
|
+
(child) => componentOf(child) === 'Title' || componentOf(byName.get(child)?.expr ?? '') === 'Title',
|
|
36
|
+
);
|
|
37
|
+
if (!hasTitle) {
|
|
38
|
+
issues.push({
|
|
39
|
+
code: 'slide-no-title',
|
|
40
|
+
severity: 'warning',
|
|
41
|
+
target,
|
|
42
|
+
message: `O slide "${statement.name}" não tem título.`,
|
|
43
|
+
hint: 'Inclua um Title como primeiro filho.',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (call.component === 'Bullets') {
|
|
48
|
+
const count = stringArgs(args.join(', ')).length;
|
|
49
|
+
if (count > MAX_BULLETS) {
|
|
50
|
+
issues.push({
|
|
51
|
+
code: 'bullets-overflow',
|
|
52
|
+
severity: 'warning',
|
|
53
|
+
target,
|
|
54
|
+
message: `"${statement.name}" tem ${count} marcadores; o limite é ${MAX_BULLETS}.`,
|
|
55
|
+
hint: 'Divida em dois slides ou dois blocos.',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (call.component === 'Text' || call.component === 'Title' || call.component === 'Subtitle') {
|
|
60
|
+
const longest = Math.max(0, ...stringArgs(call.inner).map((text) => text.length));
|
|
61
|
+
if (longest > MAX_TEXT_CHARS) {
|
|
62
|
+
issues.push({
|
|
63
|
+
code: 'text-overflow',
|
|
64
|
+
severity: 'warning',
|
|
65
|
+
target,
|
|
66
|
+
message: `"${statement.name}" tem ${longest} caracteres; o provável limite é ${MAX_TEXT_CHARS}.`,
|
|
67
|
+
hint: 'Encurte o texto ou use Bullets.',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (call.component === 'Image') {
|
|
72
|
+
const hasAlt = stringArgs(call.inner).length >= 2;
|
|
73
|
+
if (!hasAlt) {
|
|
74
|
+
issues.push({
|
|
75
|
+
code: 'image-alt',
|
|
76
|
+
severity: 'warning',
|
|
77
|
+
target,
|
|
78
|
+
message: `A imagem "${statement.name}" não tem texto alternativo.`,
|
|
79
|
+
hint: 'Escreva Image("asset://id", "descrição").',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return issues;
|
|
86
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { AdapterContext } from '@artkit-ai/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import {
|
|
4
|
+
childrenOf,
|
|
5
|
+
componentOf,
|
|
6
|
+
formatStatement,
|
|
7
|
+
references,
|
|
8
|
+
removeReferences,
|
|
9
|
+
renameReferences,
|
|
10
|
+
type Statement,
|
|
11
|
+
setChildren,
|
|
12
|
+
} from './syntax.js';
|
|
13
|
+
|
|
14
|
+
const name = z
|
|
15
|
+
.string()
|
|
16
|
+
.regex(/^[a-z][A-Za-z0-9_]{0,63}$/, 'Declaration names start with a lowercase letter (e.g. "capa").');
|
|
17
|
+
const expr = z
|
|
18
|
+
.string()
|
|
19
|
+
.min(3)
|
|
20
|
+
.max(4000)
|
|
21
|
+
.refine((text) => !text.includes('\n'), 'One declaration per line.')
|
|
22
|
+
.describe('Right-hand side, e.g. Slide("title", [Title("Portal")]).');
|
|
23
|
+
|
|
24
|
+
export const openuiOperationSchema = z.discriminatedUnion('op', [
|
|
25
|
+
z.object({ op: z.literal('set_statement'), name, expr }).strict(),
|
|
26
|
+
z.object({ op: z.literal('remove_statement'), name, cascade: z.boolean().default(false) }).strict(),
|
|
27
|
+
z.object({ op: z.literal('rename_statement'), name, to: name }).strict(),
|
|
28
|
+
z
|
|
29
|
+
.object({
|
|
30
|
+
op: z.literal('insert_child'),
|
|
31
|
+
name,
|
|
32
|
+
child: name,
|
|
33
|
+
position: z.number().int().min(0).optional().describe('0-based position; omit to append.'),
|
|
34
|
+
after: name.optional().describe('Insert after this child name.'),
|
|
35
|
+
})
|
|
36
|
+
.strict(),
|
|
37
|
+
z.object({ op: z.literal('remove_child'), name, child: name }).strict(),
|
|
38
|
+
z.object({ op: z.literal('move_child'), name, child: name, position: z.number().int().min(0) }).strict(),
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
export type OpenUIOperation = z.infer<typeof openuiOperationSchema>;
|
|
42
|
+
|
|
43
|
+
export const OPENUI_OPERATION_NAMES = [
|
|
44
|
+
'set_statement',
|
|
45
|
+
'remove_statement',
|
|
46
|
+
'rename_statement',
|
|
47
|
+
'insert_child',
|
|
48
|
+
'remove_child',
|
|
49
|
+
'move_child',
|
|
50
|
+
] as const;
|
|
51
|
+
|
|
52
|
+
export interface OpenUIApplyState {
|
|
53
|
+
statements: Statement[];
|
|
54
|
+
createdRefs: string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function indexOfStatement(
|
|
58
|
+
statements: readonly Statement[],
|
|
59
|
+
target: string,
|
|
60
|
+
ctx: AdapterContext,
|
|
61
|
+
path: string,
|
|
62
|
+
): number {
|
|
63
|
+
const index = statements.findIndex((statement) => statement.name === target);
|
|
64
|
+
if (index < 0) {
|
|
65
|
+
ctx.fail(`A declaração "${target}" não existe.`, {
|
|
66
|
+
path,
|
|
67
|
+
hint: 'Leia o outline e use um nome declarado nele.',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return index;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function referrersOf(statements: readonly Statement[], target: string): string[] {
|
|
74
|
+
return statements
|
|
75
|
+
.filter((statement) => statement.name !== target && references(statement.expr).includes(target))
|
|
76
|
+
.map((statement) => statement.name);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Applies one declaration operation on the list of statements. Order is kept; new statements are appended. */
|
|
80
|
+
export function applyOpenUIOperation(
|
|
81
|
+
state: OpenUIApplyState,
|
|
82
|
+
op: OpenUIOperation,
|
|
83
|
+
ctx: AdapterContext,
|
|
84
|
+
): void {
|
|
85
|
+
const statements = state.statements;
|
|
86
|
+
switch (op.op) {
|
|
87
|
+
case 'set_statement': {
|
|
88
|
+
if (!componentOf(op.expr) || !op.expr.includes('(')) {
|
|
89
|
+
ctx.fail(`A declaração "${op.name}" precisa chamar um componente, como Title("texto").`, {
|
|
90
|
+
path: 'expr',
|
|
91
|
+
hint: 'Escreva o componente com seus argumentos entre parênteses.',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const at = statements.findIndex((statement) => statement.name === op.name);
|
|
95
|
+
if (at >= 0) statements[at] = { name: op.name, expr: op.expr };
|
|
96
|
+
else {
|
|
97
|
+
statements.push({ name: op.name, expr: op.expr });
|
|
98
|
+
state.createdRefs.push(op.name);
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
case 'remove_statement': {
|
|
103
|
+
const at = indexOfStatement(statements, op.name, ctx, 'name');
|
|
104
|
+
const referrers = referrersOf(statements, op.name);
|
|
105
|
+
if (referrers.length > 0 && !op.cascade) {
|
|
106
|
+
ctx.fail(`"${op.name}" ainda é usada por ${referrers.join(', ')}.`, {
|
|
107
|
+
path: 'cascade',
|
|
108
|
+
hint: 'Remova ou troque a referência antes, ou envie cascade: true para remover as referências junto.',
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
statements.splice(at, 1);
|
|
112
|
+
for (const statement of statements) statement.expr = removeReferences(statement.expr, op.name);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
case 'rename_statement': {
|
|
116
|
+
const at = indexOfStatement(statements, op.name, ctx, 'name');
|
|
117
|
+
if (statements.some((statement) => statement.name === op.to)) {
|
|
118
|
+
ctx.fail(`Já existe uma declaração chamada "${op.to}".`, { path: 'to', hint: 'Escolha outro nome.' });
|
|
119
|
+
}
|
|
120
|
+
statements[at]!.name = op.to;
|
|
121
|
+
for (const statement of statements) statement.expr = renameReferences(statement.expr, op.name, op.to);
|
|
122
|
+
state.createdRefs.push(op.to);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
case 'insert_child': {
|
|
126
|
+
const at = indexOfStatement(statements, op.name, ctx, 'name');
|
|
127
|
+
indexOfStatement(statements, op.child, ctx, 'child');
|
|
128
|
+
const current = childrenOf(statements[at]!.expr);
|
|
129
|
+
if (!/\[/.test(statements[at]!.expr)) {
|
|
130
|
+
ctx.fail(`A declaração "${op.name}" não tem lista de filhos.`, {
|
|
131
|
+
path: 'name',
|
|
132
|
+
hint: 'Escolha um componente que aceite filhos, como Slide ou Card.',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
const items = current.filter((item) => item !== op.child);
|
|
136
|
+
let position = op.position ?? items.length;
|
|
137
|
+
if (op.after !== undefined) {
|
|
138
|
+
const index = items.indexOf(op.after);
|
|
139
|
+
if (index < 0)
|
|
140
|
+
ctx.fail(`"${op.after}" não está na lista de filhos de "${op.name}".`, { path: 'after' });
|
|
141
|
+
position = index + 1;
|
|
142
|
+
}
|
|
143
|
+
items.splice(Math.min(position, items.length), 0, op.child);
|
|
144
|
+
statements[at]!.expr = setChildren(statements[at]!.expr, items)!;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
case 'remove_child': {
|
|
148
|
+
const at = indexOfStatement(statements, op.name, ctx, 'name');
|
|
149
|
+
const items = childrenOf(statements[at]!.expr);
|
|
150
|
+
if (!items.includes(op.child)) {
|
|
151
|
+
ctx.fail(`"${op.child}" não está na lista de filhos de "${op.name}".`, { path: 'child' });
|
|
152
|
+
}
|
|
153
|
+
statements[at]!.expr = setChildren(
|
|
154
|
+
statements[at]!.expr,
|
|
155
|
+
items.filter((item) => item !== op.child),
|
|
156
|
+
)!;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
case 'move_child': {
|
|
160
|
+
const at = indexOfStatement(statements, op.name, ctx, 'name');
|
|
161
|
+
const items = childrenOf(statements[at]!.expr);
|
|
162
|
+
if (!items.includes(op.child))
|
|
163
|
+
ctx.fail(`"${op.child}" não está na lista de filhos de "${op.name}".`, { path: 'child' });
|
|
164
|
+
const rest = items.filter((item) => item !== op.child);
|
|
165
|
+
rest.splice(Math.min(op.position, rest.length), 0, op.child);
|
|
166
|
+
statements[at]!.expr = setChildren(statements[at]!.expr, rest)!;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export { formatStatement };
|
package/src/read.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { ellipsis, type ProjectionItem, paginate, type ReadRequest, type ReadResult } from '@artkit-ai/core';
|
|
2
|
+
import { childrenOf, componentOf, formatStatement, parseStatements, type Statement } from './syntax.js';
|
|
3
|
+
|
|
4
|
+
function labelOf(expr: string): string {
|
|
5
|
+
const match = /"((?:[^"\\]|\\.)*)"/.exec(expr);
|
|
6
|
+
return match ? ellipsis(match[1]!, 60) : '';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function outlineLine(statement: Statement): string {
|
|
10
|
+
const component = componentOf(statement.expr) ?? '?';
|
|
11
|
+
const children = childrenOf(statement.expr);
|
|
12
|
+
const parts = [`- ${statement.name}: ${component}`];
|
|
13
|
+
const label = labelOf(statement.expr);
|
|
14
|
+
if (label) parts.push(`"${label}"`);
|
|
15
|
+
if (children.length > 0) parts.push(`(${children.length} filhos: ${children.join(', ')})`);
|
|
16
|
+
return parts.join(' ');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Compact reading (spec §12.4): `outline` from the root, `statements` by name, and `full`. */
|
|
20
|
+
export function readOpenUI(content: string, request: ReadRequest): ReadResult {
|
|
21
|
+
const { statements } = parseStatements(content);
|
|
22
|
+
const budget = {
|
|
23
|
+
...(request.maxTokens === undefined ? {} : { maxTokens: request.maxTokens }),
|
|
24
|
+
...(request.cursor === undefined ? {} : { cursor: request.cursor }),
|
|
25
|
+
...(request.charsPerToken === undefined ? {} : { charsPerToken: request.charsPerToken }),
|
|
26
|
+
};
|
|
27
|
+
if (request.mode === 'outline') {
|
|
28
|
+
const items: ProjectionItem[] = statements.map((statement) => {
|
|
29
|
+
const line = outlineLine(statement);
|
|
30
|
+
return { id: statement.name, text: line, outline: line };
|
|
31
|
+
});
|
|
32
|
+
const projection = paginate(items, {
|
|
33
|
+
header: 'Declarações (raiz primeiro):',
|
|
34
|
+
...budget,
|
|
35
|
+
outlineOnly: true,
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
text: projection.text,
|
|
39
|
+
truncated: projection.truncated,
|
|
40
|
+
...(projection.cursor ? { cursor: projection.cursor } : {}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const wanted =
|
|
44
|
+
request.mode === 'statements' && request.range
|
|
45
|
+
? new Set(request.range.split(',').map((part) => part.trim()))
|
|
46
|
+
: undefined;
|
|
47
|
+
const items: ProjectionItem[] = statements
|
|
48
|
+
.filter((statement) => !wanted || wanted.has(statement.name))
|
|
49
|
+
.map((statement) => {
|
|
50
|
+
const line = formatStatement(statement);
|
|
51
|
+
return { id: statement.name, text: line, outline: ellipsis(line, 80) };
|
|
52
|
+
});
|
|
53
|
+
const projection = paginate(items, budget);
|
|
54
|
+
return {
|
|
55
|
+
text: projection.text,
|
|
56
|
+
truncated: projection.truncated,
|
|
57
|
+
...(projection.cursor ? { cursor: projection.cursor } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|