@cxpinsight/survey-spec 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/package.json +4 -4
- package/src/expressionEval.d.ts +0 -7
- package/src/expressionEval.d.ts.map +0 -1
- package/src/expressionEval.js +0 -337
- package/src/expressionEval.js.map +0 -1
- package/src/expressionEval.ts +0 -321
- package/src/index.d.ts +0 -16
- package/src/index.d.ts.map +0 -1
- package/src/index.js +0 -22
- package/src/index.js.map +0 -1
- package/src/index.ts +0 -15
- package/src/resolveVariable.d.ts +0 -19
- package/src/resolveVariable.d.ts.map +0 -1
- package/src/resolveVariable.js +0 -52
- package/src/resolveVariable.js.map +0 -1
- package/src/resolveVariable.ts +0 -50
- package/src/types.d.ts +0 -17
- package/src/types.d.ts.map +0 -1
- package/src/types.js +0 -3
- package/src/types.js.map +0 -1
- package/src/types.ts +0 -18
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CXPinsight
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cxpinsight/survey-spec",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "The survey specification the renderers share — expression evaluation first. Source of truth for behaviour that must be identical on web, links and mobile.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"files": [
|
|
8
8
|
"dist/",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
14
14
|
"typecheck": "tsc --noEmit",
|
|
15
15
|
"test": "node --test test/*.test.js",
|
|
16
|
-
"prepublishOnly": "npm run typecheck && npm run build"
|
|
16
|
+
"prepublishOnly": "node scripts/check-toolchain.js && npm run typecheck && npm run build"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"typescript": ">=5.0.0"
|
package/src/expressionEval.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export type { AnswersMap, VariablesMap, EvalContext } from './types';
|
|
2
|
-
import type { EvalContext } from './types';
|
|
3
|
-
export declare function evaluateExpression(source: string, ctx: EvalContext): unknown;
|
|
4
|
-
export declare function evaluateCondition(source: string | undefined, ctx: EvalContext): boolean;
|
|
5
|
-
/** Whether `source` parses at all, independent of what it evaluates to. */
|
|
6
|
-
export declare function isParseable(source: string | undefined): boolean;
|
|
7
|
-
//# sourceMappingURL=expressionEval.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"expressionEval.d.ts","sourceRoot":"","sources":["expressionEval.ts"],"names":[],"mappings":"AAwBA,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACrE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAI3C,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,GAAG,OAAO,CAY5E;AAID,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,WAAW,GAAG,OAAO,CAcvF;AAED,2EAA2E;AAC3E,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAS/D"}
|
package/src/expressionEval.js
DELETED
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// Expression evaluator — supports the subset of operators the spec
|
|
3
|
-
// commits to (spec §7, §22):
|
|
4
|
-
//
|
|
5
|
-
// Comparators: = != < > <= >=
|
|
6
|
-
// Boolean: and or not && || !
|
|
7
|
-
// Arithmetic: + - * /
|
|
8
|
-
// Refs: {q_id} the respondent's answer to q_id
|
|
9
|
-
// {{variable}} resolved variable value
|
|
10
|
-
// 'string' string literal
|
|
11
|
-
// 42 numeric literal
|
|
12
|
-
// true / false boolean literal
|
|
13
|
-
//
|
|
14
|
-
// This is deliberately a simple tokens-based evaluator, not a full
|
|
15
|
-
// JS-compatible expression engine. It's used for:
|
|
16
|
-
// - showWhen / enabledWhen / requiredWhen (question + section)
|
|
17
|
-
// - `expression` type questions (computed value)
|
|
18
|
-
// - `calculated` variable expressions
|
|
19
|
-
//
|
|
20
|
-
// Being cautious: an untyped eval() would let survey authors run
|
|
21
|
-
// arbitrary JS against respondent answers. This restricted grammar
|
|
22
|
-
// keeps the surface area bounded.
|
|
23
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
-
exports.evaluateExpression = evaluateExpression;
|
|
25
|
-
exports.evaluateCondition = evaluateCondition;
|
|
26
|
-
exports.isParseable = isParseable;
|
|
27
|
-
// ─── Public entry point ───────────────────────────────────────────────
|
|
28
|
-
function evaluateExpression(source, ctx) {
|
|
29
|
-
if (typeof source !== 'string')
|
|
30
|
-
return undefined;
|
|
31
|
-
const src = source.trim();
|
|
32
|
-
if (!src)
|
|
33
|
-
return undefined;
|
|
34
|
-
try {
|
|
35
|
-
const tokens = tokenize(src);
|
|
36
|
-
const [value, consumed] = parseExpression(tokens, 0, ctx);
|
|
37
|
-
if (consumed !== tokens.length)
|
|
38
|
-
return undefined; // trailing garbage
|
|
39
|
-
return value;
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
return undefined;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
// showWhen/enabledWhen coerce the result to boolean. Empty / undefined
|
|
46
|
-
// evaluates to false (hides the question) — friendlier than throwing.
|
|
47
|
-
function evaluateCondition(source, ctx) {
|
|
48
|
-
if (!source)
|
|
49
|
-
return true; // no condition = always shown
|
|
50
|
-
// An expression that cannot be PARSED is not a false condition — it is not a
|
|
51
|
-
// condition. Treating a typo as "false" hid the question, and a hidden question
|
|
52
|
-
// is invisible in the response data: no answers, no error, no signal, and the
|
|
53
|
-
// author concludes nobody wanted to answer it. Silent data loss is the worse
|
|
54
|
-
// failure, so an invalid condition is ignored and the question shows. The
|
|
55
|
-
// builder is where a malformed expression should be surfaced, before it ships.
|
|
56
|
-
//
|
|
57
|
-
// A well-formed expression that evaluates falsy still hides the question, which
|
|
58
|
-
// is the whole point of showWhen. Only unparseable input is ignored.
|
|
59
|
-
if (!isParseable(source))
|
|
60
|
-
return true;
|
|
61
|
-
const result = evaluateExpression(source, ctx);
|
|
62
|
-
return truthy(result);
|
|
63
|
-
}
|
|
64
|
-
/** Whether `source` parses at all, independent of what it evaluates to. */
|
|
65
|
-
function isParseable(source) {
|
|
66
|
-
if (!source || !source.trim())
|
|
67
|
-
return true;
|
|
68
|
-
try {
|
|
69
|
-
const tokens = tokenize(source.trim());
|
|
70
|
-
const [, consumed] = parseExpression(tokens, 0, { variables: {}, answers: {} });
|
|
71
|
-
return consumed === tokens.length;
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
function tokenize(src) {
|
|
78
|
-
const tokens = [];
|
|
79
|
-
let i = 0;
|
|
80
|
-
const len = src.length;
|
|
81
|
-
while (i < len) {
|
|
82
|
-
const c = src[i];
|
|
83
|
-
// whitespace
|
|
84
|
-
if (c === ' ' || c === '\t' || c === '\n' || c === '\r') {
|
|
85
|
-
i++;
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
// string literal — single or double quoted
|
|
89
|
-
if (c === "'" || c === '"') {
|
|
90
|
-
const quote = c;
|
|
91
|
-
let out = '';
|
|
92
|
-
i++;
|
|
93
|
-
while (i < len && src[i] !== quote) {
|
|
94
|
-
if (src[i] === '\\' && i + 1 < len) {
|
|
95
|
-
out += src[i + 1];
|
|
96
|
-
i += 2;
|
|
97
|
-
}
|
|
98
|
-
else {
|
|
99
|
-
out += src[i];
|
|
100
|
-
i++;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
i++; // closing quote
|
|
104
|
-
tokens.push({ kind: 'str', value: out });
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
// numeric literal
|
|
108
|
-
if ((c >= '0' && c <= '9') || (c === '-' && src[i + 1] >= '0' && src[i + 1] <= '9' && !prevExpectsBinary(tokens))) {
|
|
109
|
-
let j = i;
|
|
110
|
-
if (src[j] === '-')
|
|
111
|
-
j++;
|
|
112
|
-
while (j < len && ((src[j] >= '0' && src[j] <= '9') || src[j] === '.'))
|
|
113
|
-
j++;
|
|
114
|
-
tokens.push({ kind: 'num', value: parseFloat(src.slice(i, j)) });
|
|
115
|
-
i = j;
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
// {{variable_ref}} — spec §22 interpolation shape
|
|
119
|
-
if (c === '{' && src[i + 1] === '{') {
|
|
120
|
-
const end = src.indexOf('}}', i + 2);
|
|
121
|
-
if (end < 0)
|
|
122
|
-
throw new Error('unterminated {{');
|
|
123
|
-
const inner = src.slice(i + 2, end).trim();
|
|
124
|
-
const dot = inner.indexOf('.');
|
|
125
|
-
if (dot > 0) {
|
|
126
|
-
tokens.push({ kind: 'variable_ref', name: inner.slice(0, dot).trim(), suffix: inner.slice(dot + 1).trim() });
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
tokens.push({ kind: 'variable_ref', name: inner });
|
|
130
|
-
}
|
|
131
|
-
i = end + 2;
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
// {q_id} — answer reference (single-brace, spec §7)
|
|
135
|
-
if (c === '{') {
|
|
136
|
-
const end = src.indexOf('}', i + 1);
|
|
137
|
-
if (end < 0)
|
|
138
|
-
throw new Error('unterminated {');
|
|
139
|
-
tokens.push({ kind: 'answer_ref', name: src.slice(i + 1, end).trim() });
|
|
140
|
-
i = end + 1;
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
// parentheses
|
|
144
|
-
if (c === '(') {
|
|
145
|
-
tokens.push({ kind: 'lparen' });
|
|
146
|
-
i++;
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
if (c === ')') {
|
|
150
|
-
tokens.push({ kind: 'rparen' });
|
|
151
|
-
i++;
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
// Multi-character operators. Order matters — longest first.
|
|
155
|
-
//
|
|
156
|
-
// The "did we consume one?" check used to be `src[i - 1] === src[i]`, which
|
|
157
|
-
// is not that question. After `<=` is consumed, `c` still holds `<` from the
|
|
158
|
-
// top of the loop, so the single-character branch below pushed a SECOND `<`
|
|
159
|
-
// token and advanced i again. The extra token made `consumed !== tokens.length`
|
|
160
|
-
// in evaluateExpression, which returns undefined, which is falsy — so every
|
|
161
|
-
// showWhen using <=, >=, ==, <> , && or || silently evaluated to FALSE and hid
|
|
162
|
-
// its question. Only `!=` survived, by accident of token ordering.
|
|
163
|
-
//
|
|
164
|
-
// This is the live respondent path for link surveys and SdkSurveyHost, while
|
|
165
|
-
// the mobile and web SDKs both tokenize correctly — so the same survey branched
|
|
166
|
-
// differently depending on how a respondent arrived. Mobile already carries
|
|
167
|
-
// this fix (survey/expressionEval.ts); this is that fix, ported back.
|
|
168
|
-
let matchedMulti = false;
|
|
169
|
-
for (const op of ['<=', '>=', '!=', '<>', '&&', '||', '==']) {
|
|
170
|
-
if (src.slice(i, i + op.length) === op) {
|
|
171
|
-
tokens.push({ kind: 'op', value: op === '==' ? '=' : op });
|
|
172
|
-
i += op.length;
|
|
173
|
-
matchedMulti = true;
|
|
174
|
-
break;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
if (matchedMulti)
|
|
178
|
-
continue;
|
|
179
|
-
// Single-char operators
|
|
180
|
-
if ('=<>+-*/!'.includes(c)) {
|
|
181
|
-
tokens.push({ kind: 'op', value: c });
|
|
182
|
-
i++;
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
// Keywords: true / false / null / and / or / not / contains
|
|
186
|
-
const kw = src.slice(i).match(/^[a-zA-Z_][a-zA-Z_0-9]*/);
|
|
187
|
-
if (kw) {
|
|
188
|
-
const word = kw[0];
|
|
189
|
-
i += word.length;
|
|
190
|
-
const lower = word.toLowerCase();
|
|
191
|
-
if (lower === 'true')
|
|
192
|
-
tokens.push({ kind: 'bool', value: true });
|
|
193
|
-
else if (lower === 'false')
|
|
194
|
-
tokens.push({ kind: 'bool', value: false });
|
|
195
|
-
else if (lower === 'null')
|
|
196
|
-
tokens.push({ kind: 'null' });
|
|
197
|
-
else if (lower === 'and')
|
|
198
|
-
tokens.push({ kind: 'op', value: '&&' });
|
|
199
|
-
else if (lower === 'or')
|
|
200
|
-
tokens.push({ kind: 'op', value: '||' });
|
|
201
|
-
else if (lower === 'not')
|
|
202
|
-
tokens.push({ kind: 'op', value: '!' });
|
|
203
|
-
else if (lower === 'contains')
|
|
204
|
-
tokens.push({ kind: 'op', value: 'contains' });
|
|
205
|
-
else
|
|
206
|
-
throw new Error(`unknown identifier "${word}" — refs must be wrapped in {}`);
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
throw new Error(`unexpected character "${c}" at position ${i}`);
|
|
210
|
-
}
|
|
211
|
-
return tokens;
|
|
212
|
-
}
|
|
213
|
-
function prevExpectsBinary(tokens) {
|
|
214
|
-
const prev = tokens[tokens.length - 1];
|
|
215
|
-
if (!prev)
|
|
216
|
-
return false;
|
|
217
|
-
return prev.kind === 'num' || prev.kind === 'str' || prev.kind === 'bool'
|
|
218
|
-
|| prev.kind === 'answer_ref' || prev.kind === 'variable_ref'
|
|
219
|
-
|| prev.kind === 'rparen';
|
|
220
|
-
}
|
|
221
|
-
// ─── Parser — precedence climbing ────────────────────────────────────
|
|
222
|
-
// Operator precedence table — higher number = tighter binding.
|
|
223
|
-
const PREC = {
|
|
224
|
-
'||': 1, '&&': 2,
|
|
225
|
-
'=': 3, '!=': 3, '<>': 3, 'contains': 3,
|
|
226
|
-
'<': 4, '>': 4, '<=': 4, '>=': 4,
|
|
227
|
-
'+': 5, '-': 5,
|
|
228
|
-
'*': 6, '/': 6,
|
|
229
|
-
};
|
|
230
|
-
function parseExpression(tokens, pos, ctx, minPrec = 1) {
|
|
231
|
-
let [lhs, next] = parseUnary(tokens, pos, ctx);
|
|
232
|
-
while (next < tokens.length) {
|
|
233
|
-
const t = tokens[next];
|
|
234
|
-
if (t.kind !== 'op')
|
|
235
|
-
break;
|
|
236
|
-
const prec = PREC[t.value];
|
|
237
|
-
if (prec === undefined || prec < minPrec)
|
|
238
|
-
break;
|
|
239
|
-
const op = t.value;
|
|
240
|
-
const [rhs, after] = parseExpression(tokens, next + 1, ctx, prec + 1);
|
|
241
|
-
lhs = applyBinary(op, lhs, rhs);
|
|
242
|
-
next = after;
|
|
243
|
-
}
|
|
244
|
-
return [lhs, next];
|
|
245
|
-
}
|
|
246
|
-
function parseUnary(tokens, pos, ctx) {
|
|
247
|
-
const t = tokens[pos];
|
|
248
|
-
if (!t)
|
|
249
|
-
throw new Error('unexpected end of expression');
|
|
250
|
-
if (t.kind === 'op' && t.value === '!') {
|
|
251
|
-
const [inner, after] = parseUnary(tokens, pos + 1, ctx);
|
|
252
|
-
return [!truthy(inner), after];
|
|
253
|
-
}
|
|
254
|
-
if (t.kind === 'op' && t.value === '-') {
|
|
255
|
-
const [inner, after] = parseUnary(tokens, pos + 1, ctx);
|
|
256
|
-
return [-Number(inner), after];
|
|
257
|
-
}
|
|
258
|
-
return parsePrimary(tokens, pos, ctx);
|
|
259
|
-
}
|
|
260
|
-
function parsePrimary(tokens, pos, ctx) {
|
|
261
|
-
const t = tokens[pos];
|
|
262
|
-
if (!t)
|
|
263
|
-
throw new Error('unexpected end of expression');
|
|
264
|
-
switch (t.kind) {
|
|
265
|
-
case 'num': return [t.value, pos + 1];
|
|
266
|
-
case 'str': return [t.value, pos + 1];
|
|
267
|
-
case 'bool': return [t.value, pos + 1];
|
|
268
|
-
case 'null': return [null, pos + 1];
|
|
269
|
-
case 'answer_ref': return [ctx.answers[t.name], pos + 1];
|
|
270
|
-
case 'variable_ref': {
|
|
271
|
-
// Resolution rules for {{name.suffix}}:
|
|
272
|
-
// • {{q1.answer}} → ctx.answers.q1 (special-case for legacy)
|
|
273
|
-
// • {{customer_name}} → ctx.variables.customer_name
|
|
274
|
-
// • {{device.theme}} → walk dotted path into ctx.variables.device
|
|
275
|
-
// • {{a.b.c.d}} → walk into ctx.variables.a.b.c.d
|
|
276
|
-
// A dotted suffix that hits a non-object mid-walk returns undefined —
|
|
277
|
-
// which then compares falsy in showWhen. This lets authors reference
|
|
278
|
-
// any structured runtime signal (device, session, session.location, …)
|
|
279
|
-
// without adding new token kinds — they just live under variables.*
|
|
280
|
-
// as nested objects, injected by the SDK at render time.
|
|
281
|
-
if (t.suffix === 'answer')
|
|
282
|
-
return [ctx.answers[t.name], pos + 1];
|
|
283
|
-
const root = ctx.variables[t.name];
|
|
284
|
-
if (!t.suffix)
|
|
285
|
-
return [root, pos + 1];
|
|
286
|
-
let cursor = root;
|
|
287
|
-
for (const seg of t.suffix.split('.')) {
|
|
288
|
-
if (cursor == null || typeof cursor !== 'object') {
|
|
289
|
-
cursor = undefined;
|
|
290
|
-
break;
|
|
291
|
-
}
|
|
292
|
-
cursor = cursor[seg];
|
|
293
|
-
}
|
|
294
|
-
return [cursor, pos + 1];
|
|
295
|
-
}
|
|
296
|
-
case 'lparen': {
|
|
297
|
-
const [inner, after] = parseExpression(tokens, pos + 1, ctx, 1);
|
|
298
|
-
if (tokens[after]?.kind !== 'rparen')
|
|
299
|
-
throw new Error('missing closing paren');
|
|
300
|
-
return [inner, after + 1];
|
|
301
|
-
}
|
|
302
|
-
default: throw new Error(`unexpected token: ${JSON.stringify(t)}`);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
// ─── Operators ───────────────────────────────────────────────────────
|
|
306
|
-
function applyBinary(op, lhs, rhs) {
|
|
307
|
-
switch (op) {
|
|
308
|
-
case '=': return String(lhs) === String(rhs);
|
|
309
|
-
case '!=':
|
|
310
|
-
case '<>': return String(lhs) !== String(rhs);
|
|
311
|
-
case '<': return Number(lhs) < Number(rhs);
|
|
312
|
-
case '>': return Number(lhs) > Number(rhs);
|
|
313
|
-
case '<=': return Number(lhs) <= Number(rhs);
|
|
314
|
-
case '>=': return Number(lhs) >= Number(rhs);
|
|
315
|
-
case '&&': return truthy(lhs) && truthy(rhs);
|
|
316
|
-
case '||': return truthy(lhs) || truthy(rhs);
|
|
317
|
-
case '+': return typeof lhs === 'string' || typeof rhs === 'string'
|
|
318
|
-
? String(lhs) + String(rhs)
|
|
319
|
-
: Number(lhs) + Number(rhs);
|
|
320
|
-
case '-': return Number(lhs) - Number(rhs);
|
|
321
|
-
case '*': return Number(lhs) * Number(rhs);
|
|
322
|
-
case '/': return Number(lhs) / Number(rhs);
|
|
323
|
-
case 'contains':
|
|
324
|
-
if (Array.isArray(lhs))
|
|
325
|
-
return lhs.includes(rhs);
|
|
326
|
-
return String(lhs ?? '').includes(String(rhs));
|
|
327
|
-
default: return undefined;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
function truthy(v) {
|
|
331
|
-
if (v === false || v === null || v === undefined || v === 0 || v === '')
|
|
332
|
-
return false;
|
|
333
|
-
if (Array.isArray(v))
|
|
334
|
-
return v.length > 0;
|
|
335
|
-
return true;
|
|
336
|
-
}
|
|
337
|
-
//# sourceMappingURL=expressionEval.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"expressionEval.js","sourceRoot":"","sources":["expressionEval.ts"],"names":[],"mappings":"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"}
|
package/src/expressionEval.ts
DELETED
|
@@ -1,321 +0,0 @@
|
|
|
1
|
-
// Expression evaluator — supports the subset of operators the spec
|
|
2
|
-
// commits to (spec §7, §22):
|
|
3
|
-
//
|
|
4
|
-
// Comparators: = != < > <= >=
|
|
5
|
-
// Boolean: and or not && || !
|
|
6
|
-
// Arithmetic: + - * /
|
|
7
|
-
// Refs: {q_id} the respondent's answer to q_id
|
|
8
|
-
// {{variable}} resolved variable value
|
|
9
|
-
// 'string' string literal
|
|
10
|
-
// 42 numeric literal
|
|
11
|
-
// true / false boolean literal
|
|
12
|
-
//
|
|
13
|
-
// This is deliberately a simple tokens-based evaluator, not a full
|
|
14
|
-
// JS-compatible expression engine. It's used for:
|
|
15
|
-
// - showWhen / enabledWhen / requiredWhen (question + section)
|
|
16
|
-
// - `expression` type questions (computed value)
|
|
17
|
-
// - `calculated` variable expressions
|
|
18
|
-
//
|
|
19
|
-
// Being cautious: an untyped eval() would let survey authors run
|
|
20
|
-
// arbitrary JS against respondent answers. This restricted grammar
|
|
21
|
-
// keeps the surface area bounded.
|
|
22
|
-
|
|
23
|
-
import type { AnswersMap, VariablesMap } from './types';
|
|
24
|
-
|
|
25
|
-
export type { AnswersMap, VariablesMap, EvalContext } from './types';
|
|
26
|
-
import type { EvalContext } from './types';
|
|
27
|
-
|
|
28
|
-
// ─── Public entry point ───────────────────────────────────────────────
|
|
29
|
-
|
|
30
|
-
export function evaluateExpression(source: string, ctx: EvalContext): unknown {
|
|
31
|
-
if (typeof source !== 'string') return undefined;
|
|
32
|
-
const src = source.trim();
|
|
33
|
-
if (!src) return undefined;
|
|
34
|
-
try {
|
|
35
|
-
const tokens = tokenize(src);
|
|
36
|
-
const [value, consumed] = parseExpression(tokens, 0, ctx);
|
|
37
|
-
if (consumed !== tokens.length) return undefined; // trailing garbage
|
|
38
|
-
return value;
|
|
39
|
-
} catch {
|
|
40
|
-
return undefined;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// showWhen/enabledWhen coerce the result to boolean. Empty / undefined
|
|
45
|
-
// evaluates to false (hides the question) — friendlier than throwing.
|
|
46
|
-
export function evaluateCondition(source: string | undefined, ctx: EvalContext): boolean {
|
|
47
|
-
if (!source) return true; // no condition = always shown
|
|
48
|
-
// An expression that cannot be PARSED is not a false condition — it is not a
|
|
49
|
-
// condition. Treating a typo as "false" hid the question, and a hidden question
|
|
50
|
-
// is invisible in the response data: no answers, no error, no signal, and the
|
|
51
|
-
// author concludes nobody wanted to answer it. Silent data loss is the worse
|
|
52
|
-
// failure, so an invalid condition is ignored and the question shows. The
|
|
53
|
-
// builder is where a malformed expression should be surfaced, before it ships.
|
|
54
|
-
//
|
|
55
|
-
// A well-formed expression that evaluates falsy still hides the question, which
|
|
56
|
-
// is the whole point of showWhen. Only unparseable input is ignored.
|
|
57
|
-
if (!isParseable(source)) return true;
|
|
58
|
-
const result = evaluateExpression(source, ctx);
|
|
59
|
-
return truthy(result);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Whether `source` parses at all, independent of what it evaluates to. */
|
|
63
|
-
export function isParseable(source: string | undefined): boolean {
|
|
64
|
-
if (!source || !source.trim()) return true;
|
|
65
|
-
try {
|
|
66
|
-
const tokens = tokenize(source.trim());
|
|
67
|
-
const [, consumed] = parseExpression(tokens, 0, { variables: {}, answers: {} });
|
|
68
|
-
return consumed === tokens.length;
|
|
69
|
-
} catch {
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ─── Tokenizer ────────────────────────────────────────────────────────
|
|
75
|
-
|
|
76
|
-
type Token =
|
|
77
|
-
| { kind: 'num'; value: number }
|
|
78
|
-
| { kind: 'str'; value: string }
|
|
79
|
-
| { kind: 'bool'; value: boolean }
|
|
80
|
-
| { kind: 'null' }
|
|
81
|
-
| { kind: 'answer_ref'; name: string } // {q_id}
|
|
82
|
-
| { kind: 'variable_ref'; name: string; suffix?: string } // {{name}} or {{q_id.answer}}
|
|
83
|
-
| { kind: 'op'; value: string }
|
|
84
|
-
| { kind: 'lparen' }
|
|
85
|
-
| { kind: 'rparen' };
|
|
86
|
-
|
|
87
|
-
function tokenize(src: string): Token[] {
|
|
88
|
-
const tokens: Token[] = [];
|
|
89
|
-
let i = 0;
|
|
90
|
-
const len = src.length;
|
|
91
|
-
|
|
92
|
-
while (i < len) {
|
|
93
|
-
const c = src[i];
|
|
94
|
-
|
|
95
|
-
// whitespace
|
|
96
|
-
if (c === ' ' || c === '\t' || c === '\n' || c === '\r') { i++; continue; }
|
|
97
|
-
|
|
98
|
-
// string literal — single or double quoted
|
|
99
|
-
if (c === "'" || c === '"') {
|
|
100
|
-
const quote = c;
|
|
101
|
-
let out = '';
|
|
102
|
-
i++;
|
|
103
|
-
while (i < len && src[i] !== quote) {
|
|
104
|
-
if (src[i] === '\\' && i + 1 < len) { out += src[i + 1]; i += 2; }
|
|
105
|
-
else { out += src[i]; i++; }
|
|
106
|
-
}
|
|
107
|
-
i++; // closing quote
|
|
108
|
-
tokens.push({ kind: 'str', value: out });
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// numeric literal
|
|
113
|
-
if ((c >= '0' && c <= '9') || (c === '-' && src[i + 1] >= '0' && src[i + 1] <= '9' && !prevExpectsBinary(tokens))) {
|
|
114
|
-
let j = i;
|
|
115
|
-
if (src[j] === '-') j++;
|
|
116
|
-
while (j < len && ((src[j] >= '0' && src[j] <= '9') || src[j] === '.')) j++;
|
|
117
|
-
tokens.push({ kind: 'num', value: parseFloat(src.slice(i, j)) });
|
|
118
|
-
i = j;
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// {{variable_ref}} — spec §22 interpolation shape
|
|
123
|
-
if (c === '{' && src[i + 1] === '{') {
|
|
124
|
-
const end = src.indexOf('}}', i + 2);
|
|
125
|
-
if (end < 0) throw new Error('unterminated {{');
|
|
126
|
-
const inner = src.slice(i + 2, end).trim();
|
|
127
|
-
const dot = inner.indexOf('.');
|
|
128
|
-
if (dot > 0) {
|
|
129
|
-
tokens.push({ kind: 'variable_ref', name: inner.slice(0, dot).trim(), suffix: inner.slice(dot + 1).trim() });
|
|
130
|
-
} else {
|
|
131
|
-
tokens.push({ kind: 'variable_ref', name: inner });
|
|
132
|
-
}
|
|
133
|
-
i = end + 2;
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// {q_id} — answer reference (single-brace, spec §7)
|
|
138
|
-
if (c === '{') {
|
|
139
|
-
const end = src.indexOf('}', i + 1);
|
|
140
|
-
if (end < 0) throw new Error('unterminated {');
|
|
141
|
-
tokens.push({ kind: 'answer_ref', name: src.slice(i + 1, end).trim() });
|
|
142
|
-
i = end + 1;
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// parentheses
|
|
147
|
-
if (c === '(') { tokens.push({ kind: 'lparen' }); i++; continue; }
|
|
148
|
-
if (c === ')') { tokens.push({ kind: 'rparen' }); i++; continue; }
|
|
149
|
-
|
|
150
|
-
// Multi-character operators. Order matters — longest first.
|
|
151
|
-
//
|
|
152
|
-
// The "did we consume one?" check used to be `src[i - 1] === src[i]`, which
|
|
153
|
-
// is not that question. After `<=` is consumed, `c` still holds `<` from the
|
|
154
|
-
// top of the loop, so the single-character branch below pushed a SECOND `<`
|
|
155
|
-
// token and advanced i again. The extra token made `consumed !== tokens.length`
|
|
156
|
-
// in evaluateExpression, which returns undefined, which is falsy — so every
|
|
157
|
-
// showWhen using <=, >=, ==, <> , && or || silently evaluated to FALSE and hid
|
|
158
|
-
// its question. Only `!=` survived, by accident of token ordering.
|
|
159
|
-
//
|
|
160
|
-
// This is the live respondent path for link surveys and SdkSurveyHost, while
|
|
161
|
-
// the mobile and web SDKs both tokenize correctly — so the same survey branched
|
|
162
|
-
// differently depending on how a respondent arrived. Mobile already carries
|
|
163
|
-
// this fix (survey/expressionEval.ts); this is that fix, ported back.
|
|
164
|
-
let matchedMulti = false;
|
|
165
|
-
for (const op of ['<=', '>=', '!=', '<>', '&&', '||', '==']) {
|
|
166
|
-
if (src.slice(i, i + op.length) === op) {
|
|
167
|
-
tokens.push({ kind: 'op', value: op === '==' ? '=' : op });
|
|
168
|
-
i += op.length;
|
|
169
|
-
matchedMulti = true;
|
|
170
|
-
break;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
if (matchedMulti) continue;
|
|
174
|
-
|
|
175
|
-
// Single-char operators
|
|
176
|
-
if ('=<>+-*/!'.includes(c)) {
|
|
177
|
-
tokens.push({ kind: 'op', value: c });
|
|
178
|
-
i++;
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Keywords: true / false / null / and / or / not / contains
|
|
183
|
-
const kw = src.slice(i).match(/^[a-zA-Z_][a-zA-Z_0-9]*/);
|
|
184
|
-
if (kw) {
|
|
185
|
-
const word = kw[0];
|
|
186
|
-
i += word.length;
|
|
187
|
-
const lower = word.toLowerCase();
|
|
188
|
-
if (lower === 'true') tokens.push({ kind: 'bool', value: true });
|
|
189
|
-
else if (lower === 'false') tokens.push({ kind: 'bool', value: false });
|
|
190
|
-
else if (lower === 'null') tokens.push({ kind: 'null' });
|
|
191
|
-
else if (lower === 'and') tokens.push({ kind: 'op', value: '&&' });
|
|
192
|
-
else if (lower === 'or') tokens.push({ kind: 'op', value: '||' });
|
|
193
|
-
else if (lower === 'not') tokens.push({ kind: 'op', value: '!' });
|
|
194
|
-
else if (lower === 'contains') tokens.push({ kind: 'op', value: 'contains' });
|
|
195
|
-
else throw new Error(`unknown identifier "${word}" — refs must be wrapped in {}`);
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
throw new Error(`unexpected character "${c}" at position ${i}`);
|
|
200
|
-
}
|
|
201
|
-
return tokens;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function prevExpectsBinary(tokens: Token[]): boolean {
|
|
205
|
-
const prev = tokens[tokens.length - 1];
|
|
206
|
-
if (!prev) return false;
|
|
207
|
-
return prev.kind === 'num' || prev.kind === 'str' || prev.kind === 'bool'
|
|
208
|
-
|| prev.kind === 'answer_ref' || prev.kind === 'variable_ref'
|
|
209
|
-
|| prev.kind === 'rparen';
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// ─── Parser — precedence climbing ────────────────────────────────────
|
|
213
|
-
|
|
214
|
-
// Operator precedence table — higher number = tighter binding.
|
|
215
|
-
const PREC: Record<string, number> = {
|
|
216
|
-
'||': 1, '&&': 2,
|
|
217
|
-
'=': 3, '!=': 3, '<>': 3, 'contains': 3,
|
|
218
|
-
'<': 4, '>': 4, '<=': 4, '>=': 4,
|
|
219
|
-
'+': 5, '-': 5,
|
|
220
|
-
'*': 6, '/': 6,
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
function parseExpression(tokens: Token[], pos: number, ctx: EvalContext, minPrec = 1): [unknown, number] {
|
|
224
|
-
let [lhs, next] = parseUnary(tokens, pos, ctx);
|
|
225
|
-
while (next < tokens.length) {
|
|
226
|
-
const t = tokens[next];
|
|
227
|
-
if (t.kind !== 'op') break;
|
|
228
|
-
const prec = PREC[t.value];
|
|
229
|
-
if (prec === undefined || prec < minPrec) break;
|
|
230
|
-
const op = t.value;
|
|
231
|
-
const [rhs, after] = parseExpression(tokens, next + 1, ctx, prec + 1);
|
|
232
|
-
lhs = applyBinary(op, lhs, rhs);
|
|
233
|
-
next = after;
|
|
234
|
-
}
|
|
235
|
-
return [lhs, next];
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
function parseUnary(tokens: Token[], pos: number, ctx: EvalContext): [unknown, number] {
|
|
239
|
-
const t = tokens[pos];
|
|
240
|
-
if (!t) throw new Error('unexpected end of expression');
|
|
241
|
-
if (t.kind === 'op' && t.value === '!') {
|
|
242
|
-
const [inner, after] = parseUnary(tokens, pos + 1, ctx);
|
|
243
|
-
return [!truthy(inner), after];
|
|
244
|
-
}
|
|
245
|
-
if (t.kind === 'op' && t.value === '-') {
|
|
246
|
-
const [inner, after] = parseUnary(tokens, pos + 1, ctx);
|
|
247
|
-
return [-Number(inner), after];
|
|
248
|
-
}
|
|
249
|
-
return parsePrimary(tokens, pos, ctx);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function parsePrimary(tokens: Token[], pos: number, ctx: EvalContext): [unknown, number] {
|
|
253
|
-
const t = tokens[pos];
|
|
254
|
-
if (!t) throw new Error('unexpected end of expression');
|
|
255
|
-
switch (t.kind) {
|
|
256
|
-
case 'num': return [t.value, pos + 1];
|
|
257
|
-
case 'str': return [t.value, pos + 1];
|
|
258
|
-
case 'bool': return [t.value, pos + 1];
|
|
259
|
-
case 'null': return [null, pos + 1];
|
|
260
|
-
case 'answer_ref': return [ctx.answers[t.name], pos + 1];
|
|
261
|
-
case 'variable_ref': {
|
|
262
|
-
// Resolution rules for {{name.suffix}}:
|
|
263
|
-
// • {{q1.answer}} → ctx.answers.q1 (special-case for legacy)
|
|
264
|
-
// • {{customer_name}} → ctx.variables.customer_name
|
|
265
|
-
// • {{device.theme}} → walk dotted path into ctx.variables.device
|
|
266
|
-
// • {{a.b.c.d}} → walk into ctx.variables.a.b.c.d
|
|
267
|
-
// A dotted suffix that hits a non-object mid-walk returns undefined —
|
|
268
|
-
// which then compares falsy in showWhen. This lets authors reference
|
|
269
|
-
// any structured runtime signal (device, session, session.location, …)
|
|
270
|
-
// without adding new token kinds — they just live under variables.*
|
|
271
|
-
// as nested objects, injected by the SDK at render time.
|
|
272
|
-
if (t.suffix === 'answer') return [ctx.answers[t.name], pos + 1];
|
|
273
|
-
const root = ctx.variables[t.name];
|
|
274
|
-
if (!t.suffix) return [root, pos + 1];
|
|
275
|
-
let cursor: unknown = root;
|
|
276
|
-
for (const seg of t.suffix.split('.')) {
|
|
277
|
-
if (cursor == null || typeof cursor !== 'object') { cursor = undefined; break; }
|
|
278
|
-
cursor = (cursor as Record<string, unknown>)[seg];
|
|
279
|
-
}
|
|
280
|
-
return [cursor, pos + 1];
|
|
281
|
-
}
|
|
282
|
-
case 'lparen': {
|
|
283
|
-
const [inner, after] = parseExpression(tokens, pos + 1, ctx, 1);
|
|
284
|
-
if (tokens[after]?.kind !== 'rparen') throw new Error('missing closing paren');
|
|
285
|
-
return [inner, after + 1];
|
|
286
|
-
}
|
|
287
|
-
default: throw new Error(`unexpected token: ${JSON.stringify(t)}`);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// ─── Operators ───────────────────────────────────────────────────────
|
|
292
|
-
|
|
293
|
-
function applyBinary(op: string, lhs: unknown, rhs: unknown): unknown {
|
|
294
|
-
switch (op) {
|
|
295
|
-
case '=': return String(lhs) === String(rhs);
|
|
296
|
-
case '!=':
|
|
297
|
-
case '<>': return String(lhs) !== String(rhs);
|
|
298
|
-
case '<': return Number(lhs) < Number(rhs);
|
|
299
|
-
case '>': return Number(lhs) > Number(rhs);
|
|
300
|
-
case '<=': return Number(lhs) <= Number(rhs);
|
|
301
|
-
case '>=': return Number(lhs) >= Number(rhs);
|
|
302
|
-
case '&&': return truthy(lhs) && truthy(rhs);
|
|
303
|
-
case '||': return truthy(lhs) || truthy(rhs);
|
|
304
|
-
case '+': return typeof lhs === 'string' || typeof rhs === 'string'
|
|
305
|
-
? String(lhs) + String(rhs)
|
|
306
|
-
: Number(lhs) + Number(rhs);
|
|
307
|
-
case '-': return Number(lhs) - Number(rhs);
|
|
308
|
-
case '*': return Number(lhs) * Number(rhs);
|
|
309
|
-
case '/': return Number(lhs) / Number(rhs);
|
|
310
|
-
case 'contains':
|
|
311
|
-
if (Array.isArray(lhs)) return lhs.includes(rhs);
|
|
312
|
-
return String(lhs ?? '').includes(String(rhs));
|
|
313
|
-
default: return undefined;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function truthy(v: unknown): boolean {
|
|
318
|
-
if (v === false || v === null || v === undefined || v === 0 || v === '') return false;
|
|
319
|
-
if (Array.isArray(v)) return v.length > 0;
|
|
320
|
-
return true;
|
|
321
|
-
}
|
package/src/index.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @cxpinsight/survey-spec
|
|
3
|
-
*
|
|
4
|
-
* Behaviour that must be identical for a respondent regardless of how they
|
|
5
|
-
* arrived — a link, a site running the web SDK, or a mobile app. Three renderers
|
|
6
|
-
* implemented this separately and drifted: the React tokenizer silently hid every
|
|
7
|
-
* question whose condition used `<=`, `>=`, `==`, `<>`, `&&` or `||`, and the web
|
|
8
|
-
* SDK could not resolve any variable namespace except `device`.
|
|
9
|
-
*
|
|
10
|
-
* tools/survey-conformance runs the same fixtures through every renderer and
|
|
11
|
-
* fails on any disagreement.
|
|
12
|
-
*/
|
|
13
|
-
export { evaluateExpression, evaluateCondition, isParseable } from './expressionEval';
|
|
14
|
-
export { resolveVariable } from './resolveVariable';
|
|
15
|
-
export type { AnswersMap, VariablesMap, EvalContext } from './types';
|
|
16
|
-
//# sourceMappingURL=index.d.ts.map
|
package/src/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACtF,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC"}
|
package/src/index.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.resolveVariable = exports.isParseable = exports.evaluateCondition = exports.evaluateExpression = void 0;
|
|
4
|
-
/**
|
|
5
|
-
* @cxpinsight/survey-spec
|
|
6
|
-
*
|
|
7
|
-
* Behaviour that must be identical for a respondent regardless of how they
|
|
8
|
-
* arrived — a link, a site running the web SDK, or a mobile app. Three renderers
|
|
9
|
-
* implemented this separately and drifted: the React tokenizer silently hid every
|
|
10
|
-
* question whose condition used `<=`, `>=`, `==`, `<>`, `&&` or `||`, and the web
|
|
11
|
-
* SDK could not resolve any variable namespace except `device`.
|
|
12
|
-
*
|
|
13
|
-
* tools/survey-conformance runs the same fixtures through every renderer and
|
|
14
|
-
* fails on any disagreement.
|
|
15
|
-
*/
|
|
16
|
-
var expressionEval_1 = require("./expressionEval");
|
|
17
|
-
Object.defineProperty(exports, "evaluateExpression", { enumerable: true, get: function () { return expressionEval_1.evaluateExpression; } });
|
|
18
|
-
Object.defineProperty(exports, "evaluateCondition", { enumerable: true, get: function () { return expressionEval_1.evaluateCondition; } });
|
|
19
|
-
Object.defineProperty(exports, "isParseable", { enumerable: true, get: function () { return expressionEval_1.isParseable; } });
|
|
20
|
-
var resolveVariable_1 = require("./resolveVariable");
|
|
21
|
-
Object.defineProperty(exports, "resolveVariable", { enumerable: true, get: function () { return resolveVariable_1.resolveVariable; } });
|
|
22
|
-
//# sourceMappingURL=index.js.map
|
package/src/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;GAWG;AACH,mDAAsF;AAA7E,oHAAA,kBAAkB,OAAA;AAAE,mHAAA,iBAAiB,OAAA;AAAE,6GAAA,WAAW,OAAA;AAC3D,qDAAoD;AAA3C,kHAAA,eAAe,OAAA"}
|
package/src/index.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @cxpinsight/survey-spec
|
|
3
|
-
*
|
|
4
|
-
* Behaviour that must be identical for a respondent regardless of how they
|
|
5
|
-
* arrived — a link, a site running the web SDK, or a mobile app. Three renderers
|
|
6
|
-
* implemented this separately and drifted: the React tokenizer silently hid every
|
|
7
|
-
* question whose condition used `<=`, `>=`, `==`, `<>`, `&&` or `||`, and the web
|
|
8
|
-
* SDK could not resolve any variable namespace except `device`.
|
|
9
|
-
*
|
|
10
|
-
* tools/survey-conformance runs the same fixtures through every renderer and
|
|
11
|
-
* fails on any disagreement.
|
|
12
|
-
*/
|
|
13
|
-
export { evaluateExpression, evaluateCondition, isParseable } from './expressionEval';
|
|
14
|
-
export { resolveVariable } from './resolveVariable';
|
|
15
|
-
export type { AnswersMap, VariablesMap, EvalContext } from './types';
|
package/src/resolveVariable.d.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* One rule for resolving `{{name}}` and `{{name.path}}` against the variable bag.
|
|
3
|
-
*
|
|
4
|
-
* It exists because the same reference meant two different things depending on
|
|
5
|
-
* where it appeared. Conditions walked nested namespaces; text interpolation
|
|
6
|
-
* looked up a flat dotted key and, when that missed, returned the NAMESPACE
|
|
7
|
-
* OBJECT — so a nested bag rendered
|
|
8
|
-
*
|
|
9
|
-
* "Hi {{user.name}}" → Hi {"plan":"pro","name":"Sam","email":"…"}
|
|
10
|
-
*
|
|
11
|
-
* putting every field of that namespace on screen in front of the respondent.
|
|
12
|
-
* An author has no reason to expect two resolution rules for one syntax, so
|
|
13
|
-
* there is now one, and both callers use it.
|
|
14
|
-
*
|
|
15
|
-
* Flat key first, then walk: a bag built the old way keeps resolving unchanged,
|
|
16
|
-
* and a nested bag resolves properly.
|
|
17
|
-
*/
|
|
18
|
-
export declare function resolveVariable(variables: Record<string, unknown>, name: string, suffix?: string): unknown;
|
|
19
|
-
//# sourceMappingURL=resolveVariable.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"resolveVariable.d.ts","sourceRoot":"","sources":["resolveVariable.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAClC,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CA4BT"}
|
package/src/resolveVariable.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.resolveVariable = resolveVariable;
|
|
4
|
-
/**
|
|
5
|
-
* One rule for resolving `{{name}}` and `{{name.path}}` against the variable bag.
|
|
6
|
-
*
|
|
7
|
-
* It exists because the same reference meant two different things depending on
|
|
8
|
-
* where it appeared. Conditions walked nested namespaces; text interpolation
|
|
9
|
-
* looked up a flat dotted key and, when that missed, returned the NAMESPACE
|
|
10
|
-
* OBJECT — so a nested bag rendered
|
|
11
|
-
*
|
|
12
|
-
* "Hi {{user.name}}" → Hi {"plan":"pro","name":"Sam","email":"…"}
|
|
13
|
-
*
|
|
14
|
-
* putting every field of that namespace on screen in front of the respondent.
|
|
15
|
-
* An author has no reason to expect two resolution rules for one syntax, so
|
|
16
|
-
* there is now one, and both callers use it.
|
|
17
|
-
*
|
|
18
|
-
* Flat key first, then walk: a bag built the old way keeps resolving unchanged,
|
|
19
|
-
* and a nested bag resolves properly.
|
|
20
|
-
*/
|
|
21
|
-
function resolveVariable(variables, name, suffix) {
|
|
22
|
-
if (!variables)
|
|
23
|
-
return undefined;
|
|
24
|
-
const full = suffix ? `${name}.${suffix}` : name;
|
|
25
|
-
let value;
|
|
26
|
-
if (Object.prototype.hasOwnProperty.call(variables, full)) {
|
|
27
|
-
// 1. Exact flat key — what bags built before namespaces contained.
|
|
28
|
-
value = variables[full];
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
// 2. Walk the dotted path into nested namespaces.
|
|
32
|
-
const segments = full.split('.');
|
|
33
|
-
let cursor = variables;
|
|
34
|
-
for (const seg of segments) {
|
|
35
|
-
if (cursor == null || typeof cursor !== 'object') {
|
|
36
|
-
cursor = undefined;
|
|
37
|
-
break;
|
|
38
|
-
}
|
|
39
|
-
cursor = cursor[seg];
|
|
40
|
-
}
|
|
41
|
-
value = cursor;
|
|
42
|
-
}
|
|
43
|
-
// 3. A namespace is not a value. `{{user}}` with no path resolves to the whole
|
|
44
|
-
// object, and stringifying it puts every field of that bag — plan, email,
|
|
45
|
-
// internal ids — on screen in front of the respondent. Applies to BOTH
|
|
46
|
-
// lookup paths: a flat bag can hold an object under a bare key too.
|
|
47
|
-
// Arrays are values (a multi-select answer), so they pass.
|
|
48
|
-
if (value !== null && typeof value === 'object' && !Array.isArray(value))
|
|
49
|
-
return undefined;
|
|
50
|
-
return value;
|
|
51
|
-
}
|
|
52
|
-
//# sourceMappingURL=resolveVariable.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"resolveVariable.js","sourceRoot":"","sources":["resolveVariable.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,yBACE,SAAkC,EAClC,IAAY,EACZ,MAAe;IAEf,IAAI,CAAC,SAAS;QAAE,OAAO,SAAS,CAAC;IAEjC,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,MAAM,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IAEjD,IAAI,KAAc,CAAC;IACnB,IAAI,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,CAAC;QAC1D,mEAAmE;QACnE,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;SAAM,CAAC;QACN,kDAAkD;QAClD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,MAAM,GAAY,SAAS,CAAC;QAChC,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;YAC3B,IAAI,MAAM,IAAI,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;gBAAC,MAAM,GAAG,SAAS,CAAC;gBAAC,MAAM;YAAC,CAAC;YAChF,MAAM,GAAI,MAAkC,CAAC,GAAG,CAAC,CAAC;QACpD,CAAC;QACD,KAAK,GAAG,MAAM,CAAC;IACjB,CAAC;IAED,+EAA+E;IAC/E,6EAA6E;IAC7E,0EAA0E;IAC1E,uEAAuE;IACvE,8DAA8D;IAC9D,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAE3F,OAAO,KAAK,CAAC;AACf,CAAC"}
|
package/src/resolveVariable.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* One rule for resolving `{{name}}` and `{{name.path}}` against the variable bag.
|
|
3
|
-
*
|
|
4
|
-
* It exists because the same reference meant two different things depending on
|
|
5
|
-
* where it appeared. Conditions walked nested namespaces; text interpolation
|
|
6
|
-
* looked up a flat dotted key and, when that missed, returned the NAMESPACE
|
|
7
|
-
* OBJECT — so a nested bag rendered
|
|
8
|
-
*
|
|
9
|
-
* "Hi {{user.name}}" → Hi {"plan":"pro","name":"Sam","email":"…"}
|
|
10
|
-
*
|
|
11
|
-
* putting every field of that namespace on screen in front of the respondent.
|
|
12
|
-
* An author has no reason to expect two resolution rules for one syntax, so
|
|
13
|
-
* there is now one, and both callers use it.
|
|
14
|
-
*
|
|
15
|
-
* Flat key first, then walk: a bag built the old way keeps resolving unchanged,
|
|
16
|
-
* and a nested bag resolves properly.
|
|
17
|
-
*/
|
|
18
|
-
export function resolveVariable(
|
|
19
|
-
variables: Record<string, unknown>,
|
|
20
|
-
name: string,
|
|
21
|
-
suffix?: string,
|
|
22
|
-
): unknown {
|
|
23
|
-
if (!variables) return undefined;
|
|
24
|
-
|
|
25
|
-
const full = suffix ? `${name}.${suffix}` : name;
|
|
26
|
-
|
|
27
|
-
let value: unknown;
|
|
28
|
-
if (Object.prototype.hasOwnProperty.call(variables, full)) {
|
|
29
|
-
// 1. Exact flat key — what bags built before namespaces contained.
|
|
30
|
-
value = variables[full];
|
|
31
|
-
} else {
|
|
32
|
-
// 2. Walk the dotted path into nested namespaces.
|
|
33
|
-
const segments = full.split('.');
|
|
34
|
-
let cursor: unknown = variables;
|
|
35
|
-
for (const seg of segments) {
|
|
36
|
-
if (cursor == null || typeof cursor !== 'object') { cursor = undefined; break; }
|
|
37
|
-
cursor = (cursor as Record<string, unknown>)[seg];
|
|
38
|
-
}
|
|
39
|
-
value = cursor;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// 3. A namespace is not a value. `{{user}}` with no path resolves to the whole
|
|
43
|
-
// object, and stringifying it puts every field of that bag — plan, email,
|
|
44
|
-
// internal ids — on screen in front of the respondent. Applies to BOTH
|
|
45
|
-
// lookup paths: a flat bag can hold an object under a bare key too.
|
|
46
|
-
// Arrays are values (a multi-select answer), so they pass.
|
|
47
|
-
if (value !== null && typeof value === 'object' && !Array.isArray(value)) return undefined;
|
|
48
|
-
|
|
49
|
-
return value;
|
|
50
|
-
}
|
package/src/types.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/** Answers keyed by question id, as the renderer has collected them so far. */
|
|
2
|
-
export type AnswersMap = Record<string, unknown>;
|
|
3
|
-
/**
|
|
4
|
-
* The variable environment a condition resolves against.
|
|
5
|
-
*
|
|
6
|
-
* Namespaces are NESTED objects — `variables.device.theme`, `variables.user.plan`
|
|
7
|
-
* — which is what lets an SDK expose structured runtime signals without the
|
|
8
|
-
* expression grammar growing new token kinds. The web SDK used to flatten one
|
|
9
|
-
* namespace by concatenating `'device.'` onto a key, which meant every OTHER
|
|
10
|
-
* namespace silently evaluated false there and true elsewhere.
|
|
11
|
-
*/
|
|
12
|
-
export type VariablesMap = Record<string, unknown>;
|
|
13
|
-
export interface EvalContext {
|
|
14
|
-
variables: VariablesMap;
|
|
15
|
-
answers: AnswersMap;
|
|
16
|
-
}
|
|
17
|
-
//# sourceMappingURL=types.d.ts.map
|
package/src/types.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEjD;;;;;;;;GAQG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEnD,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,YAAY,CAAC;IACxB,OAAO,EAAE,UAAU,CAAC;CACrB"}
|
package/src/types.js
DELETED
package/src/types.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":""}
|
package/src/types.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/** Answers keyed by question id, as the renderer has collected them so far. */
|
|
2
|
-
export type AnswersMap = Record<string, unknown>;
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* The variable environment a condition resolves against.
|
|
6
|
-
*
|
|
7
|
-
* Namespaces are NESTED objects — `variables.device.theme`, `variables.user.plan`
|
|
8
|
-
* — which is what lets an SDK expose structured runtime signals without the
|
|
9
|
-
* expression grammar growing new token kinds. The web SDK used to flatten one
|
|
10
|
-
* namespace by concatenating `'device.'` onto a key, which meant every OTHER
|
|
11
|
-
* namespace silently evaluated false there and true elsewhere.
|
|
12
|
-
*/
|
|
13
|
-
export type VariablesMap = Record<string, unknown>;
|
|
14
|
-
|
|
15
|
-
export interface EvalContext {
|
|
16
|
-
variables: VariablesMap;
|
|
17
|
-
answers: AnswersMap;
|
|
18
|
-
}
|