xlsform2lstsv 0.2.2 → 0.3.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/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Convert XLSForm surveys to LimeSurvey TSV format.
|
|
|
35
35
|
- its a complex task to ensure the transpiler covers everything and we currently cannot guarantee error free/complete transpiling
|
|
36
36
|
|
|
37
37
|
- constraint_message ❌
|
|
38
|
-
- XLSForms Calculation
|
|
38
|
+
- XLSForms Calculation ✅ (`calculate` type → LimeSurvey Equation question `*`; `${var}` references in labels/hints converted to EM `{var}` syntax)
|
|
39
39
|
- XLSForms Trigger ❌
|
|
40
40
|
- Repeats ❌
|
|
41
41
|
- LimeSurvey Assessments ❌
|
|
@@ -38,7 +38,7 @@ function sanitizeName(name) {
|
|
|
38
38
|
* @returns The transpiled LimeSurvey expression string
|
|
39
39
|
* @throws Error if an unsupported node structure is encountered
|
|
40
40
|
*/
|
|
41
|
-
function transpile(node,
|
|
41
|
+
function transpile(node, ctx) {
|
|
42
42
|
if (!node)
|
|
43
43
|
return '';
|
|
44
44
|
// https://getodk.github.io/xforms-spec/#xpath-functions
|
|
@@ -46,29 +46,30 @@ function transpile(node, lookupAnswerCode) {
|
|
|
46
46
|
if (node.id) {
|
|
47
47
|
switch (node.id) {
|
|
48
48
|
case 'count':
|
|
49
|
-
return `count(${node.args?.map(arg => transpile(arg,
|
|
49
|
+
return `count(${node.args?.map(arg => transpile(arg, ctx)).join(', ') || ''})`;
|
|
50
50
|
case 'concat':
|
|
51
|
-
return node.args?.map(arg => transpile(arg,
|
|
51
|
+
return node.args?.map(arg => transpile(arg, ctx)).join(' + ') || '';
|
|
52
52
|
case 'regex':
|
|
53
|
-
return `regexMatch(${node.args?.map(arg => transpile(arg,
|
|
53
|
+
return `regexMatch(${node.args?.map(arg => transpile(arg, ctx)).join(', ') || ''})`;
|
|
54
54
|
case 'contains':
|
|
55
55
|
// Custom handling for contains
|
|
56
56
|
if (node.args?.length === 2) {
|
|
57
|
-
return `contains(${transpile(node.args[0],
|
|
57
|
+
return `contains(${transpile(node.args[0], ctx)}, ${transpile(node.args[1], ctx)})`;
|
|
58
58
|
}
|
|
59
59
|
break;
|
|
60
60
|
case 'selected':
|
|
61
|
-
// Handle selected(${field}, 'value')
|
|
61
|
+
// Handle selected(${field}, 'value')
|
|
62
62
|
if (node.args?.length === 2) {
|
|
63
63
|
const fieldArg = node.args[0];
|
|
64
64
|
const valueArg = node.args[1];
|
|
65
|
-
const fieldName = transpile(fieldArg,
|
|
66
|
-
let value = transpile(valueArg,
|
|
67
|
-
// Remove any existing quotes
|
|
65
|
+
const fieldName = transpile(fieldArg, ctx);
|
|
66
|
+
let value = transpile(valueArg, ctx);
|
|
67
|
+
// Remove any existing quotes
|
|
68
68
|
value = value.replace(/^['"]|['"]$/g, "");
|
|
69
69
|
const sanitizedField = sanitizeName(fieldName);
|
|
70
|
-
if (
|
|
71
|
-
|
|
70
|
+
// Use buildSelectedExpr if available (handles select_one vs select_multiple)
|
|
71
|
+
if (ctx?.buildSelectedExpr) {
|
|
72
|
+
return ctx.buildSelectedExpr(sanitizedField, value);
|
|
72
73
|
}
|
|
73
74
|
return `(${sanitizedField}=="${value}")`;
|
|
74
75
|
}
|
|
@@ -76,66 +77,73 @@ function transpile(node, lookupAnswerCode) {
|
|
|
76
77
|
case 'string':
|
|
77
78
|
// string() function - just return the argument
|
|
78
79
|
if (node.args?.length === 1) {
|
|
79
|
-
return transpile(node.args[0],
|
|
80
|
+
return transpile(node.args[0], ctx);
|
|
80
81
|
}
|
|
81
82
|
break;
|
|
82
83
|
case 'number':
|
|
83
84
|
// number() function - just return the argument
|
|
84
85
|
if (node.args?.length === 1) {
|
|
85
|
-
return transpile(node.args[0],
|
|
86
|
+
return transpile(node.args[0], ctx);
|
|
86
87
|
}
|
|
87
88
|
break;
|
|
88
89
|
case 'floor':
|
|
89
90
|
if (node.args?.length === 1) {
|
|
90
|
-
return `floor(${transpile(node.args[0],
|
|
91
|
+
return `floor(${transpile(node.args[0], ctx)})`;
|
|
91
92
|
}
|
|
92
93
|
break;
|
|
93
94
|
case 'ceiling':
|
|
94
95
|
if (node.args?.length === 1) {
|
|
95
|
-
return `ceil(${transpile(node.args[0],
|
|
96
|
+
return `ceil(${transpile(node.args[0], ctx)})`;
|
|
96
97
|
}
|
|
97
98
|
break;
|
|
98
99
|
case 'round':
|
|
99
100
|
if (node.args?.length === 1) {
|
|
100
|
-
return `round(${transpile(node.args[0],
|
|
101
|
+
return `round(${transpile(node.args[0], ctx)})`;
|
|
101
102
|
}
|
|
102
103
|
break;
|
|
103
104
|
case 'sum':
|
|
104
105
|
if (node.args?.length === 1) {
|
|
105
|
-
return `sum(${transpile(node.args[0],
|
|
106
|
+
return `sum(${transpile(node.args[0], ctx)})`;
|
|
106
107
|
}
|
|
107
108
|
break;
|
|
108
109
|
case 'substring':
|
|
109
110
|
if (node.args && node.args.length >= 2) {
|
|
110
|
-
const stringArg = transpile(node.args[0],
|
|
111
|
-
const startArg = transpile(node.args[1],
|
|
112
|
-
const lengthArg = node.args.length > 2 ? transpile(node.args[2],
|
|
111
|
+
const stringArg = transpile(node.args[0], ctx);
|
|
112
|
+
const startArg = transpile(node.args[1], ctx);
|
|
113
|
+
const lengthArg = node.args.length > 2 ? transpile(node.args[2], ctx) : '';
|
|
113
114
|
return `substr(${stringArg}, ${startArg}${lengthArg ? ', ' + lengthArg : ''})`;
|
|
114
115
|
}
|
|
115
116
|
break;
|
|
116
117
|
case 'string-length':
|
|
117
118
|
if (node.args?.length === 1) {
|
|
118
|
-
return `strlen(${transpile(node.args[0],
|
|
119
|
+
return `strlen(${transpile(node.args[0], ctx)})`;
|
|
119
120
|
}
|
|
120
121
|
break;
|
|
121
122
|
case 'starts-with':
|
|
122
123
|
if (node.args?.length === 2) {
|
|
123
|
-
return `startsWith(${transpile(node.args[0],
|
|
124
|
+
return `startsWith(${transpile(node.args[0], ctx)}, ${transpile(node.args[1], ctx)})`;
|
|
124
125
|
}
|
|
125
126
|
break;
|
|
126
127
|
case 'ends-with':
|
|
127
128
|
if (node.args?.length === 2) {
|
|
128
|
-
return `endsWith(${transpile(node.args[0],
|
|
129
|
+
return `endsWith(${transpile(node.args[0], ctx)}, ${transpile(node.args[1], ctx)})`;
|
|
130
|
+
}
|
|
131
|
+
break;
|
|
132
|
+
case 'normalize-space':
|
|
133
|
+
if (node.args?.length === 1) {
|
|
134
|
+
return `trim(${transpile(node.args[0], ctx)})`;
|
|
129
135
|
}
|
|
130
136
|
break;
|
|
131
137
|
case 'not':
|
|
132
138
|
if (node.args?.length === 1) {
|
|
133
|
-
return `!(${transpile(node.args[0],
|
|
139
|
+
return `!(${transpile(node.args[0], ctx)})`;
|
|
134
140
|
}
|
|
135
141
|
break;
|
|
136
142
|
case 'if':
|
|
143
|
+
// Use if() function instead of ternary (? :) because EM's ternary parser
|
|
144
|
+
// can misinterpret colons inside string literals (e.g. '2026-03-CHW: Chancenwerk')
|
|
137
145
|
if (node.args?.length === 3) {
|
|
138
|
-
return `(${transpile(node.args[0],
|
|
146
|
+
return `if(${transpile(node.args[0], ctx)}, ${transpile(node.args[1], ctx)}, ${transpile(node.args[2], ctx)})`;
|
|
139
147
|
}
|
|
140
148
|
break;
|
|
141
149
|
case 'today':
|
|
@@ -146,15 +154,16 @@ function transpile(node, lookupAnswerCode) {
|
|
|
146
154
|
throw new Error(`Unsupported function: ${node.id}`);
|
|
147
155
|
}
|
|
148
156
|
}
|
|
149
|
-
// https://getodk.github.io/xforms-spec/#xpath-operators
|
|
157
|
+
// https://getodk.github.io/xforms-spec/#xpath-operators
|
|
150
158
|
// to https://www.limesurvey.org/manual/ExpressionScript_-_Presentation (see syntax)
|
|
151
159
|
if (node.type) {
|
|
160
|
+
const lookupAnswerCode = ctx?.lookupAnswerCode;
|
|
152
161
|
switch (node.type) {
|
|
153
162
|
// Comparison operators
|
|
154
163
|
case '<=':
|
|
155
|
-
return `${transpile(node.left,
|
|
164
|
+
return `${transpile(node.left, ctx)} <= ${transpile(node.right, ctx)}`;
|
|
156
165
|
case '>=':
|
|
157
|
-
return `${transpile(node.left,
|
|
166
|
+
return `${transpile(node.left, ctx)} >= ${transpile(node.right, ctx)}`;
|
|
158
167
|
case '=':
|
|
159
168
|
case '==': {
|
|
160
169
|
const leftNode = node.left;
|
|
@@ -164,10 +173,10 @@ function transpile(node, lookupAnswerCode) {
|
|
|
164
173
|
const rawValue = rightNode.value;
|
|
165
174
|
const rewritten = lookupAnswerCode(fieldName, rawValue);
|
|
166
175
|
if (rewritten !== rawValue) {
|
|
167
|
-
return `${fieldName} ==
|
|
176
|
+
return `${fieldName} == '${rewritten}'`;
|
|
168
177
|
}
|
|
169
178
|
}
|
|
170
|
-
return `${transpile(leftNode,
|
|
179
|
+
return `${transpile(leftNode, ctx)} == ${transpile(rightNode, ctx)}`;
|
|
171
180
|
}
|
|
172
181
|
case '!=': {
|
|
173
182
|
const leftNode = node.left;
|
|
@@ -177,31 +186,31 @@ function transpile(node, lookupAnswerCode) {
|
|
|
177
186
|
const rawValue = rightNode.value;
|
|
178
187
|
const rewritten = lookupAnswerCode(fieldName, rawValue);
|
|
179
188
|
if (rewritten !== rawValue) {
|
|
180
|
-
return `${fieldName} !=
|
|
189
|
+
return `${fieldName} != '${rewritten}'`;
|
|
181
190
|
}
|
|
182
191
|
}
|
|
183
|
-
return `${transpile(leftNode,
|
|
192
|
+
return `${transpile(leftNode, ctx)} != ${transpile(rightNode, ctx)}`;
|
|
184
193
|
}
|
|
185
194
|
case '<':
|
|
186
|
-
return `${transpile(node.left,
|
|
195
|
+
return `${transpile(node.left, ctx)} < ${transpile(node.right, ctx)}`;
|
|
187
196
|
case '>':
|
|
188
|
-
return `${transpile(node.left,
|
|
197
|
+
return `${transpile(node.left, ctx)} > ${transpile(node.right, ctx)}`;
|
|
189
198
|
// Arithmetic operators
|
|
190
199
|
case '+':
|
|
191
|
-
return `${transpile(node.left,
|
|
200
|
+
return `${transpile(node.left, ctx)} + ${transpile(node.right, ctx)}`;
|
|
192
201
|
case '-':
|
|
193
|
-
return `${transpile(node.left,
|
|
202
|
+
return `${transpile(node.left, ctx)} - ${transpile(node.right, ctx)}`;
|
|
194
203
|
case '*':
|
|
195
|
-
return `${transpile(node.left,
|
|
204
|
+
return `${transpile(node.left, ctx)} * ${transpile(node.right, ctx)}`;
|
|
196
205
|
case 'div':
|
|
197
|
-
return `${transpile(node.left,
|
|
206
|
+
return `${transpile(node.left, ctx)} / ${transpile(node.right, ctx)}`;
|
|
198
207
|
case 'mod':
|
|
199
|
-
return `${transpile(node.left,
|
|
208
|
+
return `${transpile(node.left, ctx)} % ${transpile(node.right, ctx)}`;
|
|
200
209
|
// Logical operators
|
|
201
210
|
case 'and':
|
|
202
|
-
return `${transpile(node.left,
|
|
211
|
+
return `${transpile(node.left, ctx)} and ${transpile(node.right, ctx)}`;
|
|
203
212
|
case 'or':
|
|
204
|
-
return `${transpile(node.left,
|
|
213
|
+
return `${transpile(node.left, ctx)} or ${transpile(node.right, ctx)}`;
|
|
205
214
|
// Unsupported operators
|
|
206
215
|
case '|':
|
|
207
216
|
case '/':
|
|
@@ -251,7 +260,7 @@ function transpile(node, lookupAnswerCode) {
|
|
|
251
260
|
* @param xpathExpr - The XPath expression to convert
|
|
252
261
|
* @returns LimeSurvey Expression Manager syntax, or null if conversion fails
|
|
253
262
|
*/
|
|
254
|
-
export async function xpathToLimeSurvey(xpathExpr,
|
|
263
|
+
export async function xpathToLimeSurvey(xpathExpr, ctx) {
|
|
255
264
|
if (!xpathExpr || xpathExpr.trim() === '') {
|
|
256
265
|
return '1'; // Default relevance expression
|
|
257
266
|
}
|
|
@@ -273,7 +282,7 @@ export async function xpathToLimeSurvey(xpathExpr, lookupAnswerCode) {
|
|
|
273
282
|
throw new Error('js-xpath module does not export parse function');
|
|
274
283
|
}
|
|
275
284
|
const parsed = jxpath.parse(processedExpr);
|
|
276
|
-
return transpile(parsed,
|
|
285
|
+
return transpile(parsed, ctx);
|
|
277
286
|
}
|
|
278
287
|
catch (error) {
|
|
279
288
|
console.error(`Transpilation error: ${error.message}`);
|
|
@@ -417,14 +426,14 @@ function parseRegexMatchArguments(argsString) {
|
|
|
417
426
|
* @param xpath - The XPath relevance expression
|
|
418
427
|
* @returns LimeSurvey Expression Manager syntax
|
|
419
428
|
*/
|
|
420
|
-
export async function convertRelevance(xpathExpr,
|
|
429
|
+
export async function convertRelevance(xpathExpr, ctx) {
|
|
421
430
|
if (!xpathExpr)
|
|
422
431
|
return '1';
|
|
423
432
|
// Preprocess: normalize operators to lowercase for jsxpath compatibility
|
|
424
433
|
let normalizedXPath = xpathExpr
|
|
425
434
|
.replace(/\bAND\b/gi, 'and')
|
|
426
435
|
.replace(/\bOR\b/gi, 'or');
|
|
427
|
-
const result = await xpathToLimeSurvey(normalizedXPath,
|
|
436
|
+
const result = await xpathToLimeSurvey(normalizedXPath, ctx);
|
|
428
437
|
// Handle edge case: selected() with just {field} (without $)
|
|
429
438
|
if (result && result.includes('selected(')) {
|
|
430
439
|
return result.replace(/selected\s*\(\s*\{(\w+)\}\s*,\s*["']([^'"]+)["']\s*\)/g, (_match, fieldName, value) => {
|
|
@@ -20,11 +20,12 @@ export class TSVGenerator {
|
|
|
20
20
|
'mandatory',
|
|
21
21
|
'other',
|
|
22
22
|
'default',
|
|
23
|
-
'same_default'
|
|
23
|
+
'same_default',
|
|
24
|
+
'hidden'
|
|
24
25
|
];
|
|
25
26
|
const lines = [headers.join('\t')];
|
|
26
27
|
for (const row of this.rows) {
|
|
27
|
-
const values = headers.map((h) => this.escapeForTSV(row[h]
|
|
28
|
+
const values = headers.map((h) => this.escapeForTSV(row[h] ?? ''));
|
|
28
29
|
lines.push(values.join('\t'));
|
|
29
30
|
}
|
|
30
31
|
return lines.join('\n');
|
|
@@ -20,6 +20,7 @@ export const TYPE_MAPPINGS = {
|
|
|
20
20
|
select_multiple: { limeSurveyType: 'M', supportsOther: true, answerClass: 'SQ' },
|
|
21
21
|
// Other types
|
|
22
22
|
note: { limeSurveyType: 'X' },
|
|
23
|
+
calculate: { limeSurveyType: '*' },
|
|
23
24
|
rank: { limeSurveyType: 'R', answerClass: 'A', supportsOther: true },
|
|
24
25
|
};
|
|
25
26
|
export class TypeMapper {
|
package/dist/xlsformConverter.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ConfigManager } from './config/ConfigManager.js';
|
|
2
|
-
import { convertRelevance, convertConstraint } from './converters/xpathTranspiler.js';
|
|
2
|
+
import { convertRelevance, convertConstraint, xpathToLimeSurvey } from './converters/xpathTranspiler.js';
|
|
3
3
|
import { FieldSanitizer } from './processors/FieldSanitizer.js';
|
|
4
4
|
import { TSVGenerator } from './processors/TSVGenerator.js';
|
|
5
5
|
import { TypeMapper } from './processors/TypeMapper.js';
|
|
@@ -7,7 +7,7 @@ import { getBaseLanguage } from './utils/languageUtils.js';
|
|
|
7
7
|
// Metadata types that should be silently skipped (no visual representation)
|
|
8
8
|
const SKIP_TYPES = [
|
|
9
9
|
'start', 'end', 'today', 'deviceid', 'username',
|
|
10
|
-
'
|
|
10
|
+
'hidden', 'audit'
|
|
11
11
|
];
|
|
12
12
|
// Unimplemented XLSForm types that should raise an error
|
|
13
13
|
const UNIMPLEMENTED_TYPES = [
|
|
@@ -51,6 +51,7 @@ export class XLSFormToTSVConverter {
|
|
|
51
51
|
this.groupContentBuffer = [];
|
|
52
52
|
this.answerCodeMap = new Map();
|
|
53
53
|
this.questionToListMap = new Map();
|
|
54
|
+
this.questionBaseTypeMap = new Map();
|
|
54
55
|
}
|
|
55
56
|
/**
|
|
56
57
|
* Get the current configuration
|
|
@@ -201,11 +202,13 @@ export class XLSFormToTSVConverter {
|
|
|
201
202
|
}
|
|
202
203
|
buildQuestionToListMap(surveyData) {
|
|
203
204
|
this.questionToListMap = new Map();
|
|
205
|
+
this.questionBaseTypeMap = new Map();
|
|
204
206
|
for (const row of surveyData) {
|
|
205
207
|
const typeInfo = this.parseType(row.type || '');
|
|
206
208
|
if (typeInfo.listName && row.name) {
|
|
207
209
|
const sanitizedName = this.sanitizeName(row.name.trim());
|
|
208
210
|
this.questionToListMap.set(sanitizedName, typeInfo.listName);
|
|
211
|
+
this.questionBaseTypeMap.set(sanitizedName, typeInfo.base);
|
|
209
212
|
}
|
|
210
213
|
}
|
|
211
214
|
}
|
|
@@ -537,6 +540,21 @@ export class XLSFormToTSVConverter {
|
|
|
537
540
|
sanitizeAnswerCode(code) {
|
|
538
541
|
return this.fieldSanitizer.sanitizeAnswerCode(code);
|
|
539
542
|
}
|
|
543
|
+
/**
|
|
544
|
+
* Convert ${varname} references in text to LimeSurvey EM syntax {sanitizedname}.
|
|
545
|
+
*/
|
|
546
|
+
convertVariableReferences(text) {
|
|
547
|
+
return text.replace(/\$\{([^}]+)\}/g, (_, name) => {
|
|
548
|
+
const sanitized = name.replace(/[_-]/g, '');
|
|
549
|
+
return `{${sanitized}}`;
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Transpile an XLSForm calculation expression to a LimeSurvey EM expression.
|
|
554
|
+
*/
|
|
555
|
+
async convertCalculation(calculation) {
|
|
556
|
+
return await xpathToLimeSurvey(calculation, this.buildTranspilerContext());
|
|
557
|
+
}
|
|
540
558
|
async addGroup(row) {
|
|
541
559
|
// Auto-generate name if missing (matches LimeSurvey behavior)
|
|
542
560
|
const groupName = row.name && row.name.trim() !== ''
|
|
@@ -554,10 +572,10 @@ export class XLSFormToTSVConverter {
|
|
|
554
572
|
this.tsvGenerator.addRow({
|
|
555
573
|
class: 'G',
|
|
556
574
|
'type/scale': groupSeqKey,
|
|
557
|
-
name: groupName,
|
|
575
|
+
name: this.getLanguageSpecificValue(row.label, lang) || groupName,
|
|
558
576
|
relevance: await this.convertRelevance(row.relevant),
|
|
559
|
-
text: this.getLanguageSpecificValue(row.
|
|
560
|
-
help:
|
|
577
|
+
text: this.getLanguageSpecificValue(row.hint, lang) || '',
|
|
578
|
+
help: '',
|
|
561
579
|
language: lang,
|
|
562
580
|
validation: '',
|
|
563
581
|
em_validation_q: "",
|
|
@@ -609,22 +627,40 @@ export class XLSFormToTSVConverter {
|
|
|
609
627
|
}
|
|
610
628
|
// Notes have special handling
|
|
611
629
|
const isNote = xfTypeInfo.base === 'note';
|
|
630
|
+
const isCalculate = xfTypeInfo.base === 'calculate';
|
|
631
|
+
// For calculate type, transpile the calculation expression to EM syntax
|
|
632
|
+
let calculationExpr = '';
|
|
633
|
+
if (isCalculate && row.calculation) {
|
|
634
|
+
calculationExpr = await this.convertCalculation(row.calculation);
|
|
635
|
+
}
|
|
612
636
|
// Add main question for each language
|
|
613
637
|
for (const lang of this.availableLanguages) {
|
|
638
|
+
let text;
|
|
639
|
+
if (isCalculate) {
|
|
640
|
+
// Equation question: the EM expression wrapped in {} IS the question text
|
|
641
|
+
text = `{${calculationExpr}}`;
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
text = this.getLanguageSpecificValue(row.label, lang) || questionName;
|
|
645
|
+
}
|
|
646
|
+
// Convert ${var} references to EM {var} syntax in text and help
|
|
647
|
+
text = this.convertVariableReferences(text);
|
|
648
|
+
const help = this.convertVariableReferences(this.getLanguageSpecificValue(row.hint, lang) || '');
|
|
614
649
|
this.bufferRow({
|
|
615
650
|
class: 'Q',
|
|
616
651
|
'type/scale': isNote ? 'X' : lsType.type,
|
|
617
652
|
name: questionName,
|
|
618
653
|
relevance: await this.convertRelevance(row.relevant),
|
|
619
|
-
text
|
|
620
|
-
help
|
|
654
|
+
text,
|
|
655
|
+
help,
|
|
621
656
|
language: lang,
|
|
622
657
|
validation: "",
|
|
623
|
-
em_validation_q: isNote ? "" : await convertConstraint(row.constraint || ""),
|
|
624
|
-
mandatory: isNote ? '' : (row.required === 'yes' || row.required === 'true' ? 'Y' : ''),
|
|
625
|
-
other: isNote ? '' : (lsType.other ? 'Y' : ''),
|
|
626
|
-
default: isNote ? '' : (row.default || ''),
|
|
627
|
-
same_default: ''
|
|
658
|
+
em_validation_q: (isNote || isCalculate) ? "" : await convertConstraint(row.constraint || ""),
|
|
659
|
+
mandatory: (isNote || isCalculate) ? '' : (row.required === 'yes' || row.required === 'true' ? 'Y' : ''),
|
|
660
|
+
other: (isNote || isCalculate) ? '' : (lsType.other ? 'Y' : ''),
|
|
661
|
+
default: (isNote || isCalculate) ? '' : (row.default || ''),
|
|
662
|
+
same_default: '',
|
|
663
|
+
hidden: isCalculate ? '1' : '',
|
|
628
664
|
});
|
|
629
665
|
}
|
|
630
666
|
// Reset answer sequence for this question
|
|
@@ -784,20 +820,37 @@ export class XLSFormToTSVConverter {
|
|
|
784
820
|
}
|
|
785
821
|
}
|
|
786
822
|
}
|
|
823
|
+
lookupAnswerCode(fieldName, choiceValue) {
|
|
824
|
+
// Truncate to 20 chars to match converter's field sanitization
|
|
825
|
+
// (the transpiler only removes _/- but doesn't truncate)
|
|
826
|
+
const truncated = fieldName.length > 20 ? fieldName.substring(0, 20) : fieldName;
|
|
827
|
+
const listName = this.questionToListMap.get(truncated);
|
|
828
|
+
if (!listName)
|
|
829
|
+
return { code: choiceValue, listName: undefined };
|
|
830
|
+
const codeMap = this.answerCodeMap.get(listName);
|
|
831
|
+
if (!codeMap)
|
|
832
|
+
return { code: choiceValue, listName };
|
|
833
|
+
return { code: codeMap.get(choiceValue) ?? choiceValue, listName };
|
|
834
|
+
}
|
|
835
|
+
buildTranspilerContext() {
|
|
836
|
+
return {
|
|
837
|
+
lookupAnswerCode: (fieldName, choiceValue) => {
|
|
838
|
+
return this.lookupAnswerCode(fieldName, choiceValue).code;
|
|
839
|
+
},
|
|
840
|
+
buildSelectedExpr: (fieldName, choiceValue) => {
|
|
841
|
+
const truncated = fieldName.length > 20 ? fieldName.substring(0, 20) : fieldName;
|
|
842
|
+
const { code } = this.lookupAnswerCode(fieldName, choiceValue);
|
|
843
|
+
const baseType = this.questionBaseTypeMap.get(truncated);
|
|
844
|
+
if (baseType === 'select_multiple') {
|
|
845
|
+
return `(${truncated}_${code}.NAOK == 'Y')`;
|
|
846
|
+
}
|
|
847
|
+
return `(${truncated}.NAOK=='${code}')`;
|
|
848
|
+
},
|
|
849
|
+
};
|
|
850
|
+
}
|
|
787
851
|
async convertRelevance(relevant) {
|
|
788
852
|
if (!relevant)
|
|
789
853
|
return '1';
|
|
790
|
-
return await convertRelevance(relevant, (
|
|
791
|
-
// Truncate to 20 chars to match converter's field sanitization
|
|
792
|
-
// (the transpiler only removes _/- but doesn't truncate)
|
|
793
|
-
const truncated = questionName.length > 20 ? questionName.substring(0, 20) : questionName;
|
|
794
|
-
const listName = this.questionToListMap.get(truncated);
|
|
795
|
-
if (!listName)
|
|
796
|
-
return choiceValue;
|
|
797
|
-
const codeMap = this.answerCodeMap.get(listName);
|
|
798
|
-
if (!codeMap)
|
|
799
|
-
return choiceValue;
|
|
800
|
-
return codeMap.get(choiceValue) ?? choiceValue;
|
|
801
|
-
});
|
|
854
|
+
return await convertRelevance(relevant, this.buildTranspilerContext());
|
|
802
855
|
}
|
|
803
856
|
}
|