@hatchingpoint/point 0.0.11 → 0.0.13
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 +2 -2
- package/package.json +4 -2
- package/src/cli.ts +0 -0
- package/src/core/ast.ts +21 -1
- package/src/core/check-docs.ts +155 -0
- package/src/core/check.ts +10 -2
- package/src/core/cli.ts +90 -31
- package/src/core/context.ts +3 -0
- package/src/core/emit-javascript.ts +67 -1
- package/src/core/emit-python.ts +188 -0
- package/src/core/emit-typescript.ts +89 -5
- package/src/core/index.ts +3 -0
- package/src/core/packages.ts +138 -0
- package/src/core/semantic-source.ts +2 -2
- package/src/core/test-only/legacy-lowering.ts +76 -7
- package/src/semantic/ast.ts +14 -1
- package/src/semantic/callables.ts +2 -1
- package/src/semantic/context.ts +12 -1
- package/src/semantic/desugar.ts +82 -3
- package/src/semantic/expressions.ts +28 -3
- package/src/semantic/format.ts +10 -0
- package/src/semantic/metadata.ts +3 -0
- package/src/semantic/naming.ts +4 -2
- package/src/semantic/parse.ts +81 -2
- package/src/std/http.ts +30 -0
- package/src/std/json.ts +13 -0
package/src/semantic/ast.ts
CHANGED
|
@@ -24,6 +24,7 @@ export type PointSemanticDeclaration =
|
|
|
24
24
|
| PointSemanticActionDeclaration
|
|
25
25
|
| PointSemanticPolicyDeclaration
|
|
26
26
|
| PointSemanticViewDeclaration
|
|
27
|
+
| PointSemanticPageDeclaration
|
|
27
28
|
| PointSemanticRouteDeclaration
|
|
28
29
|
| PointSemanticWorkflowDeclaration
|
|
29
30
|
| PointSemanticCommandDeclaration;
|
|
@@ -123,6 +124,16 @@ export interface PointSemanticViewDeclaration {
|
|
|
123
124
|
span?: PointSourceSpan;
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
export interface PointSemanticPageDeclaration {
|
|
128
|
+
kind: "page";
|
|
129
|
+
name: string;
|
|
130
|
+
inputs: PointSemanticBinding[];
|
|
131
|
+
title: PointSemanticExpression;
|
|
132
|
+
description?: PointSemanticExpression;
|
|
133
|
+
main: PointSemanticExpression;
|
|
134
|
+
span?: PointSourceSpan;
|
|
135
|
+
}
|
|
136
|
+
|
|
126
137
|
export interface PointSemanticRouteDeclaration {
|
|
127
138
|
kind: "route";
|
|
128
139
|
name: string;
|
|
@@ -182,7 +193,9 @@ export type PointSemanticPolicyStatement =
|
|
|
182
193
|
|
|
183
194
|
export type PointSemanticViewStatement =
|
|
184
195
|
| { kind: "render"; value: PointSemanticExpression; span?: PointSourceSpan }
|
|
185
|
-
| { kind: "whenRender"; condition: PointSemanticExpression; value: PointSemanticExpression; span?: PointSourceSpan }
|
|
196
|
+
| { kind: "whenRender"; condition: PointSemanticExpression; value: PointSemanticExpression; span?: PointSourceSpan }
|
|
197
|
+
| { kind: "bindCheckbox"; label: string; target: PointSemanticExpression; span?: PointSourceSpan }
|
|
198
|
+
| { kind: "onChangeCall"; callback: string; span?: PointSourceSpan };
|
|
186
199
|
|
|
187
200
|
export type PointSemanticRouteStatement = { kind: "return"; value: PointSemanticExpression; span?: PointSourceSpan };
|
|
188
201
|
|
|
@@ -4,6 +4,7 @@ const CALLABLE_KEYWORDS = [
|
|
|
4
4
|
"label",
|
|
5
5
|
"action",
|
|
6
6
|
"view",
|
|
7
|
+
"page",
|
|
7
8
|
"route",
|
|
8
9
|
"workflow",
|
|
9
10
|
"command",
|
|
@@ -47,5 +48,5 @@ function collectBody(lines: string[], start: number): { lines: string[]; next: n
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
function isTopLevel(line: string): boolean {
|
|
50
|
-
return /^(module|use|record|calculation|rule|label|external|action|policy|view|route|workflow|command)\s+/.test(line);
|
|
51
|
+
return /^(module|use|record|calculation|rule|label|external|action|policy|view|page|route|workflow|command)\s+/.test(line);
|
|
51
52
|
}
|
package/src/semantic/context.ts
CHANGED
|
@@ -27,6 +27,7 @@ export type PointSemanticSymbolKind =
|
|
|
27
27
|
| "action"
|
|
28
28
|
| "policy"
|
|
29
29
|
| "view"
|
|
30
|
+
| "page"
|
|
30
31
|
| "route"
|
|
31
32
|
| "workflow"
|
|
32
33
|
| "command"
|
|
@@ -215,6 +216,15 @@ function callableDeclaration(declaration: PointSemanticDeclaration):
|
|
|
215
216
|
if (declaration.kind === "view" || declaration.kind === "route" || declaration.kind === "workflow" || declaration.kind === "command") {
|
|
216
217
|
return { kind: declaration.kind, name: declaration.name, inputs: declaration.inputs, output: declaration.output, declaration };
|
|
217
218
|
}
|
|
219
|
+
if (declaration.kind === "page") {
|
|
220
|
+
return {
|
|
221
|
+
kind: "page",
|
|
222
|
+
name: declaration.name,
|
|
223
|
+
inputs: declaration.inputs,
|
|
224
|
+
output: { name: "page", type: { kind: "typeRef", name: "Page", args: [] } },
|
|
225
|
+
declaration,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
218
228
|
return null;
|
|
219
229
|
}
|
|
220
230
|
|
|
@@ -313,7 +323,7 @@ function relatedRefsFor(symbol: PointSemanticSymbol, index: PointSemanticIndex):
|
|
|
313
323
|
const ownerPath = symbol.path.split(".").slice(0, 2).join(".");
|
|
314
324
|
return index.refs.filter((candidate) => candidate.path.startsWith(`${ownerPath}.`) && candidate.ref !== symbol.ref).map((candidate) => candidate.ref);
|
|
315
325
|
}
|
|
316
|
-
if (symbol.kind === "record" || symbol.kind === "calculation" || symbol.kind === "rule" || symbol.kind === "label" || symbol.kind === "action" || symbol.kind === "policy" || symbol.kind === "view" || symbol.kind === "route" || symbol.kind === "workflow" || symbol.kind === "command" || symbol.kind === "external") {
|
|
326
|
+
if (symbol.kind === "record" || symbol.kind === "calculation" || symbol.kind === "rule" || symbol.kind === "label" || symbol.kind === "action" || symbol.kind === "policy" || symbol.kind === "view" || symbol.kind === "page" || symbol.kind === "route" || symbol.kind === "workflow" || symbol.kind === "command" || symbol.kind === "external") {
|
|
317
327
|
return index.refs.filter((candidate) => candidate.path.startsWith(`${symbol.path}.`)).map((candidate) => candidate.ref);
|
|
318
328
|
}
|
|
319
329
|
return [];
|
|
@@ -332,6 +342,7 @@ function summaryFor(symbol: PointSemanticSymbol): string {
|
|
|
332
342
|
if (symbol.kind === "action") return `Semantic action ${symbol.name} returns ${symbol.type}; effects: ${(symbol.effects ?? []).join(", ") || "none"}.`;
|
|
333
343
|
if (symbol.kind === "policy") return `Semantic policy ${symbol.name} returns ${symbol.type}.`;
|
|
334
344
|
if (symbol.kind === "view") return `Semantic view ${symbol.name} returns ${symbol.type}.`;
|
|
345
|
+
if (symbol.kind === "page") return `Semantic page ${symbol.name} returns a Next.js page shell (${symbol.type}).`;
|
|
335
346
|
if (symbol.kind === "route") return `Semantic route ${symbol.name} returns ${symbol.type}.`;
|
|
336
347
|
if (symbol.kind === "workflow") return `Semantic workflow ${symbol.name} returns ${symbol.type}.`;
|
|
337
348
|
if (symbol.kind === "command") return `Semantic command ${symbol.name} returns ${symbol.type}.`;
|
package/src/semantic/desugar.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
PointCoreTypeExpression,
|
|
12
12
|
PointCoreValueDeclaration,
|
|
13
13
|
PointSourceSpan,
|
|
14
|
+
PointSemanticViewControls,
|
|
14
15
|
} from "../core/ast.ts";
|
|
15
16
|
import type {
|
|
16
17
|
PointSemanticBinding,
|
|
@@ -35,6 +36,8 @@ import type {
|
|
|
35
36
|
PointSemanticTypeExpression,
|
|
36
37
|
PointSemanticUseDeclaration,
|
|
37
38
|
PointSemanticViewDeclaration,
|
|
39
|
+
PointSemanticViewStatement,
|
|
40
|
+
PointSemanticPageDeclaration,
|
|
38
41
|
PointSemanticWorkflowDeclaration,
|
|
39
42
|
PointSemanticWorkflowStatement,
|
|
40
43
|
PointSemanticActionDeclaration,
|
|
@@ -104,6 +107,7 @@ function buildCallableMap(
|
|
|
104
107
|
declaration.kind === "action" ||
|
|
105
108
|
declaration.kind === "policy" ||
|
|
106
109
|
declaration.kind === "view" ||
|
|
110
|
+
declaration.kind === "page" ||
|
|
107
111
|
declaration.kind === "route" ||
|
|
108
112
|
declaration.kind === "workflow" ||
|
|
109
113
|
declaration.kind === "command"
|
|
@@ -119,6 +123,7 @@ function defaultOutputName(declaration: PointSemanticDeclaration): string {
|
|
|
119
123
|
if (declaration.kind === "label") return "label";
|
|
120
124
|
if (declaration.kind === "policy") return "policy";
|
|
121
125
|
if (declaration.kind === "view") return "view";
|
|
126
|
+
if (declaration.kind === "page") return "page";
|
|
122
127
|
if (declaration.kind === "route") return "route";
|
|
123
128
|
if ("output" in declaration) return toIdentifier(declaration.output.name);
|
|
124
129
|
return "result";
|
|
@@ -152,6 +157,8 @@ function desugarDeclaration(
|
|
|
152
157
|
return [desugarPolicy(declaration, records, callables)];
|
|
153
158
|
case "view":
|
|
154
159
|
return [desugarView(declaration, records, callables)];
|
|
160
|
+
case "page":
|
|
161
|
+
return [desugarPage(declaration, records, callables)];
|
|
155
162
|
case "route":
|
|
156
163
|
return [desugarRoute(declaration, records, callables)];
|
|
157
164
|
case "workflow":
|
|
@@ -296,13 +303,85 @@ function desugarView(
|
|
|
296
303
|
const outputType: PointCoreTypeExpression = { kind: "typeRef", name: "Text", args: [] };
|
|
297
304
|
const { params, bindings } = collectBindings(declaration.inputs, declaration.output);
|
|
298
305
|
const ctx: DesugarContext = { records, callables, bindings, outputName: "page", outputType };
|
|
306
|
+
const renderStatements = declaration.body.filter(
|
|
307
|
+
(statement): statement is Extract<PointSemanticViewStatement, { kind: "render" | "whenRender" }> =>
|
|
308
|
+
statement.kind === "render" || statement.kind === "whenRender",
|
|
309
|
+
);
|
|
310
|
+
const metadata = semanticDeclarationMetadata(declaration);
|
|
311
|
+
const viewControls = buildViewControls(declaration, ctx);
|
|
312
|
+
if (viewControls) metadata.viewControls = viewControls;
|
|
299
313
|
return {
|
|
300
314
|
kind: "function",
|
|
301
315
|
name: semanticFunctionName(declaration.name, "view", "view"),
|
|
302
316
|
params,
|
|
303
317
|
returnType: outputType,
|
|
304
|
-
body: desugarViewBody(
|
|
305
|
-
semantic:
|
|
318
|
+
body: desugarViewBody(renderStatements, ctx),
|
|
319
|
+
semantic: metadata,
|
|
320
|
+
span: declaration.span,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function buildViewControls(
|
|
325
|
+
declaration: PointSemanticViewDeclaration,
|
|
326
|
+
ctx: DesugarContext,
|
|
327
|
+
): PointSemanticViewControls | undefined {
|
|
328
|
+
const bindStatements = declaration.body.filter(
|
|
329
|
+
(statement): statement is Extract<PointSemanticViewStatement, { kind: "bindCheckbox" }> => statement.kind === "bindCheckbox",
|
|
330
|
+
);
|
|
331
|
+
if (bindStatements.length === 0) return undefined;
|
|
332
|
+
|
|
333
|
+
const onChangeCall = declaration.body.find(
|
|
334
|
+
(statement): statement is Extract<PointSemanticViewStatement, { kind: "onChangeCall" }> => statement.kind === "onChangeCall",
|
|
335
|
+
);
|
|
336
|
+
const handlerInput = declaration.inputs.find((input) => input.type.name === "Handler" && input.type.args.length === 1);
|
|
337
|
+
const callbackLabel = onChangeCall?.callback ?? handlerInput?.label;
|
|
338
|
+
if (!callbackLabel) {
|
|
339
|
+
throw new Error(`View ${declaration.name} with bind checkbox requires input Handler T or on change call`);
|
|
340
|
+
}
|
|
341
|
+
const changeCallback = toIdentifier(callbackLabel);
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
changeCallback,
|
|
345
|
+
checkboxes: bindStatements.map((statement) => {
|
|
346
|
+
const target = desugarExpression(statement.target, ctx);
|
|
347
|
+
if (target.kind !== "property") {
|
|
348
|
+
throw new Error(`bind checkbox target must be a record field access`);
|
|
349
|
+
}
|
|
350
|
+
if (target.target.kind !== "identifier") {
|
|
351
|
+
throw new Error(`bind checkbox target must start with an input record`);
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
label: statement.label,
|
|
355
|
+
target,
|
|
356
|
+
recordParam: target.target.name,
|
|
357
|
+
fieldName: target.name,
|
|
358
|
+
};
|
|
359
|
+
}),
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function desugarPage(
|
|
364
|
+
declaration: PointSemanticPageDeclaration,
|
|
365
|
+
records: Map<string, Map<string, string>>,
|
|
366
|
+
callables: Map<string, string>,
|
|
367
|
+
): PointCoreFunctionDeclaration {
|
|
368
|
+
const outputType: PointCoreTypeExpression = { kind: "typeRef", name: "Text", args: [] };
|
|
369
|
+
const pageType: PointCoreTypeExpression = { kind: "typeRef", name: "Page", args: [] };
|
|
370
|
+
const { params, bindings } = collectBindings(declaration.inputs, { name: "page", type: pageType });
|
|
371
|
+
const ctx: DesugarContext = { records, callables, bindings, outputName: "page", outputType };
|
|
372
|
+
const metadata = semanticDeclarationMetadata(declaration);
|
|
373
|
+
metadata.pageLayout = {
|
|
374
|
+
title: desugarExpression(declaration.title, ctx),
|
|
375
|
+
description: declaration.description ? desugarExpression(declaration.description, ctx) : undefined,
|
|
376
|
+
main: desugarExpression(declaration.main, ctx),
|
|
377
|
+
};
|
|
378
|
+
return {
|
|
379
|
+
kind: "function",
|
|
380
|
+
name: semanticFunctionName(declaration.name, "page", "page"),
|
|
381
|
+
params,
|
|
382
|
+
returnType: outputType,
|
|
383
|
+
body: [{ kind: "return", value: metadata.pageLayout.main, span: declaration.span }],
|
|
384
|
+
semantic: metadata,
|
|
306
385
|
span: declaration.span,
|
|
307
386
|
};
|
|
308
387
|
}
|
|
@@ -390,7 +469,7 @@ function desugarParameter(binding: PointSemanticBinding): PointCoreParameter {
|
|
|
390
469
|
}
|
|
391
470
|
|
|
392
471
|
function desugarType(type: PointSemanticTypeExpression): PointCoreTypeExpression {
|
|
393
|
-
if (type.name === "List" || type.name === "Maybe" || type.name === "Or") {
|
|
472
|
+
if (type.name === "List" || type.name === "Maybe" || type.name === "Or" || type.name === "Handler") {
|
|
394
473
|
return { kind: "typeRef", name: type.name, args: type.args.map(desugarType) };
|
|
395
474
|
}
|
|
396
475
|
const primitives = new Set(["Text", "Int", "Float", "Bool", "Void", "Error", "Page"]);
|
|
@@ -32,6 +32,14 @@ export function parseSemanticTypeExpression(source: string): PointSemanticTypeEx
|
|
|
32
32
|
if (maybeMatch) {
|
|
33
33
|
return { kind: "typeRef", name: "Maybe", args: [parseSemanticTypeExpression(maybeMatch[1] ?? "")] };
|
|
34
34
|
}
|
|
35
|
+
const handlerMatch = trimmed.match(/^Handler\s+(.+)$/);
|
|
36
|
+
if (handlerMatch) {
|
|
37
|
+
return { kind: "typeRef", name: "Handler", args: [parseSemanticTypeExpression(handlerMatch[1] ?? "")] };
|
|
38
|
+
}
|
|
39
|
+
const handlerGenericMatch = trimmed.match(/^Handler<(.+)>$/);
|
|
40
|
+
if (handlerGenericMatch) {
|
|
41
|
+
return { kind: "typeRef", name: "Handler", args: [parseSemanticTypeExpression(handlerGenericMatch[1] ?? "")] };
|
|
42
|
+
}
|
|
35
43
|
return { kind: "typeRef", name: trimmed, args: [] };
|
|
36
44
|
}
|
|
37
45
|
|
|
@@ -138,9 +146,8 @@ function parsePrimaryExpression(source: string, context: PointSemanticExpression
|
|
|
138
146
|
return parseRecordExpression(trimmed, context);
|
|
139
147
|
}
|
|
140
148
|
if (trimmed.startsWith('"')) {
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
return { expression: { kind: "literal", value }, consumed: trimmed.slice(end + 1) };
|
|
149
|
+
const literal = parseJsonStringLiteral(trimmed);
|
|
150
|
+
return { expression: { kind: "literal", value: literal.value }, consumed: literal.consumed };
|
|
144
151
|
}
|
|
145
152
|
if (/^(true|false|null|none)\b/.test(trimmed)) {
|
|
146
153
|
const match = trimmed.match(/^(true|false|null|none)\b/);
|
|
@@ -183,6 +190,24 @@ function parsePrimaryExpression(source: string, context: PointSemanticExpression
|
|
|
183
190
|
throw new Error(`Unable to parse semantic expression: ${source}`);
|
|
184
191
|
}
|
|
185
192
|
|
|
193
|
+
function parseJsonStringLiteral(source: string): { value: string; consumed: string } {
|
|
194
|
+
if (!source.startsWith('"')) throw new Error(`Expected string literal: ${source}`);
|
|
195
|
+
let index = 1;
|
|
196
|
+
while (index < source.length) {
|
|
197
|
+
const char = source[index];
|
|
198
|
+
if (char === "\\") {
|
|
199
|
+
index += 2;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (char === '"') {
|
|
203
|
+
const value = JSON.parse(source.slice(0, index + 1)) as string;
|
|
204
|
+
return { value, consumed: source.slice(index + 1) };
|
|
205
|
+
}
|
|
206
|
+
index += 1;
|
|
207
|
+
}
|
|
208
|
+
throw new Error(`Unterminated string literal: ${source}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
186
211
|
function parseListExpression(source: string, context: PointSemanticExpressionContext): { expression: PointSemanticExpression; consumed: string } {
|
|
187
212
|
let rest = source.trim().slice(1).trimStart();
|
|
188
213
|
const items: PointSemanticExpression[] = [];
|
package/src/semantic/format.ts
CHANGED
|
@@ -83,6 +83,14 @@ function formatDeclaration(declaration: PointSemanticDeclaration): string[] {
|
|
|
83
83
|
...formatViewOutput(declaration.output),
|
|
84
84
|
...declaration.body.map((statement) => ` ${formatViewStatement(statement)}`),
|
|
85
85
|
];
|
|
86
|
+
case "page":
|
|
87
|
+
return [
|
|
88
|
+
`page ${declaration.name}`,
|
|
89
|
+
...formatInputs(declaration.inputs),
|
|
90
|
+
` title ${formatExpression(declaration.title)}`,
|
|
91
|
+
...(declaration.description ? [` description ${formatExpression(declaration.description)}`] : []),
|
|
92
|
+
` main render ${formatExpression(declaration.main)}`,
|
|
93
|
+
];
|
|
86
94
|
case "route":
|
|
87
95
|
return [
|
|
88
96
|
`route ${declaration.name}`,
|
|
@@ -176,6 +184,8 @@ function formatPolicyStatement(statement: PointSemanticPolicyStatement): string
|
|
|
176
184
|
|
|
177
185
|
function formatViewStatement(statement: PointSemanticViewStatement): string {
|
|
178
186
|
if (statement.kind === "whenRender") return `when ${formatExpression(statement.condition)} render ${formatExpression(statement.value)}`;
|
|
187
|
+
if (statement.kind === "bindCheckbox") return `bind checkbox "${statement.label}" to ${formatExpression(statement.target)}`;
|
|
188
|
+
if (statement.kind === "onChangeCall") return `on change call ${statement.callback}`;
|
|
179
189
|
return `render ${formatExpression(statement.value)}`;
|
|
180
190
|
}
|
|
181
191
|
|
package/src/semantic/metadata.ts
CHANGED
|
@@ -30,6 +30,9 @@ export function semanticDeclarationMetadata(declaration: PointSemanticDeclaratio
|
|
|
30
30
|
effects: [],
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
+
if (declaration.kind === "page") {
|
|
34
|
+
return { kind: "page", name: declaration.name, outputName: "page", effects: [] };
|
|
35
|
+
}
|
|
33
36
|
if (declaration.kind === "route") {
|
|
34
37
|
return { kind: "route", name: declaration.name, outputName: declaration.output.name, effects: [] };
|
|
35
38
|
}
|
package/src/semantic/naming.ts
CHANGED
|
@@ -11,7 +11,7 @@ export function toIdentifier(label: string): string {
|
|
|
11
11
|
export function semanticFunctionName(
|
|
12
12
|
label: string,
|
|
13
13
|
outputName: string,
|
|
14
|
-
kind: "calculation" | "rule" | "label" | "action" | "policy" | "view" | "route" | "workflow" | "command",
|
|
14
|
+
kind: "calculation" | "rule" | "label" | "action" | "policy" | "view" | "page" | "route" | "workflow" | "command",
|
|
15
15
|
): string {
|
|
16
16
|
const base = toIdentifier(label);
|
|
17
17
|
const suffix =
|
|
@@ -21,7 +21,9 @@ export function semanticFunctionName(
|
|
|
21
21
|
? "Policy"
|
|
22
22
|
: kind === "view"
|
|
23
23
|
? "View"
|
|
24
|
-
: kind === "
|
|
24
|
+
: kind === "page"
|
|
25
|
+
? "Page"
|
|
26
|
+
: kind === "route"
|
|
25
27
|
? "Route"
|
|
26
28
|
: kind === "workflow"
|
|
27
29
|
? "Workflow"
|
package/src/semantic/parse.ts
CHANGED
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
PointSemanticUseDeclaration,
|
|
26
26
|
PointSemanticViewDeclaration,
|
|
27
27
|
PointSemanticViewStatement,
|
|
28
|
+
PointSemanticPageDeclaration,
|
|
28
29
|
PointSemanticWorkflowDeclaration,
|
|
29
30
|
PointSemanticWorkflowStatement,
|
|
30
31
|
PointSemanticBinding,
|
|
@@ -129,6 +130,12 @@ export function parseSemanticSource(source: string): PointSemanticProgram {
|
|
|
129
130
|
index = parsed.next;
|
|
130
131
|
continue;
|
|
131
132
|
}
|
|
133
|
+
if (trimmed.startsWith("page ")) {
|
|
134
|
+
const parsed = parsePage(lines, index, source, records, callables);
|
|
135
|
+
declarations.push(parsed.declaration);
|
|
136
|
+
index = parsed.next;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
132
139
|
if (trimmed.startsWith("route ")) {
|
|
133
140
|
const parsed = parseRoute(lines, index, source, records, callables);
|
|
134
141
|
declarations.push(parsed.declaration);
|
|
@@ -586,6 +593,24 @@ function parseView(
|
|
|
586
593
|
});
|
|
587
594
|
continue;
|
|
588
595
|
}
|
|
596
|
+
const bindCheckbox = line.match(/^bind checkbox "(.+)" to (.+)$/);
|
|
597
|
+
if (bindCheckbox) {
|
|
598
|
+
statements.push({
|
|
599
|
+
kind: "bindCheckbox",
|
|
600
|
+
label: bindCheckbox[1] ?? "",
|
|
601
|
+
target: parseLineExpression(bindCheckbox[2] ?? "", context, source, lineNumber),
|
|
602
|
+
span: lineSpan(source, lineNumber),
|
|
603
|
+
});
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
if (line.startsWith("on change call ")) {
|
|
607
|
+
statements.push({
|
|
608
|
+
kind: "onChangeCall",
|
|
609
|
+
callback: line.slice("on change call ".length).trim(),
|
|
610
|
+
span: lineSpan(source, lineNumber),
|
|
611
|
+
});
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
589
614
|
throw new Error(`Unknown view statement: ${line}`);
|
|
590
615
|
}
|
|
591
616
|
|
|
@@ -595,6 +620,57 @@ function parseView(
|
|
|
595
620
|
};
|
|
596
621
|
}
|
|
597
622
|
|
|
623
|
+
function parsePage(
|
|
624
|
+
lines: string[],
|
|
625
|
+
start: number,
|
|
626
|
+
source: string,
|
|
627
|
+
records: Map<string, Map<string, string>>,
|
|
628
|
+
callables: string[],
|
|
629
|
+
): { declaration: PointSemanticPageDeclaration; next: number } {
|
|
630
|
+
const name = (lines[start] ?? "").trim().slice("page ".length).trim();
|
|
631
|
+
const body = collectSemanticBody(lines, start + 1);
|
|
632
|
+
const inputs: PointSemanticBinding[] = [];
|
|
633
|
+
const paramTypes = new Map<string, string>();
|
|
634
|
+
const bindings: string[] = [];
|
|
635
|
+
let title: PointSemanticPageDeclaration["title"] | undefined;
|
|
636
|
+
let description: PointSemanticPageDeclaration["description"];
|
|
637
|
+
let main: PointSemanticPageDeclaration["main"] | undefined;
|
|
638
|
+
|
|
639
|
+
for (let lineIndex = 0; lineIndex < body.lines.length; lineIndex += 1) {
|
|
640
|
+
const line = body.lines[lineIndex] ?? "";
|
|
641
|
+
const lineNumber = body.lineNumbers[lineIndex] ?? start + 2;
|
|
642
|
+
if (line.startsWith("input ")) {
|
|
643
|
+
const binding = parseInputBinding(line.slice("input ".length), source, lineNumber);
|
|
644
|
+
inputs.push(binding);
|
|
645
|
+
paramTypes.set(binding.label, typeLabel(binding.type));
|
|
646
|
+
bindings.push(binding.label);
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
const context = buildExpressionContext({ bindings, paramTypes, recordFields: records, callables });
|
|
650
|
+
if (line.startsWith("title ")) {
|
|
651
|
+
title = parseLineExpression(line.slice("title ".length), context, source, lineNumber);
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
if (line.startsWith("description ")) {
|
|
655
|
+
description = parseLineExpression(line.slice("description ".length), context, source, lineNumber);
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
if (line.startsWith("main render ")) {
|
|
659
|
+
main = parseLineExpression(line.slice("main render ".length), context, source, lineNumber);
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
throw new Error(`Unknown page statement: ${line}`);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (!title) throw new Error(`Page ${name} requires a title`);
|
|
666
|
+
if (!main) throw new Error(`Page ${name} requires main render`);
|
|
667
|
+
|
|
668
|
+
return {
|
|
669
|
+
declaration: { kind: "page", name, inputs, title, description, main, span: lineSpan(source, start + 1) },
|
|
670
|
+
next: body.next,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
|
|
598
674
|
function parseRoute(
|
|
599
675
|
lines: string[],
|
|
600
676
|
start: number,
|
|
@@ -893,7 +969,7 @@ function collectSemanticBody(lines: string[], start: number): SemanticBody {
|
|
|
893
969
|
}
|
|
894
970
|
|
|
895
971
|
function isSemanticTopLevel(line: string): boolean {
|
|
896
|
-
return /^(module|use|record|calculation|rule|label|external|action|policy|view|route|workflow|command)\s+/.test(line);
|
|
972
|
+
return /^(module|use|record|calculation|rule|label|external|action|policy|view|page|route|workflow|command)\s+/.test(line);
|
|
897
973
|
}
|
|
898
974
|
|
|
899
975
|
function isLoopBoundary(line: string): boolean {
|
|
@@ -914,7 +990,10 @@ function typeLabel(type: { kind: "typeRef"; name: string; args: unknown[] }): st
|
|
|
914
990
|
if (type.name === "Maybe" && type.args[0]) {
|
|
915
991
|
return `Maybe<${typeLabel(type.args[0] as { kind: "typeRef"; name: string; args: unknown[] })}>`;
|
|
916
992
|
}
|
|
917
|
-
|
|
993
|
+
if (type.name === "Handler" && type.args[0]) {
|
|
994
|
+
return `Handler ${typeLabel(type.args[0] as { kind: "typeRef"; name: string; args: unknown[] })}`;
|
|
995
|
+
}
|
|
996
|
+
const primitives = new Set(["Text", "Int", "Float", "Bool", "Void", "Maybe", "Or", "Error", "Page", "Handler"]);
|
|
918
997
|
if (primitives.has(type.name)) return type.name;
|
|
919
998
|
return toPascalCase(type.name);
|
|
920
999
|
}
|
package/src/std/http.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
type PointStdError = { message: string };
|
|
2
|
+
|
|
3
|
+
async function readResponseText(response: Response): Promise<string | PointStdError> {
|
|
4
|
+
if (!response.ok) {
|
|
5
|
+
return { message: `HTTP ${response.status}: ${response.statusText}` };
|
|
6
|
+
}
|
|
7
|
+
return response.text();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function httpGet(url: string): Promise<string | PointStdError> {
|
|
11
|
+
try {
|
|
12
|
+
return await readResponseText(await fetch(url));
|
|
13
|
+
} catch (error) {
|
|
14
|
+
return { message: error instanceof Error ? error.message : String(error) };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function httpPost(url: string, body: string): Promise<string | PointStdError> {
|
|
19
|
+
try {
|
|
20
|
+
return await readResponseText(
|
|
21
|
+
await fetch(url, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
|
24
|
+
body,
|
|
25
|
+
}),
|
|
26
|
+
);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return { message: error instanceof Error ? error.message : String(error) };
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/std/json.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type PointStdError = { message: string };
|
|
2
|
+
|
|
3
|
+
export function jsonParse(value: string): string | PointStdError {
|
|
4
|
+
try {
|
|
5
|
+
return JSON.stringify(JSON.parse(value));
|
|
6
|
+
} catch (error) {
|
|
7
|
+
return { message: error instanceof Error ? error.message : String(error) };
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function jsonStringify(value: string): string {
|
|
12
|
+
return JSON.stringify(JSON.parse(value));
|
|
13
|
+
}
|