@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/predicate.js
CHANGED
|
@@ -1,417 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
const CHAR_TAB = 9;
|
|
4
|
-
const CHAR_LINE_FEED = 10;
|
|
5
|
-
const CHAR_CARRIAGE_RETURN = 13;
|
|
6
|
-
const CHAR_SPACE = 32;
|
|
7
|
-
const CHAR_BANG = 33;
|
|
8
|
-
const CHAR_SINGLE_QUOTE = 39;
|
|
9
|
-
const CHAR_OPEN_PAREN = 40;
|
|
10
|
-
const CHAR_CLOSE_PAREN = 41;
|
|
11
|
-
const CHAR_MINUS = 45;
|
|
12
|
-
const CHAR_DOT = 46;
|
|
13
|
-
const CHAR_LESS_THAN = 60;
|
|
14
|
-
const CHAR_EQUALS = 61;
|
|
15
|
-
const CHAR_GREATER_THAN = 62;
|
|
16
|
-
const CHAR_UNDERSCORE = 95;
|
|
17
|
-
const CHAR_0 = 48;
|
|
18
|
-
const CHAR_9 = 57;
|
|
19
|
-
const CHAR_UPPER_A = 65;
|
|
20
|
-
const CHAR_UPPER_Z = 90;
|
|
21
|
-
const CHAR_LOWER_A = 97;
|
|
22
|
-
const CHAR_LOWER_Z = 122;
|
|
23
|
-
const isWhitespace = (code) => code === CHAR_SPACE || code === CHAR_TAB || code === CHAR_LINE_FEED || code === CHAR_CARRIAGE_RETURN;
|
|
24
|
-
const isDigit = (code) => code >= CHAR_0 && code <= CHAR_9;
|
|
25
|
-
const isLetter = (code) => code >= CHAR_UPPER_A && code <= CHAR_UPPER_Z || code >= CHAR_LOWER_A && code <= CHAR_LOWER_Z;
|
|
26
|
-
const isNameStart = (code) => isLetter(code) || code === CHAR_UNDERSCORE;
|
|
27
|
-
const isNameChar = (code) => isNameStart(code) || isDigit(code);
|
|
28
|
-
const KEYWORDS = [
|
|
29
|
-
"and",
|
|
30
|
-
"or",
|
|
31
|
-
"not"
|
|
32
|
-
];
|
|
33
|
-
const isKeyword = (text) => KEYWORDS.includes(text);
|
|
34
|
-
const tokenize = (source, baseOffset) => {
|
|
35
|
-
const tokens = [];
|
|
36
|
-
let index = 0;
|
|
37
|
-
const length = source.length;
|
|
38
|
-
const failAt = (code, message, start, end) => Result.fail({ diagnostic: {
|
|
39
|
-
code,
|
|
40
|
-
message,
|
|
41
|
-
span: {
|
|
42
|
-
start: baseOffset + start,
|
|
43
|
-
end: baseOffset + end
|
|
44
|
-
}
|
|
45
|
-
} });
|
|
46
|
-
while (index < length) {
|
|
47
|
-
const code = source.charCodeAt(index);
|
|
48
|
-
if (isWhitespace(code)) {
|
|
49
|
-
index++;
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
const start = index;
|
|
53
|
-
if (code === CHAR_OPEN_PAREN) {
|
|
54
|
-
index++;
|
|
55
|
-
tokens.push({
|
|
56
|
-
kind: "lparen",
|
|
57
|
-
text: "(",
|
|
58
|
-
span: {
|
|
59
|
-
start: baseOffset + start,
|
|
60
|
-
end: baseOffset + index
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
continue;
|
|
64
|
-
}
|
|
65
|
-
if (code === CHAR_CLOSE_PAREN) {
|
|
66
|
-
index++;
|
|
67
|
-
tokens.push({
|
|
68
|
-
kind: "rparen",
|
|
69
|
-
text: ")",
|
|
70
|
-
span: {
|
|
71
|
-
start: baseOffset + start,
|
|
72
|
-
end: baseOffset + index
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
if (code === CHAR_EQUALS || code === CHAR_BANG) {
|
|
78
|
-
if (source.charCodeAt(index + 1) === CHAR_EQUALS) {
|
|
79
|
-
index += 2;
|
|
80
|
-
tokens.push({
|
|
81
|
-
kind: "op",
|
|
82
|
-
text: code === CHAR_EQUALS ? "==" : "!=",
|
|
83
|
-
span: {
|
|
84
|
-
start: baseOffset + start,
|
|
85
|
-
end: baseOffset + index
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
return failAt("predicate-invalid-operator", `unexpected ${String.fromCharCode(code)} — ${code === CHAR_EQUALS ? "write == to compare" : "write != to compare"}`, start, start + 1);
|
|
91
|
-
}
|
|
92
|
-
if (code === CHAR_GREATER_THAN || code === CHAR_LESS_THAN) {
|
|
93
|
-
index++;
|
|
94
|
-
if (source.charCodeAt(index) === CHAR_EQUALS) index++;
|
|
95
|
-
tokens.push({
|
|
96
|
-
kind: "op",
|
|
97
|
-
text: source.slice(start, index),
|
|
98
|
-
span: {
|
|
99
|
-
start: baseOffset + start,
|
|
100
|
-
end: baseOffset + index
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
if (code === CHAR_SINGLE_QUOTE) {
|
|
106
|
-
index++;
|
|
107
|
-
const contentStart = index;
|
|
108
|
-
while (index < length && source.charCodeAt(index) !== CHAR_SINGLE_QUOTE) index++;
|
|
109
|
-
if (index >= length) return failAt("predicate-unterminated-string", "unterminated string — add a closing '", start, length);
|
|
110
|
-
const value = source.slice(contentStart, index);
|
|
111
|
-
index++;
|
|
112
|
-
if (value.includes("--")) return failAt("predicate-string-double-dash", "string literals cannot contain -- (predicates are embedded in HTML comment markers)", start, index);
|
|
113
|
-
tokens.push({
|
|
114
|
-
kind: "string",
|
|
115
|
-
text: source.slice(start, index),
|
|
116
|
-
string: value,
|
|
117
|
-
span: {
|
|
118
|
-
start: baseOffset + start,
|
|
119
|
-
end: baseOffset + index
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
if (isDigit(code) || code === CHAR_MINUS && isDigit(source.charCodeAt(index + 1))) {
|
|
125
|
-
index++;
|
|
126
|
-
while (index < length && isDigit(source.charCodeAt(index))) index++;
|
|
127
|
-
if (source.charCodeAt(index) === CHAR_DOT && isDigit(source.charCodeAt(index + 1))) {
|
|
128
|
-
index++;
|
|
129
|
-
while (index < length && isDigit(source.charCodeAt(index))) index++;
|
|
130
|
-
}
|
|
131
|
-
tokens.push({
|
|
132
|
-
kind: "number",
|
|
133
|
-
text: source.slice(start, index),
|
|
134
|
-
number: Number.parseFloat(source.slice(start, index)),
|
|
135
|
-
span: {
|
|
136
|
-
start: baseOffset + start,
|
|
137
|
-
end: baseOffset + index
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
if (isNameStart(code)) {
|
|
143
|
-
index++;
|
|
144
|
-
while (index < length && isNameChar(source.charCodeAt(index))) index++;
|
|
145
|
-
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);
|
|
146
|
-
tokens.push({
|
|
147
|
-
kind: "name",
|
|
148
|
-
text: source.slice(start, index),
|
|
149
|
-
span: {
|
|
150
|
-
start: baseOffset + start,
|
|
151
|
-
end: baseOffset + index
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
if (code === CHAR_DOT) return failAt("predicate-path-not-supported", "unexpected . — predicates use flat variable names, not paths", start, start + 1);
|
|
157
|
-
return failAt("predicate-unexpected-character", `unexpected character ${JSON.stringify(String.fromCharCode(code))} in predicate`, start, start + 1);
|
|
158
|
-
}
|
|
159
|
-
tokens.push({
|
|
160
|
-
kind: "eof",
|
|
161
|
-
text: "",
|
|
162
|
-
span: {
|
|
163
|
-
start: baseOffset + length,
|
|
164
|
-
end: baseOffset + length
|
|
165
|
-
}
|
|
166
|
-
});
|
|
167
|
-
return Result.succeed(tokens);
|
|
168
|
-
};
|
|
169
|
-
const COMPARISON_OPS = [
|
|
170
|
-
"==",
|
|
171
|
-
"!=",
|
|
172
|
-
">",
|
|
173
|
-
">=",
|
|
174
|
-
"<",
|
|
175
|
-
"<="
|
|
176
|
-
];
|
|
177
|
-
/**
|
|
178
|
-
* Parses a predicate. `baseOffset` is the absolute source offset of the
|
|
179
|
-
* predicate's first character (an <If> test attribute passes valueSpan.start),
|
|
180
|
-
* so diagnostic and node spans point into the containing document.
|
|
181
|
-
*/
|
|
182
|
-
const parsePredicate = (source, baseOffset = 0) => {
|
|
183
|
-
const lexed = tokenize(source, baseOffset);
|
|
184
|
-
if (Result.isFailure(lexed)) return Result.fail([lexed.failure.diagnostic]);
|
|
185
|
-
const tokens = lexed.success;
|
|
186
|
-
let index = 0;
|
|
187
|
-
let diagnostic;
|
|
188
|
-
const fail = (code, message, span) => {
|
|
189
|
-
diagnostic ??= {
|
|
190
|
-
code,
|
|
191
|
-
message,
|
|
192
|
-
span
|
|
193
|
-
};
|
|
194
|
-
return null;
|
|
195
|
-
};
|
|
196
|
-
const peek = () => tokens[index];
|
|
197
|
-
const advance = () => tokens[index++];
|
|
198
|
-
const isKeywordToken = (token, keyword) => token.kind === "name" && token.text === keyword;
|
|
199
|
-
const parsePrimary = () => {
|
|
200
|
-
const token = advance();
|
|
201
|
-
if (token.kind === "lparen") {
|
|
202
|
-
const inner = parseOr();
|
|
203
|
-
if (inner === null) return null;
|
|
204
|
-
const close = advance();
|
|
205
|
-
if (close.kind !== "rparen") return fail("predicate-missing-paren", "missing ) — close the group", close.span);
|
|
206
|
-
return inner;
|
|
207
|
-
}
|
|
208
|
-
if (token.kind === "name") {
|
|
209
|
-
if (isKeyword(token.text)) return fail("predicate-unexpected-token", `unexpected "${token.text}" — expected a variable name, comparison, or ( group )`, token.span);
|
|
210
|
-
const opToken = peek();
|
|
211
|
-
if (opToken.kind !== "op") return {
|
|
212
|
-
kind: "name",
|
|
213
|
-
name: token.text,
|
|
214
|
-
span: token.span
|
|
215
|
-
};
|
|
216
|
-
advance();
|
|
217
|
-
const op = COMPARISON_OPS.find((candidate) => candidate === opToken.text);
|
|
218
|
-
if (op === void 0) return fail("predicate-invalid-operator", `unknown operator ${opToken.text} — use ==, !=, >, >=, <, or <=`, opToken.span);
|
|
219
|
-
const literalToken = advance();
|
|
220
|
-
if (literalToken.kind === "string") return {
|
|
221
|
-
kind: "comparison",
|
|
222
|
-
name: token.text,
|
|
223
|
-
op,
|
|
224
|
-
literal: {
|
|
225
|
-
kind: "string",
|
|
226
|
-
value: literalToken.string
|
|
227
|
-
},
|
|
228
|
-
span: {
|
|
229
|
-
start: token.span.start,
|
|
230
|
-
end: literalToken.span.end
|
|
231
|
-
}
|
|
232
|
-
};
|
|
233
|
-
if (literalToken.kind === "number") return {
|
|
234
|
-
kind: "comparison",
|
|
235
|
-
name: token.text,
|
|
236
|
-
op,
|
|
237
|
-
literal: {
|
|
238
|
-
kind: "number",
|
|
239
|
-
value: literalToken.number
|
|
240
|
-
},
|
|
241
|
-
span: {
|
|
242
|
-
start: token.span.start,
|
|
243
|
-
end: literalToken.span.end
|
|
244
|
-
}
|
|
245
|
-
};
|
|
246
|
-
return fail("predicate-expected-literal", `expected a literal after ${op} — write ${token.text} ${op} 'value' or ${token.text} ${op} 5`, literalToken.span);
|
|
247
|
-
}
|
|
248
|
-
if (token.kind === "eof") return fail("predicate-unexpected-end", "unexpected end of predicate — expected a variable name, comparison, or ( group )", token.span);
|
|
249
|
-
return fail("predicate-unexpected-token", `unexpected ${token.text} — comparisons put the variable first: name == 'value'`, token.span);
|
|
250
|
-
};
|
|
251
|
-
const parseUnary = () => {
|
|
252
|
-
const token = peek();
|
|
253
|
-
if (isKeywordToken(token, "not")) {
|
|
254
|
-
advance();
|
|
255
|
-
const operand = parsePrimary();
|
|
256
|
-
if (operand === null) return null;
|
|
257
|
-
return {
|
|
258
|
-
kind: "not",
|
|
259
|
-
operand,
|
|
260
|
-
span: {
|
|
261
|
-
start: token.span.start,
|
|
262
|
-
end: operand.span.end
|
|
263
|
-
}
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
return parsePrimary();
|
|
267
|
-
};
|
|
268
|
-
const parseAnd = () => {
|
|
269
|
-
let left = parseUnary();
|
|
270
|
-
while (left !== null && isKeywordToken(peek(), "and")) {
|
|
271
|
-
advance();
|
|
272
|
-
const right = parseUnary();
|
|
273
|
-
if (right === null) return null;
|
|
274
|
-
left = {
|
|
275
|
-
kind: "and",
|
|
276
|
-
left,
|
|
277
|
-
right,
|
|
278
|
-
span: {
|
|
279
|
-
start: left.span.start,
|
|
280
|
-
end: right.span.end
|
|
281
|
-
}
|
|
282
|
-
};
|
|
283
|
-
}
|
|
284
|
-
return left;
|
|
285
|
-
};
|
|
286
|
-
const parseOr = () => {
|
|
287
|
-
let left = parseAnd();
|
|
288
|
-
while (left !== null && isKeywordToken(peek(), "or")) {
|
|
289
|
-
advance();
|
|
290
|
-
const right = parseAnd();
|
|
291
|
-
if (right === null) return null;
|
|
292
|
-
left = {
|
|
293
|
-
kind: "or",
|
|
294
|
-
left,
|
|
295
|
-
right,
|
|
296
|
-
span: {
|
|
297
|
-
start: left.span.start,
|
|
298
|
-
end: right.span.end
|
|
299
|
-
}
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
return left;
|
|
303
|
-
};
|
|
304
|
-
if (source.trim().length === 0) return Result.fail([{
|
|
305
|
-
code: "predicate-empty",
|
|
306
|
-
message: "empty predicate — write e.g. plan == 'pro'",
|
|
307
|
-
span: {
|
|
308
|
-
start: baseOffset,
|
|
309
|
-
end: baseOffset + source.length
|
|
310
|
-
}
|
|
311
|
-
}]);
|
|
312
|
-
const ast = parseOr();
|
|
313
|
-
if (ast !== null && diagnostic === void 0) {
|
|
314
|
-
const trailing = peek();
|
|
315
|
-
if (trailing.kind !== "eof") fail("predicate-trailing-input", `unexpected ${JSON.stringify(trailing.text)} after the predicate — join clauses with "and" or "or"`, trailing.span);
|
|
316
|
-
}
|
|
317
|
-
if (ast === null || diagnostic !== void 0) return Result.fail([diagnostic ?? {
|
|
318
|
-
code: "predicate-parse-failed",
|
|
319
|
-
message: "predicate parse failed",
|
|
320
|
-
span: {
|
|
321
|
-
start: baseOffset,
|
|
322
|
-
end: baseOffset + source.length
|
|
323
|
-
}
|
|
324
|
-
}]);
|
|
325
|
-
return Result.succeed(ast);
|
|
326
|
-
};
|
|
327
|
-
const toNumber = (value) => {
|
|
328
|
-
if (typeof value === "number") return value;
|
|
329
|
-
if (typeof value === "string" && value.trim() !== "") return Number(value);
|
|
330
|
-
return NaN;
|
|
331
|
-
};
|
|
332
|
-
const compareNumbers = (op, left, right) => {
|
|
333
|
-
switch (op) {
|
|
334
|
-
case "==": return left === right;
|
|
335
|
-
case "!=": return left !== right;
|
|
336
|
-
case ">": return left > right;
|
|
337
|
-
case ">=": return left >= right;
|
|
338
|
-
case "<": return left < right;
|
|
339
|
-
case "<=": return left <= right;
|
|
340
|
-
}
|
|
341
|
-
};
|
|
342
|
-
const evaluateComparison = (node, variables) => {
|
|
343
|
-
const value = variables[node.name];
|
|
344
|
-
if (value === void 0 || value === null) return false;
|
|
345
|
-
if (node.literal.kind === "number") {
|
|
346
|
-
const left = toNumber(value);
|
|
347
|
-
if (Number.isNaN(left)) return false;
|
|
348
|
-
return compareNumbers(node.op, left, node.literal.value);
|
|
349
|
-
}
|
|
350
|
-
if (node.op === "==" || node.op === "!=") {
|
|
351
|
-
if (typeof value !== "string") return false;
|
|
352
|
-
return node.op === "==" ? value === node.literal.value : value !== node.literal.value;
|
|
353
|
-
}
|
|
354
|
-
const left = toNumber(value);
|
|
355
|
-
const right = toNumber(node.literal.value);
|
|
356
|
-
if (Number.isNaN(left) || Number.isNaN(right)) return false;
|
|
357
|
-
return compareNumbers(node.op, left, right);
|
|
358
|
-
};
|
|
359
|
-
const evaluatePredicate = (ast, variables) => {
|
|
360
|
-
switch (ast.kind) {
|
|
361
|
-
case "or": return evaluatePredicate(ast.left, variables) || evaluatePredicate(ast.right, variables);
|
|
362
|
-
case "and": return evaluatePredicate(ast.left, variables) && evaluatePredicate(ast.right, variables);
|
|
363
|
-
case "not": return !evaluatePredicate(ast.operand, variables);
|
|
364
|
-
case "comparison": return evaluateComparison(ast, variables);
|
|
365
|
-
case "name": {
|
|
366
|
-
const value = variables[ast.name];
|
|
367
|
-
return value !== void 0 && value !== null && value !== "";
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
};
|
|
371
|
-
const precedence = (node) => {
|
|
372
|
-
switch (node.kind) {
|
|
373
|
-
case "or": return 1;
|
|
374
|
-
case "and": return 2;
|
|
375
|
-
case "not": return 3;
|
|
376
|
-
case "comparison":
|
|
377
|
-
case "name": return 4;
|
|
378
|
-
}
|
|
379
|
-
};
|
|
380
|
-
const printLiteral = (literal) => literal.kind === "string" ? `'${literal.value}'` : String(literal.value);
|
|
381
|
-
const printNode = (node, minPrecedence) => {
|
|
382
|
-
const wrap = (text) => precedence(node) < minPrecedence ? `(${text})` : text;
|
|
383
|
-
switch (node.kind) {
|
|
384
|
-
case "or": return wrap(`${printNode(node.left, 1)} or ${printNode(node.right, 1)}`);
|
|
385
|
-
case "and": return wrap(`${printNode(node.left, 2)} and ${printNode(node.right, 2)}`);
|
|
386
|
-
case "not": return wrap(`not ${printNode(node.operand, 4)}`);
|
|
387
|
-
case "comparison": return wrap(`${node.name} ${node.op} ${printLiteral(node.literal)}`);
|
|
388
|
-
case "name": return wrap(node.name);
|
|
389
|
-
}
|
|
390
|
-
};
|
|
391
|
-
/** Canonical form: single space around operators/keywords, minimal parens. */
|
|
392
|
-
const printPredicate = (ast) => printNode(ast, 0);
|
|
393
|
-
const collectPredicateVariables = (ast) => {
|
|
394
|
-
const names = /* @__PURE__ */ new Set();
|
|
395
|
-
const visit = (node) => {
|
|
396
|
-
switch (node.kind) {
|
|
397
|
-
case "or":
|
|
398
|
-
case "and":
|
|
399
|
-
visit(node.left);
|
|
400
|
-
visit(node.right);
|
|
401
|
-
return;
|
|
402
|
-
case "not":
|
|
403
|
-
visit(node.operand);
|
|
404
|
-
return;
|
|
405
|
-
case "comparison":
|
|
406
|
-
case "name":
|
|
407
|
-
names.add(node.name);
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
};
|
|
411
|
-
visit(ast);
|
|
412
|
-
return names;
|
|
413
|
-
};
|
|
414
|
-
//#endregion
|
|
415
|
-
export { collectPredicateVariables, evaluatePredicate, parsePredicate, printPredicate };
|
|
416
|
-
|
|
417
|
-
//# sourceMappingURL=predicate.js.map
|
|
1
|
+
import { a as printPredicate, i as parsePredicate, n as collectPredicateVariables, r as evaluatePredicate, t as PREDICATE_PARSER_LIMITS } from "./predicate-CvkzT-a2.js";
|
|
2
|
+
export { PREDICATE_PARSER_LIMITS, collectPredicateVariables, evaluatePredicate, parsePredicate, printPredicate };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as TemplateCorpusKind, c as TemplateFacetSet, d as templateFacetOptions, i as PublicCorpusRegistrationIssue, l as matchesTemplateFacets, n as OwnedEditablePreset, o as TemplateCorpusRecord, r as PublicCorpusRegistration, s as TemplateFacetQuery, t as InspirationReference, u as registerPublicCorpusEntry } from "../corpus-Dz5irJ9x.js";
|
|
2
|
+
export { InspirationReference, OwnedEditablePreset, PublicCorpusRegistration, PublicCorpusRegistrationIssue, TemplateCorpusKind, TemplateCorpusRecord, TemplateFacetQuery, TemplateFacetSet, matchesTemplateFacets, registerPublicCorpusEntry, templateFacetOptions };
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { presetCatalog } from "../presets.js";
|
|
2
|
+
import { Result, Schema } from "effect";
|
|
3
|
+
//#region presets/corpus.ts
|
|
4
|
+
const TemplateCorpusKind = Schema.Literals(["owned-editable-sml", "inspiration-reference"]);
|
|
5
|
+
const templateFacetOptions = {
|
|
6
|
+
jobType: [
|
|
7
|
+
"account-access",
|
|
8
|
+
"commerce",
|
|
9
|
+
"product-communication",
|
|
10
|
+
"lifecycle",
|
|
11
|
+
"publishing-campaigns"
|
|
12
|
+
],
|
|
13
|
+
lifecycle: [
|
|
14
|
+
"acquisition",
|
|
15
|
+
"activation",
|
|
16
|
+
"engagement",
|
|
17
|
+
"transaction",
|
|
18
|
+
"retention",
|
|
19
|
+
"recovery"
|
|
20
|
+
],
|
|
21
|
+
industry: [
|
|
22
|
+
"saas",
|
|
23
|
+
"ecommerce",
|
|
24
|
+
"developer-tools",
|
|
25
|
+
"newsletters-media"
|
|
26
|
+
],
|
|
27
|
+
season: ["evergreen", "seasonal"],
|
|
28
|
+
audience: [
|
|
29
|
+
"product-users",
|
|
30
|
+
"customers",
|
|
31
|
+
"members",
|
|
32
|
+
"readers",
|
|
33
|
+
"shoppers",
|
|
34
|
+
"teams"
|
|
35
|
+
],
|
|
36
|
+
design: [
|
|
37
|
+
"minimal",
|
|
38
|
+
"branded",
|
|
39
|
+
"editorial",
|
|
40
|
+
"data-heavy",
|
|
41
|
+
"single-cta"
|
|
42
|
+
],
|
|
43
|
+
brandProvenance: ["samva-original", "third-party-reference"]
|
|
44
|
+
};
|
|
45
|
+
const JobTypeFacet = Schema.Literals(templateFacetOptions.jobType);
|
|
46
|
+
const LifecycleFacet = Schema.Literals(templateFacetOptions.lifecycle);
|
|
47
|
+
const IndustryFacet = Schema.Literals(templateFacetOptions.industry);
|
|
48
|
+
const SeasonFacet = Schema.Literals(templateFacetOptions.season);
|
|
49
|
+
const AudienceFacet = Schema.Literals(templateFacetOptions.audience);
|
|
50
|
+
const DesignFacet = Schema.Literals(templateFacetOptions.design);
|
|
51
|
+
const BrandProvenanceFacet = Schema.Literals(templateFacetOptions.brandProvenance);
|
|
52
|
+
const TemplateFacetSet = Schema.Struct({
|
|
53
|
+
jobType: Schema.Array(JobTypeFacet),
|
|
54
|
+
lifecycle: Schema.Array(LifecycleFacet),
|
|
55
|
+
industry: Schema.Array(IndustryFacet),
|
|
56
|
+
season: Schema.Array(SeasonFacet),
|
|
57
|
+
audience: Schema.Array(AudienceFacet),
|
|
58
|
+
design: Schema.Array(DesignFacet),
|
|
59
|
+
brandProvenance: Schema.Array(BrandProvenanceFacet)
|
|
60
|
+
});
|
|
61
|
+
const CorpusReview = Schema.Struct({
|
|
62
|
+
state: Schema.Literals([
|
|
63
|
+
"draft",
|
|
64
|
+
"approved",
|
|
65
|
+
"rejected"
|
|
66
|
+
]),
|
|
67
|
+
reviewedBy: Schema.String,
|
|
68
|
+
reviewedAt: Schema.String,
|
|
69
|
+
notes: Schema.String
|
|
70
|
+
});
|
|
71
|
+
const CorpusDisplay = Schema.Struct({
|
|
72
|
+
state: Schema.Literals(["hidden", "public"]),
|
|
73
|
+
attributionLabel: Schema.String
|
|
74
|
+
});
|
|
75
|
+
const CorpusRetention = Schema.Struct({
|
|
76
|
+
policy: Schema.Literals(["retain-until-withdrawn", "retain-while-linked"]),
|
|
77
|
+
reviewedAt: Schema.String
|
|
78
|
+
});
|
|
79
|
+
const CorpusTakedown = Schema.Struct({
|
|
80
|
+
state: Schema.Literals(["active", "removed"]),
|
|
81
|
+
contact: Schema.String,
|
|
82
|
+
procedureUrl: Schema.String
|
|
83
|
+
});
|
|
84
|
+
const OwnedEditablePreset = Schema.TaggedStruct("OwnedEditablePreset", {
|
|
85
|
+
corpusKind: Schema.Literal("owned-editable-sml"),
|
|
86
|
+
provenance: Schema.Struct({
|
|
87
|
+
owner: Schema.Literal("Samva"),
|
|
88
|
+
sourcePath: Schema.String,
|
|
89
|
+
recordedAt: Schema.String
|
|
90
|
+
}),
|
|
91
|
+
review: CorpusReview,
|
|
92
|
+
display: CorpusDisplay,
|
|
93
|
+
retention: CorpusRetention,
|
|
94
|
+
takedown: CorpusTakedown
|
|
95
|
+
});
|
|
96
|
+
const InspirationReference = Schema.TaggedStruct("InspirationReference", {
|
|
97
|
+
corpusKind: Schema.Literal("inspiration-reference"),
|
|
98
|
+
provenance: Schema.Struct({
|
|
99
|
+
publisher: Schema.String,
|
|
100
|
+
sourceUrl: Schema.String,
|
|
101
|
+
capturedAt: Schema.String,
|
|
102
|
+
rightsBasis: Schema.Literals(["display-link-and-commentary", "permission"])
|
|
103
|
+
}),
|
|
104
|
+
review: CorpusReview,
|
|
105
|
+
display: CorpusDisplay,
|
|
106
|
+
retention: CorpusRetention,
|
|
107
|
+
takedown: CorpusTakedown
|
|
108
|
+
});
|
|
109
|
+
const TemplateCorpusRecord = Schema.Union([OwnedEditablePreset, InspirationReference]);
|
|
110
|
+
const ISO_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
|
|
111
|
+
const nonEmpty = (value) => value.trim().length > 0;
|
|
112
|
+
const isHttpsUrl = (value) => {
|
|
113
|
+
const url = URL.parse(value);
|
|
114
|
+
return url?.protocol === "https:" && url.hostname.length > 0 && url.username.length === 0 && url.password.length === 0;
|
|
115
|
+
};
|
|
116
|
+
const requiredFacetDimensions = [
|
|
117
|
+
"jobType",
|
|
118
|
+
"lifecycle",
|
|
119
|
+
"industry",
|
|
120
|
+
"season",
|
|
121
|
+
"audience",
|
|
122
|
+
"design",
|
|
123
|
+
"brandProvenance"
|
|
124
|
+
];
|
|
125
|
+
const canonicalOwnedPresetSources = new Map(presetCatalog.filter((preset) => preset.channel === "email").map((preset) => [preset.id, `packages/markup/presets/${preset.id}.tsx`]));
|
|
126
|
+
/** Decode and gate a record before it can enter any public projection. */
|
|
127
|
+
const registerPublicCorpusEntry = (input) => {
|
|
128
|
+
const decoded = Schema.decodeUnknownResult(Schema.Struct({
|
|
129
|
+
id: Schema.String,
|
|
130
|
+
corpus: TemplateCorpusRecord,
|
|
131
|
+
facets: TemplateFacetSet
|
|
132
|
+
}), { onExcessProperty: "error" })(input);
|
|
133
|
+
if (Result.isFailure(decoded)) return Result.fail({
|
|
134
|
+
id: "unknown",
|
|
135
|
+
message: "record does not match a corpus schema"
|
|
136
|
+
});
|
|
137
|
+
const { id, corpus, facets } = decoded.success;
|
|
138
|
+
const timestamps = [
|
|
139
|
+
corpus.review.reviewedAt,
|
|
140
|
+
corpus.retention.reviewedAt,
|
|
141
|
+
corpus._tag === "InspirationReference" ? corpus.provenance.capturedAt : corpus.provenance.recordedAt
|
|
142
|
+
];
|
|
143
|
+
if (!nonEmpty(id) || corpus.review.state !== "approved" || !nonEmpty(corpus.review.reviewedBy) || !nonEmpty(corpus.review.notes) || corpus.display.state !== "public" || !nonEmpty(corpus.display.attributionLabel) || corpus.takedown.state !== "active" || !nonEmpty(corpus.takedown.contact) || !isHttpsUrl(corpus.takedown.procedureUrl) || timestamps.some((timestamp) => !ISO_TIMESTAMP.test(timestamp))) return Result.fail({
|
|
144
|
+
id,
|
|
145
|
+
message: "public corpus records require approved review, display, retention, and takedown metadata"
|
|
146
|
+
});
|
|
147
|
+
if (corpus._tag === "OwnedEditablePreset" && (canonicalOwnedPresetSources.get(id) !== corpus.provenance.sourcePath || corpus.retention.policy !== "retain-until-withdrawn" || facets.brandProvenance.some((value) => value !== "samva-original"))) return Result.fail({
|
|
148
|
+
id,
|
|
149
|
+
message: "owned presets require a canonical editable TSX source"
|
|
150
|
+
});
|
|
151
|
+
if (corpus._tag === "InspirationReference" && (!isHttpsUrl(corpus.provenance.sourceUrl) || corpus.retention.policy !== "retain-while-linked" || facets.brandProvenance.some((value) => value !== "third-party-reference"))) return Result.fail({
|
|
152
|
+
id,
|
|
153
|
+
message: "inspiration references require an HTTPS source and third-party provenance facet"
|
|
154
|
+
});
|
|
155
|
+
for (const dimension of requiredFacetDimensions) {
|
|
156
|
+
const values = facets[dimension];
|
|
157
|
+
if (values.length === 0 || new Set(values).size !== values.length) return Result.fail({
|
|
158
|
+
id,
|
|
159
|
+
message: `facet ${dimension} must be populated without duplicates`
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return Result.succeed({
|
|
163
|
+
id,
|
|
164
|
+
corpus,
|
|
165
|
+
facets
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
const TemplateFacetQuery = Schema.Struct({
|
|
169
|
+
jobType: Schema.optionalKey(Schema.Array(JobTypeFacet)),
|
|
170
|
+
lifecycle: Schema.optionalKey(Schema.Array(LifecycleFacet)),
|
|
171
|
+
industry: Schema.optionalKey(Schema.Array(IndustryFacet)),
|
|
172
|
+
season: Schema.optionalKey(Schema.Array(SeasonFacet)),
|
|
173
|
+
audience: Schema.optionalKey(Schema.Array(AudienceFacet)),
|
|
174
|
+
design: Schema.optionalKey(Schema.Array(DesignFacet)),
|
|
175
|
+
brandProvenance: Schema.optionalKey(Schema.Array(BrandProvenanceFacet))
|
|
176
|
+
});
|
|
177
|
+
/** Strict intersection: every selected value in every selected dimension must match. */
|
|
178
|
+
const matchesTemplateFacets = (facets, query) => requiredFacetDimensions.every((dimension) => {
|
|
179
|
+
const selected = query[dimension];
|
|
180
|
+
const available = new Set(facets[dimension]);
|
|
181
|
+
return selected === void 0 || selected.every((value) => available.has(value));
|
|
182
|
+
});
|
|
183
|
+
//#endregion
|
|
184
|
+
export { InspirationReference, OwnedEditablePreset, TemplateCorpusKind, TemplateCorpusRecord, TemplateFacetQuery, TemplateFacetSet, matchesTemplateFacets, registerPublicCorpusEntry, templateFacetOptions };
|
|
185
|
+
|
|
186
|
+
//# sourceMappingURL=corpus.js.map
|