@tinta-lang/engine 0.0.0-stage → 0.1.0-dev.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.
@@ -0,0 +1,304 @@
1
+ import { attempt, Budget, fail, freeze, record } from "./common.js";
2
+ import { programs } from "./programs.js";
3
+ import { evaluate, format, } from "./expression.js";
4
+ import { prepare } from "./charts.js";
5
+ import { inDomain } from "./grid.js";
6
+ function compare(a, b) {
7
+ if (typeof a === "string" && typeof b === "string") {
8
+ const x = [...a], y = [...b];
9
+ for (let i = 0; i < Math.min(x.length, y.length); i++) {
10
+ const delta = x[i].codePointAt(0) - y[i].codePointAt(0);
11
+ if (delta)
12
+ return delta;
13
+ }
14
+ return x.length - y.length;
15
+ }
16
+ return a < b ? -1 : a > b ? 1 : 0;
17
+ }
18
+ function revision(program, state, number) {
19
+ const bound = programs.get(program);
20
+ const span = program.flow[0]?.span ??
21
+ program.declarations[0]?.span ?? {
22
+ uri: program.sourceUri,
23
+ start: { offset: 0, line: 1, column: 1 },
24
+ end: { offset: 0, line: 1, column: 1 },
25
+ };
26
+ const budget = new Budget(span, "evaluate", { limits: bound.limits });
27
+ const values = record(), tables = record();
28
+ const get = (name) => Object.hasOwn(values, name) ? values[name] : tables[name];
29
+ const env = { get, budget, metadata: program.metadata };
30
+ for (const d of bound.order) {
31
+ if (d.kind === "let")
32
+ values[d.name] = bound.constants.get(d.name);
33
+ else if (d.kind === "calc")
34
+ values[d.name] = evaluate(d.expression, env);
35
+ else if (d.kind === "data")
36
+ tables[d.name] = d.table;
37
+ else if (d.kind === "view") {
38
+ const table = tables[d.source];
39
+ const rows = table.rows.filter((row) => {
40
+ budget.charge(d.span);
41
+ return (!d.where ||
42
+ evaluate(d.where, {
43
+ ...env,
44
+ row: new Map(table.columns.map((c, i) => [c.name, row.cells[i]])),
45
+ }));
46
+ });
47
+ if (d.sort) {
48
+ const index = table.columns.findIndex((c) => c.name === d.sort);
49
+ rows.sort((a, b) => {
50
+ budget.charge(d.span);
51
+ return (compare(a.cells[index], b.cells[index]) * (d.descending ? -1 : 1));
52
+ });
53
+ }
54
+ tables[d.name] = { columns: table.columns, rows };
55
+ }
56
+ else
57
+ values[d.name] = state[d.name];
58
+ }
59
+ let marks = 0;
60
+ const text = (id, value) => {
61
+ budget.limit("textValueUnits", value.length);
62
+ return { kind: "text", id, value };
63
+ };
64
+ const element = (id, tag, children, attributes = {}) => ({
65
+ kind: "element",
66
+ id,
67
+ tag,
68
+ attributes,
69
+ children,
70
+ });
71
+ function resolve(node) {
72
+ const children = () => "children" in node ? node.children.flatMap(resolve) : [];
73
+ switch (node.kind) {
74
+ case "text":
75
+ case "raw-html":
76
+ return [text(node.id, node.value)];
77
+ case "interpolation":
78
+ return [
79
+ text(node.id, format(evaluate(node.expression, env), program.metadata)),
80
+ ];
81
+ case "when":
82
+ return evaluate(node.condition, env) ? children() : [];
83
+ case "details":
84
+ return [
85
+ element(node.id, "details", [
86
+ element(node.id, "summary", [text(node.id, node.summary)]),
87
+ ...children(),
88
+ ]),
89
+ ];
90
+ case "control":
91
+ return [
92
+ {
93
+ kind: "control",
94
+ id: node.id,
95
+ definition: bound.controls.get(node.name),
96
+ value: state[node.name],
97
+ },
98
+ ];
99
+ case "chart": {
100
+ const chart = prepare(node.chart, tables[node.chart.source], env, bound.controls);
101
+ marks += chart.points.length + chart.bins.length;
102
+ budget.limit("documentChartMarks", marks, node.span);
103
+ return [{ kind: "chart", id: node.id, chart }];
104
+ }
105
+ case "image":
106
+ return [
107
+ {
108
+ kind: "image",
109
+ id: node.id,
110
+ destination: node.destination,
111
+ alt: node.alt,
112
+ ...(node.title !== undefined ? { title: node.title } : {}),
113
+ },
114
+ ];
115
+ case "paragraph":
116
+ case "emphasis":
117
+ case "strong":
118
+ case "delete":
119
+ case "blockquote":
120
+ return [
121
+ element(node.id, {
122
+ paragraph: "p",
123
+ emphasis: "em",
124
+ strong: "strong",
125
+ delete: "del",
126
+ blockquote: "blockquote",
127
+ }[node.kind], children()),
128
+ ];
129
+ case "heading":
130
+ return [
131
+ element(node.id, `h${node.level}`, children(), { id: node.anchor }),
132
+ ];
133
+ case "link": {
134
+ const safe = /^(?:https?:|mailto:|#)/i.test(node.destination) &&
135
+ ![...node.destination].some((char) => char.codePointAt(0) <= 32);
136
+ return [
137
+ element(node.id, safe ? "a" : "span", children(), safe
138
+ ? {
139
+ href: node.destination,
140
+ ...(node.title === undefined ? {} : { title: node.title }),
141
+ }
142
+ : {}),
143
+ ];
144
+ }
145
+ case "code": {
146
+ const code = element(node.id, "code", [text(node.id, node.value)]);
147
+ return [node.block ? element(node.id, "pre", [code]) : code];
148
+ }
149
+ case "break":
150
+ return node.hard ? [element(node.id, "br", [])] : [text(node.id, "\n")];
151
+ case "thematic-break":
152
+ return [element(node.id, "hr", [])];
153
+ case "list":
154
+ return [
155
+ element(node.id, node.ordered ? "ol" : "ul", node.items.map((item) => element(item.id, "li", [
156
+ ...(item.checked === undefined
157
+ ? []
158
+ : [
159
+ element(item.id, "input", [], {
160
+ type: "checkbox",
161
+ disabled: true,
162
+ checked: item.checked,
163
+ }),
164
+ ]),
165
+ ...item.children.flatMap(resolve),
166
+ ])), node.ordered ? { start: node.start } : {}),
167
+ ];
168
+ case "markdown-table":
169
+ return [
170
+ element(node.id, "table", [
171
+ element(node.id, "thead", [
172
+ element(node.id, "tr", node.header.map((cell) => element(node.id, "th", cell.flatMap(resolve), {
173
+ scope: "col",
174
+ }))),
175
+ ]),
176
+ element(node.id, "tbody", node.rows.map((row) => element(node.id, "tr", row.map((cell) => element(node.id, "td", cell.flatMap(resolve)))))),
177
+ ]),
178
+ ];
179
+ case "data-table": {
180
+ const table = tables[node.source];
181
+ return [
182
+ element(node.id, "table", [
183
+ element(node.id, "caption", [
184
+ text(node.id, `${node.title} — ${table.rows.length} ${table.rows.length === 1 ? "row" : "rows"}`),
185
+ ]),
186
+ element(node.id, "thead", [
187
+ element(node.id, "tr", table.columns.map((c) => element(node.id, "th", [text(node.id, c.name)], {
188
+ scope: "col",
189
+ }))),
190
+ ]),
191
+ element(node.id, "tbody", table.rows.map((row) => element(node.id, "tr", row.cells.map((cell) => element(node.id, "td", [
192
+ text(node.id, format(cell, program.metadata)),
193
+ ]))))),
194
+ ]),
195
+ ...(table.rows.length
196
+ ? []
197
+ : [text(node.id, "No rows match the current choices.")]),
198
+ ];
199
+ }
200
+ }
201
+ }
202
+ return freeze({
203
+ number,
204
+ state,
205
+ values,
206
+ tables,
207
+ nodes: program.flow.flatMap(resolve),
208
+ diagnostics: [],
209
+ });
210
+ }
211
+ export function createSession(program, options) {
212
+ const fallback = {
213
+ uri: program.sourceUri,
214
+ start: { offset: 0, line: 1, column: 1 },
215
+ end: { offset: 0, line: 1, column: 1 },
216
+ };
217
+ return attempt(() => {
218
+ const bound = programs.get(program);
219
+ if (!bound)
220
+ fail("E_PROTOCOL", fallback, "host");
221
+ const baseline = record();
222
+ for (const [name, control] of bound.controls)
223
+ baseline[name] = control.initial;
224
+ const validate = (update) => {
225
+ for (const [name, value] of Object.entries(update)) {
226
+ const control = bound.controls.get(name);
227
+ if (!control)
228
+ fail("E_STATE_NAME", fallback, "evaluate");
229
+ if (!inDomain(control, value))
230
+ fail("E_STATE_DOMAIN", control.span, "evaluate");
231
+ }
232
+ };
233
+ if (options?.baseline) {
234
+ validate(options.baseline);
235
+ if (Object.keys(options.baseline).length !== bound.controls.size)
236
+ fail("E_STATE_DOMAIN", fallback, "evaluate");
237
+ Object.assign(baseline, options.baseline);
238
+ }
239
+ for (const name of Object.keys(baseline))
240
+ if (Object.is(baseline[name], -0))
241
+ baseline[name] = 0;
242
+ freeze(baseline);
243
+ let current = revision(program, baseline, 0);
244
+ let destroyed = false, notifying = false;
245
+ const listeners = new Set();
246
+ const setControls = (update) => {
247
+ const diagnostics = [];
248
+ return attempt(() => {
249
+ if (destroyed || notifying)
250
+ fail("E_PROTOCOL", fallback, "host");
251
+ validate(update);
252
+ const candidate = Object.assign(record(), current.state, update);
253
+ for (const name of Object.keys(candidate))
254
+ if (Object.is(candidate[name], -0))
255
+ candidate[name] = 0;
256
+ if (Object.keys(candidate).every((name) => candidate[name] === current.state[name]))
257
+ return current;
258
+ const next = revision(program, candidate, current.number + 1);
259
+ current = next;
260
+ notifying = true;
261
+ try {
262
+ for (const listener of [...listeners]) {
263
+ if (destroyed)
264
+ break;
265
+ try {
266
+ listener(current);
267
+ }
268
+ catch {
269
+ diagnostics.push({
270
+ code: "E_INTERNAL",
271
+ severity: "error",
272
+ phase: "host",
273
+ message: "A subscriber failed after the revision committed.",
274
+ span: fallback,
275
+ });
276
+ }
277
+ }
278
+ }
279
+ finally {
280
+ notifying = false;
281
+ }
282
+ return current;
283
+ }, diagnostics);
284
+ };
285
+ return {
286
+ program,
287
+ getRevision: () => current,
288
+ setControls,
289
+ setControl: (name, value) => setControls({ [name]: value }),
290
+ reset: () => setControls(baseline),
291
+ subscribe: (listener) => {
292
+ if (!destroyed)
293
+ listeners.add(listener);
294
+ return () => {
295
+ listeners.delete(listener);
296
+ };
297
+ },
298
+ destroy: () => {
299
+ destroyed = true;
300
+ listeners.clear();
301
+ },
302
+ };
303
+ });
304
+ }
package/package.json CHANGED
@@ -1,6 +1,77 @@
1
1
  {
2
2
  "name": "@tinta-lang/engine",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0-dev.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "engines": {
8
+ "node": ">=24.21.0 <25"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public",
12
+ "tag": "next"
13
+ },
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js"
18
+ },
19
+ "./chart-presentation": {
20
+ "types": "./dist/chart-presentation.d.ts",
21
+ "import": "./dist/chart-presentation.js"
22
+ },
23
+ "./runtime": {
24
+ "types": "./dist/runtime.d.ts",
25
+ "import": "./dist/runtime.js"
26
+ }
27
+ },
28
+ "dependencies": {
29
+ "@tinta-lang/types": "0.1.0-dev.0",
30
+ "@tinta-lang/syntax": "0.1.0-dev.0",
31
+ "@noble/hashes": "2.4.0"
32
+ },
33
+ "files": [
34
+ "LICENSE",
35
+ "THIRD_PARTY_NOTICES.txt",
36
+ "dist/analysis.d.ts",
37
+ "dist/analysis.js",
38
+ "dist/binding.d.ts",
39
+ "dist/binding.js",
40
+ "dist/chart-presentation.d.ts",
41
+ "dist/chart-presentation.js",
42
+ "dist/charts.d.ts",
43
+ "dist/charts.js",
44
+ "dist/codec.d.ts",
45
+ "dist/codec.js",
46
+ "dist/common.d.ts",
47
+ "dist/common.js",
48
+ "dist/compiler.d.ts",
49
+ "dist/compiler.js",
50
+ "dist/expression.d.ts",
51
+ "dist/expression.js",
52
+ "dist/grid.d.ts",
53
+ "dist/grid.js",
54
+ "dist/imports.d.ts",
55
+ "dist/imports.js",
56
+ "dist/index.d.ts",
57
+ "dist/index.js",
58
+ "dist/inspect.d.ts",
59
+ "dist/inspect.js",
60
+ "dist/program-validator.d.ts",
61
+ "dist/program-validator.js",
62
+ "dist/programs.d.ts",
63
+ "dist/programs.js",
64
+ "dist/registry.d.ts",
65
+ "dist/registry.js",
66
+ "dist/runtime.d.ts",
67
+ "dist/runtime.js",
68
+ "dist/session.d.ts",
69
+ "dist/session.js"
70
+ ],
71
+ "types": "./dist/index.d.ts",
72
+ "repository": {
73
+ "type": "git",
74
+ "url": "https://github.com/planta7/tinta.git",
75
+ "directory": "packages/engine"
76
+ }
6
77
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.