@cxpinsight/survey-spec 0.4.0 → 0.5.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/dist/evaluateTree.d.ts +50 -0
- package/dist/evaluateTree.d.ts.map +1 -0
- package/dist/evaluateTree.js +166 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -1
- package/dist/messages.d.ts +38 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +68 -0
- package/dist/resolveVariable.d.ts.map +1 -1
- package/dist/resolveVariable.js +13 -0
- package/dist/validate.d.ts +313 -40
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +529 -31
- package/package.json +2 -2
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Walk a compiled condition tree.
|
|
3
|
+
*
|
|
4
|
+
* The counterpart to `evaluateCondition`, which parses an author's expression
|
|
5
|
+
* string every time it is asked. The server can compile that string once, at
|
|
6
|
+
* publish time, and send the tree instead — so a client never carries a
|
|
7
|
+
* tokeniser or a precedence table. That is 206 of expressionEval's 321 lines
|
|
8
|
+
* gone from every future SDK, and the remaining evaluator is small enough to
|
|
9
|
+
* write again in Swift or Kotlin without it becoming a project.
|
|
10
|
+
*
|
|
11
|
+
* Node grammar and every coercion rule are in
|
|
12
|
+
* contracts/sdk/vocabulary.v1.json. The coercion rules matter more than they
|
|
13
|
+
* look: a differential test against a Swift implementation of exactly this
|
|
14
|
+
* disagreed on 7% of generated inputs, and every divergence came from one of
|
|
15
|
+
* them being unstated.
|
|
16
|
+
*
|
|
17
|
+
* Returns `null` for a tree carrying something this build cannot execute —
|
|
18
|
+
* never `false`. A condition that could not be evaluated must not silently
|
|
19
|
+
* hide a question; the caller decides.
|
|
20
|
+
*/
|
|
21
|
+
export type Operand = {
|
|
22
|
+
lit: unknown;
|
|
23
|
+
} | {
|
|
24
|
+
ref: string;
|
|
25
|
+
} | {
|
|
26
|
+
arith: '+' | '-' | '*' | '/';
|
|
27
|
+
of: [Operand, Operand];
|
|
28
|
+
};
|
|
29
|
+
export type ConditionNode = {
|
|
30
|
+
logic: 'all' | 'any' | 'not';
|
|
31
|
+
of: ConditionNode[];
|
|
32
|
+
} | {
|
|
33
|
+
left: Operand;
|
|
34
|
+
op: string;
|
|
35
|
+
right: Operand;
|
|
36
|
+
};
|
|
37
|
+
export interface TreeContext {
|
|
38
|
+
answers?: Record<string, unknown>;
|
|
39
|
+
variables?: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
/** The contract's cap. Deeper than this, Apple Foundation refuses the payload. */
|
|
42
|
+
export declare const MAX_TREE_DEPTH = 32;
|
|
43
|
+
/** `null` means an operator this build does not implement — reject, never guess. */
|
|
44
|
+
export declare function compareValues(a: unknown, op: string, b: unknown): boolean | null;
|
|
45
|
+
/**
|
|
46
|
+
* A null tree means "no condition", which every renderer treats as SHOW.
|
|
47
|
+
* @returns true, false, or null when the tree could not be executed.
|
|
48
|
+
*/
|
|
49
|
+
export declare function evaluateTree(node: unknown, ctx: TreeContext, depth?: number): boolean | null;
|
|
50
|
+
//# sourceMappingURL=evaluateTree.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evaluateTree.d.ts","sourceRoot":"","sources":["../src/evaluateTree.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,MAAM,OAAO,GACf;IAAE,GAAG,EAAE,OAAO,CAAA;CAAE,GAChB;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,GACf;IAAE,KAAK,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;IAAC,EAAE,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC;AAE7D,MAAM,MAAM,aAAa,GACrB;IAAE,KAAK,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;IAAC,EAAE,EAAE,aAAa,EAAE,CAAA;CAAE,GACrD;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAElD,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACrC;AAED,kFAAkF;AAClF,eAAO,MAAM,cAAc,KAAK,CAAC;AA0DjC,oFAAoF;AACpF,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI,CAqChF;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,WAAW,EAAE,KAAK,SAAI,GAAG,OAAO,GAAG,IAAI,CAsBvF"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Walk a compiled condition tree.
|
|
4
|
+
*
|
|
5
|
+
* The counterpart to `evaluateCondition`, which parses an author's expression
|
|
6
|
+
* string every time it is asked. The server can compile that string once, at
|
|
7
|
+
* publish time, and send the tree instead — so a client never carries a
|
|
8
|
+
* tokeniser or a precedence table. That is 206 of expressionEval's 321 lines
|
|
9
|
+
* gone from every future SDK, and the remaining evaluator is small enough to
|
|
10
|
+
* write again in Swift or Kotlin without it becoming a project.
|
|
11
|
+
*
|
|
12
|
+
* Node grammar and every coercion rule are in
|
|
13
|
+
* contracts/sdk/vocabulary.v1.json. The coercion rules matter more than they
|
|
14
|
+
* look: a differential test against a Swift implementation of exactly this
|
|
15
|
+
* disagreed on 7% of generated inputs, and every divergence came from one of
|
|
16
|
+
* them being unstated.
|
|
17
|
+
*
|
|
18
|
+
* Returns `null` for a tree carrying something this build cannot execute —
|
|
19
|
+
* never `false`. A condition that could not be evaluated must not silently
|
|
20
|
+
* hide a question; the caller decides.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.MAX_TREE_DEPTH = void 0;
|
|
24
|
+
exports.compareValues = compareValues;
|
|
25
|
+
exports.evaluateTree = evaluateTree;
|
|
26
|
+
/** The contract's cap. Deeper than this, Apple Foundation refuses the payload. */
|
|
27
|
+
exports.MAX_TREE_DEPTH = 32;
|
|
28
|
+
const own = (o, k) => (o != null && Object.prototype.hasOwnProperty.call(o, k) ? o[k] : undefined);
|
|
29
|
+
/**
|
|
30
|
+
* Text form. NOT `String(v)`: `String(null)` is the four characters "null", so
|
|
31
|
+
* `{q1} contains "null"` would match an unanswered question.
|
|
32
|
+
*/
|
|
33
|
+
function str(v) {
|
|
34
|
+
if (v === null || v === undefined)
|
|
35
|
+
return '';
|
|
36
|
+
if (Array.isArray(v))
|
|
37
|
+
return v.map(str).join(',');
|
|
38
|
+
return String(v);
|
|
39
|
+
}
|
|
40
|
+
/** Absent, empty text, or an empty multi-select. */
|
|
41
|
+
function empty(v) {
|
|
42
|
+
return v === null || v === undefined || v === '' || (Array.isArray(v) && v.length === 0);
|
|
43
|
+
}
|
|
44
|
+
/** Both sides numeric-looking? Then compare as numbers, not as text. */
|
|
45
|
+
function numeric(a, b) {
|
|
46
|
+
return a !== '' && b !== '' && a != null && b != null &&
|
|
47
|
+
!isNaN(Number(a)) && !isNaN(Number(b));
|
|
48
|
+
}
|
|
49
|
+
/** Follow `answers.q1` / `variables.user.plan`, own properties only. */
|
|
50
|
+
function resolve(ref, ctx) {
|
|
51
|
+
const parts = String(ref).split('.');
|
|
52
|
+
let cur = own(ctx, parts[0]);
|
|
53
|
+
for (let i = 1; i < parts.length && cur != null; i++)
|
|
54
|
+
cur = own(cur, parts[i]);
|
|
55
|
+
if (cur === undefined && parts.length > 2) {
|
|
56
|
+
const flat = own(own(ctx, parts[0]) || {}, parts.slice(1).join('.'));
|
|
57
|
+
if (flat !== undefined)
|
|
58
|
+
return flat;
|
|
59
|
+
}
|
|
60
|
+
return cur;
|
|
61
|
+
}
|
|
62
|
+
function operand(node, ctx) {
|
|
63
|
+
if (node == null || typeof node !== 'object')
|
|
64
|
+
return undefined;
|
|
65
|
+
const n = node;
|
|
66
|
+
if ('lit' in n)
|
|
67
|
+
return n.lit;
|
|
68
|
+
if ('ref' in n)
|
|
69
|
+
return resolve(String(n.ref), ctx);
|
|
70
|
+
if ('arith' in n) {
|
|
71
|
+
const of = n.of;
|
|
72
|
+
const a = Number(operand(of[0], ctx));
|
|
73
|
+
const b = Number(operand(of[1], ctx));
|
|
74
|
+
switch (n.arith) {
|
|
75
|
+
case '+': return a + b;
|
|
76
|
+
case '-': return a - b;
|
|
77
|
+
case '*': return a * b;
|
|
78
|
+
case '/': return b === 0 ? undefined : a / b;
|
|
79
|
+
default: return undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
/** `null` means an operator this build does not implement — reject, never guess. */
|
|
85
|
+
function compareValues(a, op, b) {
|
|
86
|
+
switch (op) {
|
|
87
|
+
case 'truthy': return !!a && a !== '0' && a !== 'false';
|
|
88
|
+
// An ABSENT value equals nothing, not even the empty string.
|
|
89
|
+
//
|
|
90
|
+
// str() renders absent as '', which made `{why} != ""` FALSE for an
|
|
91
|
+
// unanswered question — the parser says true, and a follow-up gated on
|
|
92
|
+
// "why has been answered" would have stopped appearing. Found by the
|
|
93
|
+
// end-to-end test walking a real respondent's path; forty-three expression
|
|
94
|
+
// fixtures never paired a missing answer with an empty-string literal.
|
|
95
|
+
//
|
|
96
|
+
// This is the seam between "absent renders as empty text" (right, and what
|
|
97
|
+
// keeps `contains "null"` from matching nothing) and "absent IS empty text"
|
|
98
|
+
// (wrong, and a behaviour change).
|
|
99
|
+
case 'eq': {
|
|
100
|
+
const aAbsent = a === null || a === undefined;
|
|
101
|
+
const bAbsent = b === null || b === undefined;
|
|
102
|
+
if (aAbsent !== bAbsent)
|
|
103
|
+
return false;
|
|
104
|
+
if (aAbsent && bAbsent)
|
|
105
|
+
return true;
|
|
106
|
+
return numeric(a, b) ? Number(a) === Number(b) : str(a) === str(b);
|
|
107
|
+
}
|
|
108
|
+
case 'neq': {
|
|
109
|
+
const e = compareValues(a, 'eq', b);
|
|
110
|
+
return e === null ? null : !e;
|
|
111
|
+
}
|
|
112
|
+
case 'gt': return numeric(a, b) && Number(a) > Number(b);
|
|
113
|
+
case 'gte': return numeric(a, b) && Number(a) >= Number(b);
|
|
114
|
+
case 'lt': return numeric(a, b) && Number(a) < Number(b);
|
|
115
|
+
case 'lte': return numeric(a, b) && Number(a) <= Number(b);
|
|
116
|
+
case 'in': return (Array.isArray(b) ? b : str(b).split(',').map((x) => x.trim()))
|
|
117
|
+
.some((v) => str(v) === str(a));
|
|
118
|
+
case 'not_in': {
|
|
119
|
+
const i = compareValues(a, 'in', b);
|
|
120
|
+
return i === null ? null : !i;
|
|
121
|
+
}
|
|
122
|
+
case 'contains': return str(a).toLowerCase().includes(str(b).toLowerCase());
|
|
123
|
+
case 'not_contains': {
|
|
124
|
+
const c = compareValues(a, 'contains', b);
|
|
125
|
+
return c === null ? null : !c;
|
|
126
|
+
}
|
|
127
|
+
case 'starts_with': return str(a).toLowerCase().startsWith(str(b).toLowerCase());
|
|
128
|
+
case 'ends_with': return str(a).toLowerCase().endsWith(str(b).toLowerCase());
|
|
129
|
+
case 'exists': return !empty(a);
|
|
130
|
+
case 'not_exists': return empty(a);
|
|
131
|
+
default: return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* A null tree means "no condition", which every renderer treats as SHOW.
|
|
136
|
+
* @returns true, false, or null when the tree could not be executed.
|
|
137
|
+
*/
|
|
138
|
+
function evaluateTree(node, ctx, depth = 0) {
|
|
139
|
+
if (depth > exports.MAX_TREE_DEPTH)
|
|
140
|
+
return null;
|
|
141
|
+
if (node == null)
|
|
142
|
+
return true;
|
|
143
|
+
if (typeof node !== 'object')
|
|
144
|
+
return null;
|
|
145
|
+
const c = {
|
|
146
|
+
answers: (ctx && ctx.answers) || {},
|
|
147
|
+
variables: (ctx && ctx.variables) || {},
|
|
148
|
+
};
|
|
149
|
+
const n = node;
|
|
150
|
+
if (n.logic) {
|
|
151
|
+
const kids = (n.of || []).map((k) => evaluateTree(k, ctx, depth + 1));
|
|
152
|
+
if (kids.some((k) => k === null))
|
|
153
|
+
return null;
|
|
154
|
+
const vals = kids;
|
|
155
|
+
if (n.logic === 'all')
|
|
156
|
+
return vals.every(Boolean);
|
|
157
|
+
if (n.logic === 'any')
|
|
158
|
+
return vals.some(Boolean);
|
|
159
|
+
if (n.logic === 'not')
|
|
160
|
+
return !vals[0];
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
if (typeof n.op !== 'string')
|
|
164
|
+
return null;
|
|
165
|
+
return compareValues(operand(n.left, c), n.op, operand(n.right, c));
|
|
166
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,10 +11,15 @@
|
|
|
11
11
|
* fails on any disagreement.
|
|
12
12
|
*/
|
|
13
13
|
export { evaluateExpression, evaluateCondition, isParseable } from './expressionEval';
|
|
14
|
+
export { evaluateTree, compareValues, MAX_TREE_DEPTH } from './evaluateTree';
|
|
15
|
+
export type { ConditionNode, Operand, TreeContext } from './evaluateTree';
|
|
14
16
|
export { resolveVariable } from './resolveVariable';
|
|
15
17
|
export { splitIntoPages } from './pageFlow';
|
|
16
|
-
export { validateAnswer, impliedChoices, impliedActions } from './validate';
|
|
17
|
-
export type { ValidatableQuestion } from './validate';
|
|
18
|
+
export { validateAnswer, impliedChoices, impliedActions, impliedStepActions, impliedEqualProminence, safeActionHref, safeImageSrc, isDeferChoice, canSwipeToDismiss, autoDismissMs, deadlineCountdown, formatCountdown, outcomeFor, voiceUsable, rewardToShow, cardClickTarget, cardHoverEffect, resolveAction } from './validate';
|
|
19
|
+
export type { ValidatableQuestion, OutcomeKind, OutcomeInput, ActionKind, RawAction, ResolvedAction } from './validate';
|
|
20
|
+
export { validateAnswerCode } from './validate';
|
|
21
|
+
export { wordMessage, EN_MESSAGES, MESSAGE_CODES } from './messages';
|
|
22
|
+
export type { ValidationMessage } from './messages';
|
|
18
23
|
export type { Page, PageItem, PageFlowOptions, FlowQuestion, FlowSection } from './pageFlow';
|
|
19
24
|
export { normalizeTranscript, matchBoolean, matchOption, matchOptions, rankOptions, matchNumberInRange, matchDate, distinctiveLabels, voiceSupport, voiceCanHandle, auditVoiceQuestions, voiceDisclaimer, skipNoteText, scaffoldFor, buildPromptText, shortCode, optionLabelOf, optionValueOf, voiceOptionsOf, optionLabelsOf, } from './voice';
|
|
20
25
|
export type { VoiceSupport, VoiceQuestion, VoiceOption, VoiceScaffold } from './voice';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/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,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACtF,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC7E,YAAY,EAAE,aAAa,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,cAAc,EAAE,YAAY,EAAE,aAAa,EAAE,iBAAiB,EAAE,aAAa,EAAE,iBAAiB,EAAE,eAAe,EAAE,UAAU,EAAE,WAAW,EAAE,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACnU,YAAY,EAAE,mBAAmB,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACxH,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AACrE,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,YAAY,EAAE,IAAI,EAAE,QAAQ,EAAE,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC7F,OAAO,EACL,mBAAmB,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EACzE,kBAAkB,EAAE,SAAS,EAAE,iBAAiB,EAChD,YAAY,EAAE,cAAc,EAAE,mBAAmB,EACjD,eAAe,EAAE,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,SAAS,EACtE,aAAa,EAAE,aAAa,EAAE,cAAc,EAAE,cAAc,GAC7D,MAAM,SAAS,CAAC;AACjB,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACvF,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.optionLabelsOf = exports.voiceOptionsOf = exports.optionValueOf = exports.optionLabelOf = exports.shortCode = exports.buildPromptText = exports.scaffoldFor = exports.skipNoteText = exports.voiceDisclaimer = exports.auditVoiceQuestions = exports.voiceCanHandle = exports.voiceSupport = exports.distinctiveLabels = exports.matchDate = exports.matchNumberInRange = exports.rankOptions = exports.matchOptions = exports.matchOption = exports.matchBoolean = exports.normalizeTranscript = exports.impliedActions = exports.impliedChoices = exports.validateAnswer = exports.splitIntoPages = exports.resolveVariable = exports.isParseable = exports.evaluateCondition = exports.evaluateExpression = void 0;
|
|
3
|
+
exports.optionLabelsOf = exports.voiceOptionsOf = exports.optionValueOf = exports.optionLabelOf = exports.shortCode = exports.buildPromptText = exports.scaffoldFor = exports.skipNoteText = exports.voiceDisclaimer = exports.auditVoiceQuestions = exports.voiceCanHandle = exports.voiceSupport = exports.distinctiveLabels = exports.matchDate = exports.matchNumberInRange = exports.rankOptions = exports.matchOptions = exports.matchOption = exports.matchBoolean = exports.normalizeTranscript = exports.MESSAGE_CODES = exports.EN_MESSAGES = exports.wordMessage = exports.validateAnswerCode = exports.resolveAction = exports.cardHoverEffect = exports.cardClickTarget = exports.rewardToShow = exports.voiceUsable = exports.outcomeFor = exports.formatCountdown = exports.deadlineCountdown = exports.autoDismissMs = exports.canSwipeToDismiss = exports.isDeferChoice = exports.safeImageSrc = exports.safeActionHref = exports.impliedEqualProminence = exports.impliedStepActions = exports.impliedActions = exports.impliedChoices = exports.validateAnswer = exports.splitIntoPages = exports.resolveVariable = exports.MAX_TREE_DEPTH = exports.compareValues = exports.evaluateTree = exports.isParseable = exports.evaluateCondition = exports.evaluateExpression = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* @cxpinsight/survey-spec
|
|
6
6
|
*
|
|
@@ -17,6 +17,10 @@ var expressionEval_1 = require("./expressionEval");
|
|
|
17
17
|
Object.defineProperty(exports, "evaluateExpression", { enumerable: true, get: function () { return expressionEval_1.evaluateExpression; } });
|
|
18
18
|
Object.defineProperty(exports, "evaluateCondition", { enumerable: true, get: function () { return expressionEval_1.evaluateCondition; } });
|
|
19
19
|
Object.defineProperty(exports, "isParseable", { enumerable: true, get: function () { return expressionEval_1.isParseable; } });
|
|
20
|
+
var evaluateTree_1 = require("./evaluateTree");
|
|
21
|
+
Object.defineProperty(exports, "evaluateTree", { enumerable: true, get: function () { return evaluateTree_1.evaluateTree; } });
|
|
22
|
+
Object.defineProperty(exports, "compareValues", { enumerable: true, get: function () { return evaluateTree_1.compareValues; } });
|
|
23
|
+
Object.defineProperty(exports, "MAX_TREE_DEPTH", { enumerable: true, get: function () { return evaluateTree_1.MAX_TREE_DEPTH; } });
|
|
20
24
|
var resolveVariable_1 = require("./resolveVariable");
|
|
21
25
|
Object.defineProperty(exports, "resolveVariable", { enumerable: true, get: function () { return resolveVariable_1.resolveVariable; } });
|
|
22
26
|
var pageFlow_1 = require("./pageFlow");
|
|
@@ -25,6 +29,27 @@ var validate_1 = require("./validate");
|
|
|
25
29
|
Object.defineProperty(exports, "validateAnswer", { enumerable: true, get: function () { return validate_1.validateAnswer; } });
|
|
26
30
|
Object.defineProperty(exports, "impliedChoices", { enumerable: true, get: function () { return validate_1.impliedChoices; } });
|
|
27
31
|
Object.defineProperty(exports, "impliedActions", { enumerable: true, get: function () { return validate_1.impliedActions; } });
|
|
32
|
+
Object.defineProperty(exports, "impliedStepActions", { enumerable: true, get: function () { return validate_1.impliedStepActions; } });
|
|
33
|
+
Object.defineProperty(exports, "impliedEqualProminence", { enumerable: true, get: function () { return validate_1.impliedEqualProminence; } });
|
|
34
|
+
Object.defineProperty(exports, "safeActionHref", { enumerable: true, get: function () { return validate_1.safeActionHref; } });
|
|
35
|
+
Object.defineProperty(exports, "safeImageSrc", { enumerable: true, get: function () { return validate_1.safeImageSrc; } });
|
|
36
|
+
Object.defineProperty(exports, "isDeferChoice", { enumerable: true, get: function () { return validate_1.isDeferChoice; } });
|
|
37
|
+
Object.defineProperty(exports, "canSwipeToDismiss", { enumerable: true, get: function () { return validate_1.canSwipeToDismiss; } });
|
|
38
|
+
Object.defineProperty(exports, "autoDismissMs", { enumerable: true, get: function () { return validate_1.autoDismissMs; } });
|
|
39
|
+
Object.defineProperty(exports, "deadlineCountdown", { enumerable: true, get: function () { return validate_1.deadlineCountdown; } });
|
|
40
|
+
Object.defineProperty(exports, "formatCountdown", { enumerable: true, get: function () { return validate_1.formatCountdown; } });
|
|
41
|
+
Object.defineProperty(exports, "outcomeFor", { enumerable: true, get: function () { return validate_1.outcomeFor; } });
|
|
42
|
+
Object.defineProperty(exports, "voiceUsable", { enumerable: true, get: function () { return validate_1.voiceUsable; } });
|
|
43
|
+
Object.defineProperty(exports, "rewardToShow", { enumerable: true, get: function () { return validate_1.rewardToShow; } });
|
|
44
|
+
Object.defineProperty(exports, "cardClickTarget", { enumerable: true, get: function () { return validate_1.cardClickTarget; } });
|
|
45
|
+
Object.defineProperty(exports, "cardHoverEffect", { enumerable: true, get: function () { return validate_1.cardHoverEffect; } });
|
|
46
|
+
Object.defineProperty(exports, "resolveAction", { enumerable: true, get: function () { return validate_1.resolveAction; } });
|
|
47
|
+
var validate_2 = require("./validate");
|
|
48
|
+
Object.defineProperty(exports, "validateAnswerCode", { enumerable: true, get: function () { return validate_2.validateAnswerCode; } });
|
|
49
|
+
var messages_1 = require("./messages");
|
|
50
|
+
Object.defineProperty(exports, "wordMessage", { enumerable: true, get: function () { return messages_1.wordMessage; } });
|
|
51
|
+
Object.defineProperty(exports, "EN_MESSAGES", { enumerable: true, get: function () { return messages_1.EN_MESSAGES; } });
|
|
52
|
+
Object.defineProperty(exports, "MESSAGE_CODES", { enumerable: true, get: function () { return messages_1.MESSAGE_CODES; } });
|
|
28
53
|
var voice_1 = require("./voice");
|
|
29
54
|
Object.defineProperty(exports, "normalizeTranscript", { enumerable: true, get: function () { return voice_1.normalizeTranscript; } });
|
|
30
55
|
Object.defineProperty(exports, "matchBoolean", { enumerable: true, get: function () { return voice_1.matchBoolean; } });
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation messages, as codes and English wording.
|
|
3
|
+
*
|
|
4
|
+
* The validator used to build its sentences inline —
|
|
5
|
+
* `` `Please keep this under ${question.maxLength} characters` `` — which welded
|
|
6
|
+
* English into the rule engine. Three consequences, all of them costs someone
|
|
7
|
+
* eventually pays:
|
|
8
|
+
*
|
|
9
|
+
* • A message cannot be translated. Arabic is on the roadmap and every one of
|
|
10
|
+
* these sentences would have had to be intercepted by string matching.
|
|
11
|
+
* • A port to Swift or Kotlin re-implements the copy, or drifts from it. The
|
|
12
|
+
* contract's whole point is that the part which must agree exactly is small,
|
|
13
|
+
* and prose is not that part.
|
|
14
|
+
* • A product change to the wording is a change to the shared spec, a publish,
|
|
15
|
+
* and a release of three renderers.
|
|
16
|
+
*
|
|
17
|
+
* So the rules return `{ code, params }` and the wording lives here. English is
|
|
18
|
+
* the default so nothing changes for a caller that does not care; a renderer
|
|
19
|
+
* that does calls `validateAnswerCode` and words it itself.
|
|
20
|
+
*/
|
|
21
|
+
export interface ValidationMessage {
|
|
22
|
+
code: string;
|
|
23
|
+
params: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
/** English wording, one entry per code. The only prose in the validator. */
|
|
26
|
+
export declare const EN_MESSAGES: Record<string, (p: Record<string, unknown>) => string>;
|
|
27
|
+
/**
|
|
28
|
+
* Word a message in English.
|
|
29
|
+
*
|
|
30
|
+
* An unknown code returns the code itself rather than empty or a throw: a
|
|
31
|
+
* respondent seeing `max_length` is confusing, but a respondent seeing NOTHING
|
|
32
|
+
* where a validation error should be is worse — they cannot submit and cannot
|
|
33
|
+
* tell why.
|
|
34
|
+
*/
|
|
35
|
+
export declare function wordMessage(message: ValidationMessage | null): string | null;
|
|
36
|
+
/** Every code the validator can return, for a translator to work from. */
|
|
37
|
+
export declare const MESSAGE_CODES: string[];
|
|
38
|
+
//# sourceMappingURL=messages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../src/messages.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,4EAA4E;AAC5E,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CA4B9E,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAI5E;AAED,0EAA0E;AAC1E,eAAO,MAAM,aAAa,UAA2B,CAAC"}
|
package/dist/messages.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Validation messages, as codes and English wording.
|
|
4
|
+
*
|
|
5
|
+
* The validator used to build its sentences inline —
|
|
6
|
+
* `` `Please keep this under ${question.maxLength} characters` `` — which welded
|
|
7
|
+
* English into the rule engine. Three consequences, all of them costs someone
|
|
8
|
+
* eventually pays:
|
|
9
|
+
*
|
|
10
|
+
* • A message cannot be translated. Arabic is on the roadmap and every one of
|
|
11
|
+
* these sentences would have had to be intercepted by string matching.
|
|
12
|
+
* • A port to Swift or Kotlin re-implements the copy, or drifts from it. The
|
|
13
|
+
* contract's whole point is that the part which must agree exactly is small,
|
|
14
|
+
* and prose is not that part.
|
|
15
|
+
* • A product change to the wording is a change to the shared spec, a publish,
|
|
16
|
+
* and a release of three renderers.
|
|
17
|
+
*
|
|
18
|
+
* So the rules return `{ code, params }` and the wording lives here. English is
|
|
19
|
+
* the default so nothing changes for a caller that does not care; a renderer
|
|
20
|
+
* that does calls `validateAnswerCode` and words it itself.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.MESSAGE_CODES = exports.EN_MESSAGES = void 0;
|
|
24
|
+
exports.wordMessage = wordMessage;
|
|
25
|
+
/** English wording, one entry per code. The only prose in the validator. */
|
|
26
|
+
exports.EN_MESSAGES = {
|
|
27
|
+
required: () => 'This field is required',
|
|
28
|
+
choose_option: () => 'Please choose an option',
|
|
29
|
+
format_email: () => 'Please enter a valid email address (e.g., you@example.com)',
|
|
30
|
+
format_url: () => 'Please enter a full URL, including https://',
|
|
31
|
+
format_number: () => 'Please enter a number',
|
|
32
|
+
// Deliberately permissive upstream; the message stays vague to match.
|
|
33
|
+
format_tel: () => 'Please enter a valid phone number',
|
|
34
|
+
max_length: (p) => `Please keep this under ${p.max} characters`,
|
|
35
|
+
min_length: (p) => `Please enter at least ${p.min} characters`,
|
|
36
|
+
min_selected: (p) => `Please select at least ${p.min}`,
|
|
37
|
+
max_selected: (p) => `Please select at most ${p.max}`,
|
|
38
|
+
min_value: (p) => `Must be at least ${p.min}`,
|
|
39
|
+
max_value: (p) => `Must be at most ${p.max}`,
|
|
40
|
+
// spec §8 author-declared rules[]. Worded differently from the built-in
|
|
41
|
+
// checks above because the author chose to add them explicitly, and the
|
|
42
|
+
// shorter phrasing is what shipped.
|
|
43
|
+
rule_min_length: (p) => `At least ${p.min} characters`,
|
|
44
|
+
rule_max_length: (p) => `At most ${p.max} characters`,
|
|
45
|
+
rule_email: () => 'Please enter a valid email address',
|
|
46
|
+
rule_url: () => 'Please enter a valid URL',
|
|
47
|
+
rule_pattern: () => "Format doesn't match",
|
|
48
|
+
// The author's own wording, in whatever language they typed it. Passed
|
|
49
|
+
// through untouched — translating an author's sentence is not this table's
|
|
50
|
+
// job, and guessing at its language would be worse than leaving it.
|
|
51
|
+
author_message: (p) => String(p.text ?? ''),
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Word a message in English.
|
|
55
|
+
*
|
|
56
|
+
* An unknown code returns the code itself rather than empty or a throw: a
|
|
57
|
+
* respondent seeing `max_length` is confusing, but a respondent seeing NOTHING
|
|
58
|
+
* where a validation error should be is worse — they cannot submit and cannot
|
|
59
|
+
* tell why.
|
|
60
|
+
*/
|
|
61
|
+
function wordMessage(message) {
|
|
62
|
+
if (!message)
|
|
63
|
+
return null;
|
|
64
|
+
const wording = exports.EN_MESSAGES[message.code];
|
|
65
|
+
return wording ? wording(message.params || {}) : message.code;
|
|
66
|
+
}
|
|
67
|
+
/** Every code the validator can return, for a translator to work from. */
|
|
68
|
+
exports.MESSAGE_CODES = Object.keys(exports.EN_MESSAGES);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolveVariable.d.ts","sourceRoot":"","sources":["../src/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,
|
|
1
|
+
{"version":3,"file":"resolveVariable.d.ts","sourceRoot":"","sources":["../src/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,CAsCT"}
|
package/dist/resolveVariable.js
CHANGED
|
@@ -29,6 +29,10 @@ function resolveVariable(variables, name, suffix) {
|
|
|
29
29
|
}
|
|
30
30
|
else {
|
|
31
31
|
// 2. Walk the dotted path into nested namespaces.
|
|
32
|
+
// OWN properties only. A plain `cursor[seg]` walks the prototype chain,
|
|
33
|
+
// so `{{constructor}}` resolved to the Object function and rendered
|
|
34
|
+
// "function Object() { [native code] }" to the respondent — the same
|
|
35
|
+
// class of leak as the namespace object above, from the same syntax.
|
|
32
36
|
const segments = full.split('.');
|
|
33
37
|
let cursor = variables;
|
|
34
38
|
for (const seg of segments) {
|
|
@@ -36,6 +40,10 @@ function resolveVariable(variables, name, suffix) {
|
|
|
36
40
|
cursor = undefined;
|
|
37
41
|
break;
|
|
38
42
|
}
|
|
43
|
+
if (!Object.prototype.hasOwnProperty.call(cursor, seg)) {
|
|
44
|
+
cursor = undefined;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
39
47
|
cursor = cursor[seg];
|
|
40
48
|
}
|
|
41
49
|
value = cursor;
|
|
@@ -47,5 +55,10 @@ function resolveVariable(variables, name, suffix) {
|
|
|
47
55
|
// Arrays are values (a multi-select answer), so they pass.
|
|
48
56
|
if (value !== null && typeof value === 'object' && !Array.isArray(value))
|
|
49
57
|
return undefined;
|
|
58
|
+
// 4. Nor is a function. `typeof fn` is 'function', not 'object', so the check
|
|
59
|
+
// above lets one through, and String(fn) prints its source. Nothing in a
|
|
60
|
+
// variable bag should ever be callable, so this only fires on a leak.
|
|
61
|
+
if (typeof value === 'function')
|
|
62
|
+
return undefined;
|
|
50
63
|
return value;
|
|
51
64
|
}
|