@samva/markup 0.1.1 → 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/AGENTS.md +25 -5
- package/README.md +92 -26
- package/dist/{ast-DAhnAvQN.d.ts → ast-63I-VLQx.d.ts} +15 -2
- package/dist/ast.d.ts +2 -2
- package/dist/ast.js +15 -2
- package/dist/{checks-CW0nFj5n.js → checks-DXuERO4B.js} +37 -3
- package/dist/checks.d.ts +8 -1
- package/dist/checks.js +2 -2
- package/dist/compile-sms.d.ts +2 -2
- package/dist/compile-whatsapp.d.ts +2 -2
- package/dist/compile.d.ts +9 -84
- package/dist/compile.js +11 -920
- package/dist/compiler.d.ts +301 -0
- package/dist/compiler.js +1389 -0
- package/dist/components.d.ts +4 -4
- package/dist/conditionals.js +1 -1
- package/dist/corpus-Dz5irJ9x.d.ts +161 -0
- package/dist/diagnostics-Djd9ckLg.d.ts +90 -0
- package/dist/diagnostics.js +6 -3
- package/dist/elements-DtwKlFVH.d.ts +820 -0
- package/dist/email/components.d.ts +119 -0
- package/dist/email/components.js +207 -0
- package/dist/email/diagnostics.d.ts +2 -0
- package/dist/email/diagnostics.js +56 -0
- package/dist/email/elements.d.ts +2 -0
- package/dist/email/elements.js +299 -0
- package/dist/email/html.d.ts +42 -0
- package/dist/email/html.js +266 -0
- package/dist/email/jsx-dev-runtime.d.ts +26 -0
- package/dist/email/jsx-dev-runtime.js +12 -0
- package/dist/email/jsx-runtime.d.ts +2 -0
- package/dist/email/jsx-runtime.js +60 -0
- package/dist/email/render.d.ts +36 -0
- package/dist/email/render.js +6 -0
- package/dist/email/selection.d.ts +102 -0
- package/dist/email/selection.js +143 -0
- package/dist/email/source-edit.d.ts +67 -0
- package/dist/email/source-edit.js +185 -0
- package/dist/email/stylesheets.d.ts +2 -0
- package/dist/email/stylesheets.js +43 -0
- package/dist/email/text.d.ts +7 -0
- package/dist/email/text.js +92 -0
- package/dist/emit.d.ts +13 -12
- package/dist/emit.js +63 -63
- package/dist/input-schema-BIfzI63B.d.ts +23 -0
- package/dist/input-schema.d.ts +2 -0
- package/dist/input-schema.js +170 -0
- package/dist/jsx-dev-runtime.d.ts +4 -3
- package/dist/jsx-dev-runtime.js +9 -1
- package/dist/{jsx-runtime-fSxcvlx4.d.ts → jsx-runtime-C5k_Zq0A.d.ts} +4 -4
- package/dist/jsx-runtime.d.ts +1 -1
- package/dist/jsx-runtime.js +20 -1
- package/dist/model.d.ts +1 -1
- package/dist/ops.d.ts +1 -1
- package/dist/ops.js +2 -2
- package/dist/{parser-yzjUOGhc.d.ts → parser-CGBS73Gz.d.ts} +12 -3
- package/dist/parser.d.ts +2 -2
- package/dist/parser.js +48 -9
- package/dist/predicate-CvkzT-a2.js +539 -0
- package/dist/{predicate-DTtVtLK7.d.ts → predicate-G2SeSvoN.d.ts} +9 -2
- package/dist/predicate.d.ts +2 -2
- package/dist/predicate.js +2 -417
- package/dist/presets/corpus.d.ts +2 -0
- package/dist/presets/corpus.js +186 -0
- package/dist/presets/public-gallery.d.ts +241 -459
- package/dist/presets/public-gallery.js +305 -5
- package/dist/presets/public-handoff.d.ts +75 -3
- package/dist/presets/public-handoff.js +220 -37
- package/dist/presets/public-preview-artifacts.js +20 -26
- package/dist/presets/public-preview.d.ts +40 -1152
- package/dist/presets/public-preview.js +388 -1152
- package/dist/presets/sources.d.ts +17 -4
- package/dist/presets/sources.js +1212 -43
- package/dist/presets.d.ts +2 -2
- package/dist/printer.d.ts +1 -1
- package/dist/render-Cddsy_XF.js +585 -0
- package/dist/render-template.d.ts +6 -3
- package/dist/render-template.js +4 -2
- package/dist/source-locations-BCJQnIfU.d.ts +89 -0
- package/dist/source-locations.d.ts +2 -0
- package/dist/source-locations.js +125 -0
- package/dist/stylesheets-oDRAnYA3.d.ts +94 -0
- package/dist/template-Dy5yOPyp.d.ts +60 -0
- package/dist/template.d.ts +2 -0
- package/dist/template.js +37 -0
- package/dist/tsx-source-instrumentation.d.ts +23 -0
- package/dist/tsx-source-instrumentation.js +97 -0
- package/dist/tsx-tracer.d.ts +116 -0
- package/dist/tsx-tracer.js +985 -0
- package/dist/{variables-3NGLpNTB.d.ts → variables-if1VrV34.d.ts} +2 -2
- package/dist/variables.d.ts +1 -1
- package/dist/variables.js +1 -1
- package/generated/sml-agent-contract.json +574 -782
- package/generated/sml-email-agent-contract.json +502 -677
- package/generated/sml-email-language-reference.md +94 -160
- package/generated/sml-language-reference.md +195 -220
- package/package.json +111 -8
package/dist/parser.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { SmlParseError } from "./diagnostics.js";
|
|
2
|
-
import { parsePredicate,
|
|
2
|
+
import { a as printPredicate, i as parsePredicate, o as SML_PARSER_LIMITS } from "./predicate-CvkzT-a2.js";
|
|
3
3
|
import { CHANNEL_BY_ROOT_TAG, ROOT_TAGS, SML_TAGS, isRootTag } from "./ast.js";
|
|
4
4
|
import { TokenKind, createLexer } from "./lexer.js";
|
|
5
5
|
import { Result } from "effect";
|
|
6
6
|
//#region src/parser.ts
|
|
7
|
+
const parserLimitFailure = (code, message, sourceLength) => Result.fail(new SmlParseError({ diagnostics: [{
|
|
8
|
+
code,
|
|
9
|
+
message,
|
|
10
|
+
span: {
|
|
11
|
+
start: 0,
|
|
12
|
+
end: sourceLength
|
|
13
|
+
}
|
|
14
|
+
}] }));
|
|
7
15
|
const SMS_CATEGORIES = [
|
|
8
16
|
"transactional",
|
|
9
17
|
"promotional",
|
|
@@ -413,8 +421,12 @@ const WRAPPER_TAG_BY_MODEL = {
|
|
|
413
421
|
buttons: "Button"
|
|
414
422
|
};
|
|
415
423
|
const parseSml = (source) => {
|
|
424
|
+
if (source.length > SML_PARSER_LIMITS.maxCharacters) return parserLimitFailure("source-character-limit", `SML source exceeds the ${SML_PARSER_LIMITS.maxCharacters} character limit (${source.length} characters)`, source.length);
|
|
425
|
+
const sourceBytes = new TextEncoder().encode(source).byteLength;
|
|
426
|
+
if (sourceBytes > SML_PARSER_LIMITS.maxUtf8Bytes) return parserLimitFailure("source-byte-limit", `SML source exceeds the ${SML_PARSER_LIMITS.maxUtf8Bytes} byte UTF-8 limit (${sourceBytes} bytes)`, source.length);
|
|
416
427
|
const lexer = createLexer(source);
|
|
417
428
|
let diagnostic;
|
|
429
|
+
let nodeCount = 0;
|
|
418
430
|
let channel = null;
|
|
419
431
|
let specs = EMAIL_TAG_SPECS;
|
|
420
432
|
const fail = (code, message, span) => {
|
|
@@ -435,6 +447,15 @@ const parseSml = (source) => {
|
|
|
435
447
|
end: token.end
|
|
436
448
|
});
|
|
437
449
|
};
|
|
450
|
+
const countNode = (span) => {
|
|
451
|
+
const observed = nodeCount + 1;
|
|
452
|
+
if (observed > SML_PARSER_LIMITS.maxNodes) {
|
|
453
|
+
fail("node-limit", `SML document exceeds the ${SML_PARSER_LIMITS.maxNodes} node limit (${observed} nodes)`, span);
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
nodeCount = observed;
|
|
457
|
+
return true;
|
|
458
|
+
};
|
|
438
459
|
const parseAttributeValue = (tag, name, spec, nameToken) => {
|
|
439
460
|
const nameSpan = {
|
|
440
461
|
start: nameToken.start,
|
|
@@ -519,7 +540,11 @@ const parseSml = (source) => {
|
|
|
519
540
|
return true;
|
|
520
541
|
};
|
|
521
542
|
/** Called with the `<` already consumed; `lessThanToken` is that token. */
|
|
522
|
-
const parseElement = (lessThanToken) => {
|
|
543
|
+
const parseElement = (lessThanToken, depth) => {
|
|
544
|
+
if (depth > SML_PARSER_LIMITS.maxDepth) return fail("depth-limit", `SML nesting exceeds the maximum depth of ${SML_PARSER_LIMITS.maxDepth} (observed ${depth})`, {
|
|
545
|
+
start: lessThanToken.start,
|
|
546
|
+
end: lessThanToken.end
|
|
547
|
+
});
|
|
523
548
|
const tagToken = lexer.scanTagToken();
|
|
524
549
|
if (tagToken.kind !== TokenKind.Identifier) {
|
|
525
550
|
if (tagToken.kind === TokenKind.Unknown) return failToken(tagToken);
|
|
@@ -548,6 +573,10 @@ const parseSml = (source) => {
|
|
|
548
573
|
end: tagToken.end
|
|
549
574
|
});
|
|
550
575
|
}
|
|
576
|
+
if (!countNode({
|
|
577
|
+
start: lessThanToken.start,
|
|
578
|
+
end: tagToken.end
|
|
579
|
+
})) return null;
|
|
551
580
|
const element = {
|
|
552
581
|
kind: "element",
|
|
553
582
|
id: "",
|
|
@@ -662,6 +691,10 @@ const parseSml = (source) => {
|
|
|
662
691
|
start: token.start,
|
|
663
692
|
end: token.end
|
|
664
693
|
});
|
|
694
|
+
if (!countNode({
|
|
695
|
+
start: token.start,
|
|
696
|
+
end: token.end
|
|
697
|
+
})) return null;
|
|
665
698
|
element.children.push({
|
|
666
699
|
kind: "text",
|
|
667
700
|
id: "",
|
|
@@ -686,6 +719,10 @@ const parseSml = (source) => {
|
|
|
686
719
|
start: token.start,
|
|
687
720
|
end: token.end
|
|
688
721
|
});
|
|
722
|
+
if (!countNode({
|
|
723
|
+
start: token.start,
|
|
724
|
+
end: token.end
|
|
725
|
+
})) return null;
|
|
689
726
|
element.children.push({
|
|
690
727
|
kind: "interpolation",
|
|
691
728
|
id: "",
|
|
@@ -716,7 +753,7 @@ const parseSml = (source) => {
|
|
|
716
753
|
break;
|
|
717
754
|
}
|
|
718
755
|
const allowed = allowedChildren(model);
|
|
719
|
-
const child = parseElement(token);
|
|
756
|
+
const child = parseElement(token, depth + 1);
|
|
720
757
|
if (child === null) return null;
|
|
721
758
|
if (!allowed.includes(child.tag)) {
|
|
722
759
|
const message = child.tag === "Else" ? `<Else> is only valid as the last child of <If> — wrap this branch in <If>` : isInlineModel(model) ? child.tag === "a" ? `<a> is not allowed inside <${tag}> — links cannot nest inside links or buttons` : `<${child.tag}> is not allowed inside <${tag}> — only text, {{variables}}${allowed.length > 0 ? `, and ${listTags(allowed)}` : ""} may appear here` : `<${child.tag}> is not allowed inside <${tag}> — <${tag}> may contain only ${listTags(allowed)}`;
|
|
@@ -767,7 +804,7 @@ const parseSml = (source) => {
|
|
|
767
804
|
end: first.end
|
|
768
805
|
});
|
|
769
806
|
let root = null;
|
|
770
|
-
if (diagnostic === void 0) root = parseElement(first);
|
|
807
|
+
if (diagnostic === void 0) root = parseElement(first, 1);
|
|
771
808
|
if (root !== null) {
|
|
772
809
|
const trailing = skipBlankText();
|
|
773
810
|
if (trailing.kind !== TokenKind.EndOfFile) {
|
|
@@ -808,14 +845,16 @@ const normalizeInlineChildren = (element) => {
|
|
|
808
845
|
/** Stable ids in document order — identical source ⇒ identical ids. */
|
|
809
846
|
const assignNodeIds = (root) => {
|
|
810
847
|
let next = 0;
|
|
811
|
-
const
|
|
848
|
+
const pending = [root];
|
|
849
|
+
while (pending.length > 0) {
|
|
850
|
+
const node = pending.pop();
|
|
812
851
|
node.id = `n${next}`;
|
|
813
852
|
next++;
|
|
814
|
-
if (node.kind
|
|
815
|
-
|
|
816
|
-
|
|
853
|
+
if (node.kind !== "element") continue;
|
|
854
|
+
for (let index = node.children.length - 1; index >= 0; index--) pending.push(node.children[index]);
|
|
855
|
+
}
|
|
817
856
|
};
|
|
818
857
|
//#endregion
|
|
819
|
-
export { CHANNEL_TAG_SPECS, EMAIL_TAG_SPECS, SMS_TAG_SPECS, UNIVERSAL_ATTRIBUTES, UNIVERSAL_ATTRIBUTE_TAGS, WHATSAPP_TAG_SPECS, allowedChildren, parseSml };
|
|
858
|
+
export { CHANNEL_TAG_SPECS, EMAIL_TAG_SPECS, SML_PARSER_LIMITS, SMS_TAG_SPECS, UNIVERSAL_ATTRIBUTES, UNIVERSAL_ATTRIBUTE_TAGS, WHATSAPP_TAG_SPECS, allowedChildren, parseSml };
|
|
820
859
|
|
|
821
860
|
//# sourceMappingURL=parser.js.map
|
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
import { Result } from "effect";
|
|
2
|
+
//#region src/limits.ts
|
|
3
|
+
/** Hard ceilings shared by SML documents and standalone predicate parsing. */
|
|
4
|
+
const SML_PARSER_LIMITS = {
|
|
5
|
+
maxCharacters: 5e5,
|
|
6
|
+
maxUtf8Bytes: 1048576,
|
|
7
|
+
maxDepth: 64,
|
|
8
|
+
maxNodes: 25e3
|
|
9
|
+
};
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/predicate.ts
|
|
12
|
+
/** Standalone predicates inherit the same source, nesting, and node ceilings as SML. */
|
|
13
|
+
const PREDICATE_PARSER_LIMITS = SML_PARSER_LIMITS;
|
|
14
|
+
const CHAR_TAB = 9;
|
|
15
|
+
const CHAR_LINE_FEED = 10;
|
|
16
|
+
const CHAR_CARRIAGE_RETURN = 13;
|
|
17
|
+
const CHAR_SPACE = 32;
|
|
18
|
+
const CHAR_BANG = 33;
|
|
19
|
+
const CHAR_SINGLE_QUOTE = 39;
|
|
20
|
+
const CHAR_OPEN_PAREN = 40;
|
|
21
|
+
const CHAR_CLOSE_PAREN = 41;
|
|
22
|
+
const CHAR_MINUS = 45;
|
|
23
|
+
const CHAR_DOT = 46;
|
|
24
|
+
const CHAR_LESS_THAN = 60;
|
|
25
|
+
const CHAR_EQUALS = 61;
|
|
26
|
+
const CHAR_GREATER_THAN = 62;
|
|
27
|
+
const CHAR_UNDERSCORE = 95;
|
|
28
|
+
const CHAR_0 = 48;
|
|
29
|
+
const CHAR_9 = 57;
|
|
30
|
+
const CHAR_UPPER_A = 65;
|
|
31
|
+
const CHAR_UPPER_Z = 90;
|
|
32
|
+
const CHAR_LOWER_A = 97;
|
|
33
|
+
const CHAR_LOWER_Z = 122;
|
|
34
|
+
const isWhitespace = (code) => code === CHAR_SPACE || code === CHAR_TAB || code === CHAR_LINE_FEED || code === CHAR_CARRIAGE_RETURN;
|
|
35
|
+
const isDigit = (code) => code >= CHAR_0 && code <= CHAR_9;
|
|
36
|
+
const isLetter = (code) => code >= CHAR_UPPER_A && code <= CHAR_UPPER_Z || code >= CHAR_LOWER_A && code <= CHAR_LOWER_Z;
|
|
37
|
+
const isNameStart = (code) => isLetter(code) || code === CHAR_UNDERSCORE;
|
|
38
|
+
const isNameChar = (code) => isNameStart(code) || isDigit(code);
|
|
39
|
+
const KEYWORDS = [
|
|
40
|
+
"and",
|
|
41
|
+
"or",
|
|
42
|
+
"not"
|
|
43
|
+
];
|
|
44
|
+
const isKeyword = (text) => KEYWORDS.includes(text);
|
|
45
|
+
const tokenize = (source, baseOffset) => {
|
|
46
|
+
const tokens = [];
|
|
47
|
+
let index = 0;
|
|
48
|
+
const length = source.length;
|
|
49
|
+
const failAt = (code, message, start, end) => Result.fail({ diagnostic: {
|
|
50
|
+
code,
|
|
51
|
+
message,
|
|
52
|
+
span: {
|
|
53
|
+
start: baseOffset + start,
|
|
54
|
+
end: baseOffset + end
|
|
55
|
+
}
|
|
56
|
+
} });
|
|
57
|
+
while (index < length) {
|
|
58
|
+
const code = source.charCodeAt(index);
|
|
59
|
+
if (isWhitespace(code)) {
|
|
60
|
+
index++;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const start = index;
|
|
64
|
+
if (code === CHAR_OPEN_PAREN) {
|
|
65
|
+
index++;
|
|
66
|
+
tokens.push({
|
|
67
|
+
kind: "lparen",
|
|
68
|
+
text: "(",
|
|
69
|
+
span: {
|
|
70
|
+
start: baseOffset + start,
|
|
71
|
+
end: baseOffset + index
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (code === CHAR_CLOSE_PAREN) {
|
|
77
|
+
index++;
|
|
78
|
+
tokens.push({
|
|
79
|
+
kind: "rparen",
|
|
80
|
+
text: ")",
|
|
81
|
+
span: {
|
|
82
|
+
start: baseOffset + start,
|
|
83
|
+
end: baseOffset + index
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (code === CHAR_EQUALS || code === CHAR_BANG) {
|
|
89
|
+
if (source.charCodeAt(index + 1) === CHAR_EQUALS) {
|
|
90
|
+
index += 2;
|
|
91
|
+
tokens.push({
|
|
92
|
+
kind: "op",
|
|
93
|
+
text: code === CHAR_EQUALS ? "==" : "!=",
|
|
94
|
+
span: {
|
|
95
|
+
start: baseOffset + start,
|
|
96
|
+
end: baseOffset + index
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
return failAt("predicate-invalid-operator", `unexpected ${String.fromCharCode(code)} — ${code === CHAR_EQUALS ? "write == to compare" : "write != to compare"}`, start, start + 1);
|
|
102
|
+
}
|
|
103
|
+
if (code === CHAR_GREATER_THAN || code === CHAR_LESS_THAN) {
|
|
104
|
+
index++;
|
|
105
|
+
if (source.charCodeAt(index) === CHAR_EQUALS) index++;
|
|
106
|
+
tokens.push({
|
|
107
|
+
kind: "op",
|
|
108
|
+
text: source.slice(start, index),
|
|
109
|
+
span: {
|
|
110
|
+
start: baseOffset + start,
|
|
111
|
+
end: baseOffset + index
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (code === CHAR_SINGLE_QUOTE) {
|
|
117
|
+
index++;
|
|
118
|
+
const contentStart = index;
|
|
119
|
+
while (index < length && source.charCodeAt(index) !== CHAR_SINGLE_QUOTE) index++;
|
|
120
|
+
if (index >= length) return failAt("predicate-unterminated-string", "unterminated string — add a closing '", start, length);
|
|
121
|
+
const value = source.slice(contentStart, index);
|
|
122
|
+
index++;
|
|
123
|
+
if (value.includes("--")) return failAt("predicate-string-double-dash", "string literals cannot contain -- (predicates are embedded in HTML comment markers)", start, index);
|
|
124
|
+
tokens.push({
|
|
125
|
+
kind: "string",
|
|
126
|
+
text: source.slice(start, index),
|
|
127
|
+
string: value,
|
|
128
|
+
span: {
|
|
129
|
+
start: baseOffset + start,
|
|
130
|
+
end: baseOffset + index
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (isDigit(code) || code === CHAR_MINUS && isDigit(source.charCodeAt(index + 1))) {
|
|
136
|
+
index++;
|
|
137
|
+
while (index < length && isDigit(source.charCodeAt(index))) index++;
|
|
138
|
+
if (source.charCodeAt(index) === CHAR_DOT && isDigit(source.charCodeAt(index + 1))) {
|
|
139
|
+
index++;
|
|
140
|
+
while (index < length && isDigit(source.charCodeAt(index))) index++;
|
|
141
|
+
}
|
|
142
|
+
tokens.push({
|
|
143
|
+
kind: "number",
|
|
144
|
+
text: source.slice(start, index),
|
|
145
|
+
number: Number.parseFloat(source.slice(start, index)),
|
|
146
|
+
span: {
|
|
147
|
+
start: baseOffset + start,
|
|
148
|
+
end: baseOffset + index
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (isNameStart(code)) {
|
|
154
|
+
index++;
|
|
155
|
+
while (index < length && isNameChar(source.charCodeAt(index))) index++;
|
|
156
|
+
if (source.charCodeAt(index) === CHAR_DOT) return failAt("predicate-path-not-supported", `unexpected . after "${source.slice(start, index)}" — predicates use flat variable names, not paths`, start, index + 1);
|
|
157
|
+
tokens.push({
|
|
158
|
+
kind: "name",
|
|
159
|
+
text: source.slice(start, index),
|
|
160
|
+
span: {
|
|
161
|
+
start: baseOffset + start,
|
|
162
|
+
end: baseOffset + index
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (code === CHAR_DOT) return failAt("predicate-path-not-supported", "unexpected . — predicates use flat variable names, not paths", start, start + 1);
|
|
168
|
+
return failAt("predicate-unexpected-character", `unexpected character ${JSON.stringify(String.fromCharCode(code))} in predicate`, start, start + 1);
|
|
169
|
+
}
|
|
170
|
+
tokens.push({
|
|
171
|
+
kind: "eof",
|
|
172
|
+
text: "",
|
|
173
|
+
span: {
|
|
174
|
+
start: baseOffset + length,
|
|
175
|
+
end: baseOffset + length
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
return Result.succeed(tokens);
|
|
179
|
+
};
|
|
180
|
+
const COMPARISON_OPS = [
|
|
181
|
+
"==",
|
|
182
|
+
"!=",
|
|
183
|
+
">",
|
|
184
|
+
">=",
|
|
185
|
+
"<",
|
|
186
|
+
"<="
|
|
187
|
+
];
|
|
188
|
+
/**
|
|
189
|
+
* Parses a predicate. `baseOffset` is the absolute source offset of the
|
|
190
|
+
* predicate's first character (an <If> test attribute passes valueSpan.start),
|
|
191
|
+
* so diagnostic and node spans point into the containing document.
|
|
192
|
+
*/
|
|
193
|
+
const parsePredicate = (source, baseOffset = 0) => {
|
|
194
|
+
if (source.length > SML_PARSER_LIMITS.maxCharacters) return Result.fail([{
|
|
195
|
+
code: "predicate-character-limit",
|
|
196
|
+
message: `predicate exceeds the ${SML_PARSER_LIMITS.maxCharacters} character limit`,
|
|
197
|
+
span: {
|
|
198
|
+
start: baseOffset,
|
|
199
|
+
end: baseOffset + source.length
|
|
200
|
+
}
|
|
201
|
+
}]);
|
|
202
|
+
if (new TextEncoder().encode(source).byteLength > SML_PARSER_LIMITS.maxUtf8Bytes) return Result.fail([{
|
|
203
|
+
code: "predicate-byte-limit",
|
|
204
|
+
message: `predicate exceeds the ${SML_PARSER_LIMITS.maxUtf8Bytes} byte UTF-8 limit`,
|
|
205
|
+
span: {
|
|
206
|
+
start: baseOffset,
|
|
207
|
+
end: baseOffset + source.length
|
|
208
|
+
}
|
|
209
|
+
}]);
|
|
210
|
+
const lexed = tokenize(source, baseOffset);
|
|
211
|
+
if (Result.isFailure(lexed)) return Result.fail([lexed.failure.diagnostic]);
|
|
212
|
+
const tokens = lexed.success;
|
|
213
|
+
let index = 0;
|
|
214
|
+
let diagnostic;
|
|
215
|
+
let nodeCount = 0;
|
|
216
|
+
const fail = (code, message, span) => {
|
|
217
|
+
diagnostic ??= {
|
|
218
|
+
code,
|
|
219
|
+
message,
|
|
220
|
+
span
|
|
221
|
+
};
|
|
222
|
+
return null;
|
|
223
|
+
};
|
|
224
|
+
const peek = () => tokens[index];
|
|
225
|
+
const advance = () => tokens[index++];
|
|
226
|
+
const isKeywordToken = (token, keyword) => token.kind === "name" && token.text === keyword;
|
|
227
|
+
const countNode = (node) => {
|
|
228
|
+
nodeCount += 1;
|
|
229
|
+
return nodeCount > SML_PARSER_LIMITS.maxNodes ? fail("predicate-node-limit", `predicate exceeds the ${SML_PARSER_LIMITS.maxNodes} node limit`, node.span) : node;
|
|
230
|
+
};
|
|
231
|
+
const parsePrimary = (depth) => {
|
|
232
|
+
const token = advance();
|
|
233
|
+
if (token.kind === "lparen") {
|
|
234
|
+
if (depth >= SML_PARSER_LIMITS.maxDepth) return fail("predicate-depth-limit", `predicate nesting exceeds the maximum depth of ${SML_PARSER_LIMITS.maxDepth}`, token.span);
|
|
235
|
+
const inner = parseOr(depth + 1);
|
|
236
|
+
if (inner === null) return null;
|
|
237
|
+
const close = advance();
|
|
238
|
+
if (close.kind !== "rparen") return fail("predicate-missing-paren", "missing ) — close the group", close.span);
|
|
239
|
+
return inner;
|
|
240
|
+
}
|
|
241
|
+
if (token.kind === "name") {
|
|
242
|
+
if (isKeyword(token.text)) return fail("predicate-unexpected-token", `unexpected "${token.text}" — expected a variable name, comparison, or ( group )`, token.span);
|
|
243
|
+
const opToken = peek();
|
|
244
|
+
if (opToken.kind !== "op") return countNode({
|
|
245
|
+
kind: "name",
|
|
246
|
+
name: token.text,
|
|
247
|
+
span: token.span
|
|
248
|
+
});
|
|
249
|
+
advance();
|
|
250
|
+
const op = COMPARISON_OPS.find((candidate) => candidate === opToken.text);
|
|
251
|
+
if (op === void 0) return fail("predicate-invalid-operator", `unknown operator ${opToken.text} — use ==, !=, >, >=, <, or <=`, opToken.span);
|
|
252
|
+
const literalToken = advance();
|
|
253
|
+
if (literalToken.kind === "string") return countNode({
|
|
254
|
+
kind: "comparison",
|
|
255
|
+
name: token.text,
|
|
256
|
+
op,
|
|
257
|
+
literal: {
|
|
258
|
+
kind: "string",
|
|
259
|
+
value: literalToken.string
|
|
260
|
+
},
|
|
261
|
+
span: {
|
|
262
|
+
start: token.span.start,
|
|
263
|
+
end: literalToken.span.end
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
if (literalToken.kind === "number") return countNode({
|
|
267
|
+
kind: "comparison",
|
|
268
|
+
name: token.text,
|
|
269
|
+
op,
|
|
270
|
+
literal: {
|
|
271
|
+
kind: "number",
|
|
272
|
+
value: literalToken.number
|
|
273
|
+
},
|
|
274
|
+
span: {
|
|
275
|
+
start: token.span.start,
|
|
276
|
+
end: literalToken.span.end
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
return fail("predicate-expected-literal", `expected a literal after ${op} — write ${token.text} ${op} 'value' or ${token.text} ${op} 5`, literalToken.span);
|
|
280
|
+
}
|
|
281
|
+
if (token.kind === "eof") return fail("predicate-unexpected-end", "unexpected end of predicate — expected a variable name, comparison, or ( group )", token.span);
|
|
282
|
+
return fail("predicate-unexpected-token", `unexpected ${token.text} — comparisons put the variable first: name == 'value'`, token.span);
|
|
283
|
+
};
|
|
284
|
+
const parseUnary = (depth) => {
|
|
285
|
+
const token = peek();
|
|
286
|
+
if (isKeywordToken(token, "not")) {
|
|
287
|
+
advance();
|
|
288
|
+
const operand = parsePrimary(depth);
|
|
289
|
+
if (operand === null) return null;
|
|
290
|
+
return countNode({
|
|
291
|
+
kind: "not",
|
|
292
|
+
operand,
|
|
293
|
+
span: {
|
|
294
|
+
start: token.span.start,
|
|
295
|
+
end: operand.span.end
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return parsePrimary(depth);
|
|
300
|
+
};
|
|
301
|
+
const parseAnd = (depth) => {
|
|
302
|
+
let left = parseUnary(depth);
|
|
303
|
+
while (left !== null && isKeywordToken(peek(), "and")) {
|
|
304
|
+
advance();
|
|
305
|
+
const right = parseUnary(depth);
|
|
306
|
+
if (right === null) return null;
|
|
307
|
+
left = countNode({
|
|
308
|
+
kind: "and",
|
|
309
|
+
left,
|
|
310
|
+
right,
|
|
311
|
+
span: {
|
|
312
|
+
start: left.span.start,
|
|
313
|
+
end: right.span.end
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return left;
|
|
318
|
+
};
|
|
319
|
+
const parseOr = (depth) => {
|
|
320
|
+
let left = parseAnd(depth);
|
|
321
|
+
while (left !== null && isKeywordToken(peek(), "or")) {
|
|
322
|
+
advance();
|
|
323
|
+
const right = parseAnd(depth);
|
|
324
|
+
if (right === null) return null;
|
|
325
|
+
left = countNode({
|
|
326
|
+
kind: "or",
|
|
327
|
+
left,
|
|
328
|
+
right,
|
|
329
|
+
span: {
|
|
330
|
+
start: left.span.start,
|
|
331
|
+
end: right.span.end
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
return left;
|
|
336
|
+
};
|
|
337
|
+
if (source.trim().length === 0) return Result.fail([{
|
|
338
|
+
code: "predicate-empty",
|
|
339
|
+
message: "empty predicate — write e.g. plan == 'pro'",
|
|
340
|
+
span: {
|
|
341
|
+
start: baseOffset,
|
|
342
|
+
end: baseOffset + source.length
|
|
343
|
+
}
|
|
344
|
+
}]);
|
|
345
|
+
const ast = parseOr(0);
|
|
346
|
+
if (ast !== null && diagnostic === void 0) {
|
|
347
|
+
const trailing = peek();
|
|
348
|
+
if (trailing.kind !== "eof") fail("predicate-trailing-input", `unexpected ${JSON.stringify(trailing.text)} after the predicate — join clauses with "and" or "or"`, trailing.span);
|
|
349
|
+
}
|
|
350
|
+
if (ast === null || diagnostic !== void 0) return Result.fail([diagnostic ?? {
|
|
351
|
+
code: "predicate-parse-failed",
|
|
352
|
+
message: "predicate parse failed",
|
|
353
|
+
span: {
|
|
354
|
+
start: baseOffset,
|
|
355
|
+
end: baseOffset + source.length
|
|
356
|
+
}
|
|
357
|
+
}]);
|
|
358
|
+
return Result.succeed(ast);
|
|
359
|
+
};
|
|
360
|
+
const toNumber = (value) => {
|
|
361
|
+
if (typeof value === "number") return value;
|
|
362
|
+
if (typeof value === "string" && value.trim() !== "") return Number(value);
|
|
363
|
+
return NaN;
|
|
364
|
+
};
|
|
365
|
+
const compareNumbers = (op, left, right) => {
|
|
366
|
+
switch (op) {
|
|
367
|
+
case "==": return left === right;
|
|
368
|
+
case "!=": return left !== right;
|
|
369
|
+
case ">": return left > right;
|
|
370
|
+
case ">=": return left >= right;
|
|
371
|
+
case "<": return left < right;
|
|
372
|
+
case "<=": return left <= right;
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
const evaluateComparison = (node, variables) => {
|
|
376
|
+
const value = variables[node.name];
|
|
377
|
+
if (value === void 0 || value === null) return false;
|
|
378
|
+
if (node.literal.kind === "number") {
|
|
379
|
+
const left = toNumber(value);
|
|
380
|
+
if (Number.isNaN(left)) return false;
|
|
381
|
+
return compareNumbers(node.op, left, node.literal.value);
|
|
382
|
+
}
|
|
383
|
+
if (node.op === "==" || node.op === "!=") {
|
|
384
|
+
if (typeof value !== "string") return false;
|
|
385
|
+
return node.op === "==" ? value === node.literal.value : value !== node.literal.value;
|
|
386
|
+
}
|
|
387
|
+
const left = toNumber(value);
|
|
388
|
+
const right = toNumber(node.literal.value);
|
|
389
|
+
if (Number.isNaN(left) || Number.isNaN(right)) return false;
|
|
390
|
+
return compareNumbers(node.op, left, right);
|
|
391
|
+
};
|
|
392
|
+
const evaluatePredicate = (ast, variables) => {
|
|
393
|
+
const tasks = [{
|
|
394
|
+
kind: "node",
|
|
395
|
+
node: ast
|
|
396
|
+
}];
|
|
397
|
+
const values = [];
|
|
398
|
+
while (tasks.length > 0) {
|
|
399
|
+
const task = tasks.pop();
|
|
400
|
+
if (task.kind === "not") {
|
|
401
|
+
values.push(!values.pop());
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (task.kind === "and" || task.kind === "or") {
|
|
405
|
+
const left = values.pop();
|
|
406
|
+
if (task.kind === "and" && !left || task.kind === "or" && left) values.push(left);
|
|
407
|
+
else tasks.push({
|
|
408
|
+
kind: "node",
|
|
409
|
+
node: task.right
|
|
410
|
+
});
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
const node = task.node;
|
|
414
|
+
switch (node.kind) {
|
|
415
|
+
case "or":
|
|
416
|
+
case "and":
|
|
417
|
+
tasks.push({
|
|
418
|
+
kind: node.kind,
|
|
419
|
+
right: node.right
|
|
420
|
+
}, {
|
|
421
|
+
kind: "node",
|
|
422
|
+
node: node.left
|
|
423
|
+
});
|
|
424
|
+
break;
|
|
425
|
+
case "not":
|
|
426
|
+
tasks.push({ kind: "not" }, {
|
|
427
|
+
kind: "node",
|
|
428
|
+
node: node.operand
|
|
429
|
+
});
|
|
430
|
+
break;
|
|
431
|
+
case "comparison":
|
|
432
|
+
values.push(evaluateComparison(node, variables));
|
|
433
|
+
break;
|
|
434
|
+
case "name": {
|
|
435
|
+
const value = variables[node.name];
|
|
436
|
+
values.push(value !== void 0 && value !== null && value !== "");
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return values[0];
|
|
442
|
+
};
|
|
443
|
+
const precedence = (node) => {
|
|
444
|
+
switch (node.kind) {
|
|
445
|
+
case "or": return 1;
|
|
446
|
+
case "and": return 2;
|
|
447
|
+
case "not": return 3;
|
|
448
|
+
case "comparison":
|
|
449
|
+
case "name": return 4;
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
const printLiteral = (literal) => literal.kind === "string" ? `'${literal.value}'` : String(literal.value);
|
|
453
|
+
/** Canonical form: single space around operators/keywords, minimal parens. */
|
|
454
|
+
const printPredicate = (ast) => {
|
|
455
|
+
const frames = [{
|
|
456
|
+
kind: "node",
|
|
457
|
+
node: ast,
|
|
458
|
+
minPrecedence: 0
|
|
459
|
+
}];
|
|
460
|
+
const output = [];
|
|
461
|
+
while (frames.length > 0) {
|
|
462
|
+
const frame = frames.pop();
|
|
463
|
+
if (frame.kind === "text") {
|
|
464
|
+
output.push(frame.value);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
const { node, minPrecedence } = frame;
|
|
468
|
+
const wrapped = precedence(node) < minPrecedence;
|
|
469
|
+
if (wrapped) frames.push({
|
|
470
|
+
kind: "text",
|
|
471
|
+
value: ")"
|
|
472
|
+
});
|
|
473
|
+
switch (node.kind) {
|
|
474
|
+
case "or":
|
|
475
|
+
case "and": {
|
|
476
|
+
const operator = node.kind === "or" ? " or " : " and ";
|
|
477
|
+
const childPrecedence = node.kind === "or" ? 1 : 2;
|
|
478
|
+
frames.push({
|
|
479
|
+
kind: "node",
|
|
480
|
+
node: node.right,
|
|
481
|
+
minPrecedence: childPrecedence
|
|
482
|
+
}, {
|
|
483
|
+
kind: "text",
|
|
484
|
+
value: operator
|
|
485
|
+
}, {
|
|
486
|
+
kind: "node",
|
|
487
|
+
node: node.left,
|
|
488
|
+
minPrecedence: childPrecedence
|
|
489
|
+
});
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
case "not":
|
|
493
|
+
frames.push({
|
|
494
|
+
kind: "node",
|
|
495
|
+
node: node.operand,
|
|
496
|
+
minPrecedence: 4
|
|
497
|
+
}, {
|
|
498
|
+
kind: "text",
|
|
499
|
+
value: "not "
|
|
500
|
+
});
|
|
501
|
+
break;
|
|
502
|
+
case "comparison":
|
|
503
|
+
frames.push({
|
|
504
|
+
kind: "text",
|
|
505
|
+
value: `${node.name} ${node.op} ${printLiteral(node.literal)}`
|
|
506
|
+
});
|
|
507
|
+
break;
|
|
508
|
+
case "name": frames.push({
|
|
509
|
+
kind: "text",
|
|
510
|
+
value: node.name
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
if (wrapped) output.push("(");
|
|
514
|
+
}
|
|
515
|
+
return output.join("");
|
|
516
|
+
};
|
|
517
|
+
const collectPredicateVariables = (ast) => {
|
|
518
|
+
const names = /* @__PURE__ */ new Set();
|
|
519
|
+
const pending = [ast];
|
|
520
|
+
while (pending.length > 0) {
|
|
521
|
+
const node = pending.pop();
|
|
522
|
+
switch (node.kind) {
|
|
523
|
+
case "or":
|
|
524
|
+
case "and":
|
|
525
|
+
pending.push(node.right, node.left);
|
|
526
|
+
break;
|
|
527
|
+
case "not":
|
|
528
|
+
pending.push(node.operand);
|
|
529
|
+
break;
|
|
530
|
+
case "comparison":
|
|
531
|
+
case "name": names.add(node.name);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return names;
|
|
535
|
+
};
|
|
536
|
+
//#endregion
|
|
537
|
+
export { printPredicate as a, parsePredicate as i, collectPredicateVariables as n, SML_PARSER_LIMITS as o, evaluatePredicate as r, PREDICATE_PARSER_LIMITS as t };
|
|
538
|
+
|
|
539
|
+
//# sourceMappingURL=predicate-CvkzT-a2.js.map
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { s as Span, t as Diagnostic } from "./diagnostics-oveE-laV.js";
|
|
2
2
|
import { Result } from "effect";
|
|
3
3
|
//#region src/predicate.d.ts
|
|
4
|
+
/** Standalone predicates inherit the same source, nesting, and node ceilings as SML. */
|
|
5
|
+
declare const PREDICATE_PARSER_LIMITS: {
|
|
6
|
+
readonly maxCharacters: 500000;
|
|
7
|
+
readonly maxUtf8Bytes: number;
|
|
8
|
+
readonly maxDepth: 64;
|
|
9
|
+
readonly maxNodes: 25000;
|
|
10
|
+
};
|
|
4
11
|
type ComparisonOp = "==" | "!=" | ">" | ">=" | "<" | "<=";
|
|
5
12
|
type PredicateLiteral = {
|
|
6
13
|
readonly kind: "string";
|
|
@@ -45,5 +52,5 @@ declare const evaluatePredicate: (ast: PredicateAst, variables: Record<string, u
|
|
|
45
52
|
declare const printPredicate: (ast: PredicateAst) => string;
|
|
46
53
|
declare const collectPredicateVariables: (ast: PredicateAst) => Set<string>;
|
|
47
54
|
//#endregion
|
|
48
|
-
export {
|
|
49
|
-
//# sourceMappingURL=predicate-
|
|
55
|
+
export { collectPredicateVariables as a, printPredicate as c, PredicateLiteral as i, PREDICATE_PARSER_LIMITS as n, evaluatePredicate as o, PredicateAst as r, parsePredicate as s, ComparisonOp as t };
|
|
56
|
+
//# sourceMappingURL=predicate-G2SeSvoN.d.ts.map
|
package/dist/predicate.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
export { ComparisonOp, PredicateAst, PredicateLiteral, collectPredicateVariables, evaluatePredicate, parsePredicate, printPredicate };
|
|
1
|
+
import { a as collectPredicateVariables, c as printPredicate, i as PredicateLiteral, n as PREDICATE_PARSER_LIMITS, o as evaluatePredicate, r as PredicateAst, s as parsePredicate, t as ComparisonOp } from "./predicate-G2SeSvoN.js";
|
|
2
|
+
export { ComparisonOp, PREDICATE_PARSER_LIMITS, PredicateAst, PredicateLiteral, collectPredicateVariables, evaluatePredicate, parsePredicate, printPredicate };
|