scjson 0.3.3 → 0.4.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 +35 -1
- package/dist/comment_promotion.d.ts +105 -0
- package/dist/comment_promotion.js +920 -0
- package/dist/converters.d.ts +74 -3
- package/dist/converters.js +255 -9
- package/dist/harness.d.ts +8 -0
- package/dist/harness.js +96 -0
- package/package.json +20 -5
- package/scjson.schema.json +219 -83
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Name: js-comment-promotion
|
|
3
|
+
*
|
|
4
|
+
* Part of the scjson project.
|
|
5
|
+
* Developed by Softoboros Technology Inc.
|
|
6
|
+
* Licensed under the BSD 1-Clause License.
|
|
7
|
+
*
|
|
8
|
+
* CONV-F (SCXML Comment Promotion) — JavaScript parity.
|
|
9
|
+
*
|
|
10
|
+
* See ``docs/concepts/SCJSON-CONV-00-CONCEPTS.md`` (CONV-F, ~lines 270-388)
|
|
11
|
+
* for the normative contract this module implements and
|
|
12
|
+
* ``py/scjson/comment_promotion.py`` for the canonical Python implementation
|
|
13
|
+
* this module mirrors.
|
|
14
|
+
*
|
|
15
|
+
* Public surface:
|
|
16
|
+
*
|
|
17
|
+
* - ``extractHelpTextFromXml(xmlStr)`` — XML pre-pass. Parses ``xmlStr`` with
|
|
18
|
+
* ``fast-xml-parser`` in ``preserveOrder`` + ``commentPropName`` mode,
|
|
19
|
+
* assigns deterministic addresses to every element, partitions XML comment
|
|
20
|
+
* nodes onto their owning element per CONV-F rules 1-8, and returns the
|
|
21
|
+
* comment-free XML plus an ``address -> [comment_text, ...]`` map.
|
|
22
|
+
* - ``attachHelpTextToModel(model, addressMap)`` — after the existing
|
|
23
|
+
* ``xmlToJson`` pipeline produces a JS object from the comment-free XML,
|
|
24
|
+
* walks the model in the same deterministic order and appends each comment
|
|
25
|
+
* string to the matching element's ``help_text`` array.
|
|
26
|
+
* - ``injectHelpTextCommentsIntoXml(xmlStr, model)`` — post-pass for the
|
|
27
|
+
* SCJSON-to-SCXML direction. Takes the model and the comment-free XML
|
|
28
|
+
* produced by ``jsonToXml`` and inserts ``<!-- ... -->`` nodes immediately
|
|
29
|
+
* before the owning element (or before the ``<scxml>`` root for root-level
|
|
30
|
+
* entries).
|
|
31
|
+
*
|
|
32
|
+
* Addressing
|
|
33
|
+
* ----------
|
|
34
|
+
*
|
|
35
|
+
* Each element gets an address of shape
|
|
36
|
+
* ``[["scxml", 0], ["state", 1], ["transition", 0], ...]`` where each
|
|
37
|
+
* segment is ``[local_tag_name, index_among_same-tag_element_siblings]``.
|
|
38
|
+
* This MUST match the Python tuple shape byte-for-byte (after canonical
|
|
39
|
+
* JSON serialization) so cross-language fixtures align.
|
|
40
|
+
*
|
|
41
|
+
* XML-local element names are used (the parser drops the ``xmlns`` prefix
|
|
42
|
+
* because SCXML elements are not namespace-qualified in serialized form;
|
|
43
|
+
* any future namespaced extensions would be opaque and not promoted).
|
|
44
|
+
*
|
|
45
|
+
* Model-attribute mapping
|
|
46
|
+
* -----------------------
|
|
47
|
+
*
|
|
48
|
+
* The JS model surface renames a few XML tags to avoid JavaScript reserved
|
|
49
|
+
* words:
|
|
50
|
+
*
|
|
51
|
+
* - ``<if>`` -> ``if_value``
|
|
52
|
+
* - ``<raise>`` -> ``raise_value``
|
|
53
|
+
* - ``<else>`` -> ``else_value``
|
|
54
|
+
*
|
|
55
|
+
* Address segments always use the XML local name; resolution into the model
|
|
56
|
+
* goes through ``TAG_TO_ATTR`` below.
|
|
57
|
+
*
|
|
58
|
+
* Singleton vs. array fields
|
|
59
|
+
* --------------------------
|
|
60
|
+
*
|
|
61
|
+
* Most child fields are arrays in the JS model (mirrors xsdata's
|
|
62
|
+
* ``list[Element]`` shape). Two exceptions match the Python model:
|
|
63
|
+
*
|
|
64
|
+
* - ``History.transition`` is a singleton object.
|
|
65
|
+
* - ``Initial.transition`` is a singleton object.
|
|
66
|
+
*
|
|
67
|
+
* Address-shape parity with Python only requires that the *first* sibling at
|
|
68
|
+
* index ``0`` resolves to that object; index > 0 returns ``null``.
|
|
69
|
+
*
|
|
70
|
+
* Script / Data exclusion (rule 5)
|
|
71
|
+
* --------------------------------
|
|
72
|
+
*
|
|
73
|
+
* Comments inside ``<script>`` and ``<data>`` are body content of opaque
|
|
74
|
+
* source. They are not promoted.
|
|
75
|
+
*
|
|
76
|
+
* Extension subtree exclusion (rule 6)
|
|
77
|
+
* ------------------------------------
|
|
78
|
+
*
|
|
79
|
+
* Any element whose local name is not in ``APPLIES_TO_TAGS`` is treated as an
|
|
80
|
+
* opaque extension. Comments inside such subtrees are not promoted.
|
|
81
|
+
*
|
|
82
|
+
* Emission text safety (§340-344)
|
|
83
|
+
* -------------------------------
|
|
84
|
+
*
|
|
85
|
+
* Comment text containing ``--`` is replaced with ``- -``; a body ending in
|
|
86
|
+
* ``-`` gets one trailing space appended; the builder handles XML
|
|
87
|
+
* metacharacters because comment bodies are emitted literally.
|
|
88
|
+
*/
|
|
89
|
+
'use strict';
|
|
90
|
+
const { XMLParser, XMLBuilder } = require('fast-xml-parser');
|
|
91
|
+
/**
|
|
92
|
+
* Local tag names that own a ``help_text`` field on the model side.
|
|
93
|
+
* Mirrors ``APPLIES_TO_TAGS`` in ``py/scjson/comment_promotion.py``.
|
|
94
|
+
*/
|
|
95
|
+
const APPLIES_TO_TAGS = new Set([
|
|
96
|
+
'scxml',
|
|
97
|
+
'state',
|
|
98
|
+
'parallel',
|
|
99
|
+
'final',
|
|
100
|
+
'history',
|
|
101
|
+
'initial',
|
|
102
|
+
'transition',
|
|
103
|
+
'onentry',
|
|
104
|
+
'onexit',
|
|
105
|
+
'invoke',
|
|
106
|
+
'finalize',
|
|
107
|
+
'datamodel',
|
|
108
|
+
'data',
|
|
109
|
+
'donedata',
|
|
110
|
+
'content',
|
|
111
|
+
'param',
|
|
112
|
+
'assign',
|
|
113
|
+
'log',
|
|
114
|
+
'raise',
|
|
115
|
+
'if',
|
|
116
|
+
'elseif',
|
|
117
|
+
'else',
|
|
118
|
+
'foreach',
|
|
119
|
+
'send',
|
|
120
|
+
'cancel',
|
|
121
|
+
'script',
|
|
122
|
+
]);
|
|
123
|
+
/**
|
|
124
|
+
* Tags whose textual content is opaque target-language source. Comments
|
|
125
|
+
* inside these elements remain content and MUST NOT be promoted (CONV-F
|
|
126
|
+
* rule 5).
|
|
127
|
+
*/
|
|
128
|
+
const SOURCE_BODY_TAGS = new Set(['script', 'data']);
|
|
129
|
+
/**
|
|
130
|
+
* XML local-tag -> JS model attribute name. Mirrors xsdata's renames.
|
|
131
|
+
*/
|
|
132
|
+
const TAG_TO_ATTR = {
|
|
133
|
+
if: 'if_value',
|
|
134
|
+
raise: 'raise_value',
|
|
135
|
+
else: 'else_value',
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* Fields in the JS model that hold a *singleton* object rather than an
|
|
139
|
+
* array of objects under XML elements named ``transition``.
|
|
140
|
+
*
|
|
141
|
+
* Used by ``resolveAddress`` so a singleton field can still satisfy an
|
|
142
|
+
* address segment ``["transition", 0]`` even though it is not wrapped in
|
|
143
|
+
* a JS array.
|
|
144
|
+
*/
|
|
145
|
+
const SINGLETON_TRANSITION_PARENTS = new Set(['history', 'initial']);
|
|
146
|
+
/**
|
|
147
|
+
* Return the JS model attribute name for an XML local tag.
|
|
148
|
+
*
|
|
149
|
+
* @param {string} tag - XML local tag name.
|
|
150
|
+
* @returns {string} Attribute name on the JS model object.
|
|
151
|
+
*/
|
|
152
|
+
function modelAttrForTag(tag) {
|
|
153
|
+
return Object.prototype.hasOwnProperty.call(TAG_TO_ATTR, tag)
|
|
154
|
+
? TAG_TO_ATTR[tag]
|
|
155
|
+
: tag;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Canonical key for a tuple-address. Used as a Map key so two addresses
|
|
159
|
+
* with the same shape collide.
|
|
160
|
+
*
|
|
161
|
+
* @param {Array<[string, number]>} address - Tuple address.
|
|
162
|
+
* @returns {string} JSON serialization of the address.
|
|
163
|
+
*/
|
|
164
|
+
function addressKey(address) {
|
|
165
|
+
return JSON.stringify(address);
|
|
166
|
+
}
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Comment text repair (parser side + emitter side share this).
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
/**
|
|
171
|
+
* Repair raw XML comment body text per CONV-F §294-299.
|
|
172
|
+
*
|
|
173
|
+
* Strips a common leading indentation margin from multi-line block comments
|
|
174
|
+
* and trims leading/trailing blank lines. Does NOT paragraph-wrap or
|
|
175
|
+
* coalesce. Single-line comments are stripped only of edge whitespace.
|
|
176
|
+
*
|
|
177
|
+
* @param {string} raw - Comment body text without the ``<!--`` / ``-->``
|
|
178
|
+
* delimiters.
|
|
179
|
+
* @returns {string} Repaired text suitable for storage in ``help_text``.
|
|
180
|
+
*/
|
|
181
|
+
function repairCommentText(raw) {
|
|
182
|
+
if (raw === null || raw === undefined) {
|
|
183
|
+
return '';
|
|
184
|
+
}
|
|
185
|
+
const str = String(raw);
|
|
186
|
+
if (!str.includes('\n')) {
|
|
187
|
+
return str.replace(/^[ \t\r\n]+|[ \t\r\n]+$/g, '');
|
|
188
|
+
}
|
|
189
|
+
const lines = str.split('\n');
|
|
190
|
+
while (lines.length && lines[0].trim() === '') {
|
|
191
|
+
lines.shift();
|
|
192
|
+
}
|
|
193
|
+
while (lines.length && lines[lines.length - 1].trim() === '') {
|
|
194
|
+
lines.pop();
|
|
195
|
+
}
|
|
196
|
+
if (lines.length === 0) {
|
|
197
|
+
return '';
|
|
198
|
+
}
|
|
199
|
+
const indents = [];
|
|
200
|
+
for (const line of lines) {
|
|
201
|
+
if (line.trim() === '')
|
|
202
|
+
continue;
|
|
203
|
+
const m = line.match(/^[ \t]*/);
|
|
204
|
+
indents.push(m ? m[0].length : 0);
|
|
205
|
+
}
|
|
206
|
+
const common = indents.length ? Math.min(...indents) : 0;
|
|
207
|
+
if (common) {
|
|
208
|
+
for (let i = 0; i < lines.length; i++) {
|
|
209
|
+
const line = lines[i];
|
|
210
|
+
lines[i] = line.length >= common ? line.slice(common) : line;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return lines.join('\n');
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Return a body string safe to wrap in ``<!-- ... -->``.
|
|
217
|
+
*
|
|
218
|
+
* Per CONV-F §340-344: the forbidden sequence ``--`` is replaced with
|
|
219
|
+
* ``- -`` and a body ending in ``-`` gets one trailing space appended.
|
|
220
|
+
*
|
|
221
|
+
* @param {string} text - Repaired help_text entry.
|
|
222
|
+
* @returns {string} Comment-safe body.
|
|
223
|
+
*/
|
|
224
|
+
function emitSafeCommentText(text) {
|
|
225
|
+
let safe = String(text == null ? '' : text).replace(/--/g, '- -');
|
|
226
|
+
if (safe.endsWith('-')) {
|
|
227
|
+
safe = safe + ' ';
|
|
228
|
+
}
|
|
229
|
+
return safe;
|
|
230
|
+
}
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
// Pre-pass node classifiers + helpers.
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
/**
|
|
235
|
+
* The preserveOrder shape of fast-xml-parser v5 represents every node as an
|
|
236
|
+
* object with exactly one own-data key plus an optional ``:@`` attributes
|
|
237
|
+
* group. This helper returns the data key, or ``null`` for the attributes
|
|
238
|
+
* group key itself.
|
|
239
|
+
*
|
|
240
|
+
* @param {object} node - preserveOrder node.
|
|
241
|
+
* @returns {string|null} The data key (tag name or ``#text`` / ``#comment``).
|
|
242
|
+
*/
|
|
243
|
+
function nodeDataKey(node) {
|
|
244
|
+
if (node === null || typeof node !== 'object')
|
|
245
|
+
return null;
|
|
246
|
+
for (const k of Object.keys(node)) {
|
|
247
|
+
if (k !== ':@')
|
|
248
|
+
return k;
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* @param {object} node - preserveOrder node.
|
|
254
|
+
* @returns {boolean}
|
|
255
|
+
*/
|
|
256
|
+
function isCommentNode(node) {
|
|
257
|
+
return nodeDataKey(node) === '#comment';
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* @param {object} node - preserveOrder node.
|
|
261
|
+
* @returns {boolean}
|
|
262
|
+
*/
|
|
263
|
+
function isTextNode(node) {
|
|
264
|
+
return nodeDataKey(node) === '#text';
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* @param {object} node - preserveOrder node.
|
|
268
|
+
* @returns {boolean} True if this is a processing instruction node.
|
|
269
|
+
*/
|
|
270
|
+
function isProcessingInstructionNode(node) {
|
|
271
|
+
const k = nodeDataKey(node);
|
|
272
|
+
return typeof k === 'string' && k.startsWith('?');
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* @param {object} node - preserveOrder node.
|
|
276
|
+
* @returns {boolean} True for element nodes (tag-named, not PI/comment/text).
|
|
277
|
+
*/
|
|
278
|
+
function isElementNode(node) {
|
|
279
|
+
const k = nodeDataKey(node);
|
|
280
|
+
if (k === null)
|
|
281
|
+
return false;
|
|
282
|
+
if (k === '#text' || k === '#comment')
|
|
283
|
+
return false;
|
|
284
|
+
if (k.startsWith('?'))
|
|
285
|
+
return false;
|
|
286
|
+
if (k.startsWith('!'))
|
|
287
|
+
return false;
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Extract the body text from a ``#comment`` node.
|
|
292
|
+
*
|
|
293
|
+
* @param {object} node - preserveOrder comment node.
|
|
294
|
+
* @returns {string} Raw body text.
|
|
295
|
+
*/
|
|
296
|
+
function commentBody(node) {
|
|
297
|
+
const arr = node['#comment'];
|
|
298
|
+
if (!Array.isArray(arr) || arr.length === 0)
|
|
299
|
+
return '';
|
|
300
|
+
const first = arr[0];
|
|
301
|
+
if (first && typeof first === 'object' && '#text' in first) {
|
|
302
|
+
return String(first['#text'] == null ? '' : first['#text']);
|
|
303
|
+
}
|
|
304
|
+
return '';
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Extract the body text from a ``#text`` node.
|
|
308
|
+
*/
|
|
309
|
+
function textBody(node) {
|
|
310
|
+
if (!node || typeof node !== 'object')
|
|
311
|
+
return '';
|
|
312
|
+
const v = node['#text'];
|
|
313
|
+
return v == null ? '' : String(v);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Strip a local prefix from a tag returned by fast-xml-parser. The parser
|
|
317
|
+
* already drops the ``xmlns`` URI; this helper survives any future
|
|
318
|
+
* namespace-aware caller using ``{namespace}local`` form.
|
|
319
|
+
*
|
|
320
|
+
* @param {string} tag - Tag name.
|
|
321
|
+
* @returns {string} Local tag name.
|
|
322
|
+
*/
|
|
323
|
+
function localName(tag) {
|
|
324
|
+
if (typeof tag !== 'string')
|
|
325
|
+
return tag;
|
|
326
|
+
if (tag.startsWith('{')) {
|
|
327
|
+
const end = tag.indexOf('}');
|
|
328
|
+
if (end !== -1)
|
|
329
|
+
return tag.slice(end + 1);
|
|
330
|
+
}
|
|
331
|
+
return tag;
|
|
332
|
+
}
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
// Pre-pass: tree walker that builds the address map.
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
/**
|
|
337
|
+
* Walk an element's preserveOrder children, partition comments onto
|
|
338
|
+
* deterministic addresses per CONV-F rules, and recurse.
|
|
339
|
+
*
|
|
340
|
+
* Mutates ``addressMap`` in place. Does NOT strip comments — that happens
|
|
341
|
+
* separately in ``buildCleanedTree``.
|
|
342
|
+
*
|
|
343
|
+
* @param {object} elem - preserveOrder element node.
|
|
344
|
+
* @param {Array<[string, number]>} address - Address of ``elem`` itself.
|
|
345
|
+
* @param {Map<string, string[]>} addressMap - canonical address-key ->
|
|
346
|
+
* comment list.
|
|
347
|
+
* @param {boolean} insideSourceBody - True if any ancestor is
|
|
348
|
+
* ``<script>`` / ``<data>``.
|
|
349
|
+
* @param {boolean} insideExtension - True if any ancestor is an opaque
|
|
350
|
+
* extension element.
|
|
351
|
+
*/
|
|
352
|
+
function walkPromotion(elem, address, addressMap, insideSourceBody, insideExtension) {
|
|
353
|
+
const elemTag = nodeDataKey(elem);
|
|
354
|
+
if (elemTag === null)
|
|
355
|
+
return;
|
|
356
|
+
const elemLocal = localName(elemTag);
|
|
357
|
+
const elemInSource = insideSourceBody || SOURCE_BODY_TAGS.has(elemLocal);
|
|
358
|
+
// The root element is always treated as inside-known: APPLIES_TO_TAGS
|
|
359
|
+
// contains "scxml" so the root passes; for any other root we still walk
|
|
360
|
+
// its subtree but no promotions will land because elemInExtension stays
|
|
361
|
+
// true at every step.
|
|
362
|
+
const elemInExtension = insideExtension || (!APPLIES_TO_TAGS.has(elemLocal) && address.length > 0);
|
|
363
|
+
const children = elem[elemTag];
|
|
364
|
+
if (!Array.isArray(children) || children.length === 0)
|
|
365
|
+
return;
|
|
366
|
+
// Pending: comments awaiting a next element sibling.
|
|
367
|
+
let pending = [];
|
|
368
|
+
// Per-local-tag sibling counters for child elements.
|
|
369
|
+
const perTagCounter = Object.create(null);
|
|
370
|
+
for (const child of children) {
|
|
371
|
+
if (isTextNode(child)) {
|
|
372
|
+
const t = textBody(child);
|
|
373
|
+
if (t && t.trim() !== '') {
|
|
374
|
+
// Non-whitespace text severs the "pending -> next element"
|
|
375
|
+
// relationship; flush pending to *this* element (its parent).
|
|
376
|
+
if (pending.length) {
|
|
377
|
+
if (!elemInSource &&
|
|
378
|
+
!elemInExtension &&
|
|
379
|
+
APPLIES_TO_TAGS.has(elemLocal) &&
|
|
380
|
+
address.length > 0) {
|
|
381
|
+
const key = addressKey(address);
|
|
382
|
+
const existing = addressMap.get(key) || [];
|
|
383
|
+
addressMap.set(key, existing.concat(pending));
|
|
384
|
+
}
|
|
385
|
+
pending = [];
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// whitespace-only text does not flush
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (isProcessingInstructionNode(child)) {
|
|
392
|
+
// PIs never promote, never sever.
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (isCommentNode(child)) {
|
|
396
|
+
const raw = commentBody(child);
|
|
397
|
+
pending.push(repairCommentText(raw));
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (isElementNode(child)) {
|
|
401
|
+
const childTag = nodeDataKey(child);
|
|
402
|
+
const childLocal = localName(childTag);
|
|
403
|
+
const idx = perTagCounter[childLocal] || 0;
|
|
404
|
+
perTagCounter[childLocal] = idx + 1;
|
|
405
|
+
const childAddr = address.concat([[childLocal, idx]]);
|
|
406
|
+
const targetEligible = APPLIES_TO_TAGS.has(childLocal) &&
|
|
407
|
+
!elemInSource &&
|
|
408
|
+
!elemInExtension;
|
|
409
|
+
if (pending.length && targetEligible) {
|
|
410
|
+
const key = addressKey(childAddr);
|
|
411
|
+
const existing = addressMap.get(key) || [];
|
|
412
|
+
addressMap.set(key, existing.concat(pending));
|
|
413
|
+
}
|
|
414
|
+
else if (pending.length) {
|
|
415
|
+
// Try flushing to parent if parent is promotion-eligible.
|
|
416
|
+
if (address.length > 0 &&
|
|
417
|
+
!elemInSource &&
|
|
418
|
+
!elemInExtension &&
|
|
419
|
+
APPLIES_TO_TAGS.has(elemLocal)) {
|
|
420
|
+
const key = addressKey(address);
|
|
421
|
+
const existing = addressMap.get(key) || [];
|
|
422
|
+
addressMap.set(key, existing.concat(pending));
|
|
423
|
+
}
|
|
424
|
+
// Else silently drop (extension subtree).
|
|
425
|
+
}
|
|
426
|
+
pending = [];
|
|
427
|
+
walkPromotion(child, childAddr, addressMap, elemInSource, elemInExtension);
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
// Anything else (DOCTYPE, etc.) — ignore.
|
|
431
|
+
}
|
|
432
|
+
// After all children, leftover pending comments attach to this element
|
|
433
|
+
// (rule 3). Suppressed under script/data and extension subtrees.
|
|
434
|
+
if (pending.length) {
|
|
435
|
+
if (!elemInSource &&
|
|
436
|
+
!elemInExtension &&
|
|
437
|
+
APPLIES_TO_TAGS.has(elemLocal) &&
|
|
438
|
+
address.length > 0) {
|
|
439
|
+
const key = addressKey(address);
|
|
440
|
+
const existing = addressMap.get(key) || [];
|
|
441
|
+
addressMap.set(key, existing.concat(pending));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Build a comment-free copy of the preserveOrder tree, preserving
|
|
447
|
+
* processing instructions but dropping every ``#comment``.
|
|
448
|
+
*
|
|
449
|
+
* @param {Array<object>} tree - Top-level preserveOrder array.
|
|
450
|
+
* @returns {Array<object>} Cleaned tree (new array, deep-cloned bodies).
|
|
451
|
+
*/
|
|
452
|
+
function buildCleanedTree(tree) {
|
|
453
|
+
function clean(node) {
|
|
454
|
+
if (node === null || typeof node !== 'object')
|
|
455
|
+
return node;
|
|
456
|
+
const dataKey = nodeDataKey(node);
|
|
457
|
+
if (dataKey === '#comment')
|
|
458
|
+
return null;
|
|
459
|
+
const out = {};
|
|
460
|
+
for (const k of Object.keys(node)) {
|
|
461
|
+
const v = node[k];
|
|
462
|
+
if (k === ':@') {
|
|
463
|
+
out[k] = Array.isArray(v) ? v.slice() : Object.assign({}, v);
|
|
464
|
+
}
|
|
465
|
+
else if (Array.isArray(v)) {
|
|
466
|
+
const cleaned = [];
|
|
467
|
+
for (const c of v) {
|
|
468
|
+
const r = clean(c);
|
|
469
|
+
if (r !== null)
|
|
470
|
+
cleaned.push(r);
|
|
471
|
+
}
|
|
472
|
+
out[k] = cleaned;
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
out[k] = v;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return out;
|
|
479
|
+
}
|
|
480
|
+
const result = [];
|
|
481
|
+
for (const n of tree) {
|
|
482
|
+
const r = clean(n);
|
|
483
|
+
if (r !== null)
|
|
484
|
+
result.push(r);
|
|
485
|
+
}
|
|
486
|
+
return result;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Locate the root element in a preserveOrder top-level tree.
|
|
490
|
+
*
|
|
491
|
+
* @param {Array<object>} tree - Top-level array.
|
|
492
|
+
* @returns {object|null} Root element node or null.
|
|
493
|
+
*/
|
|
494
|
+
function findRootElement(tree) {
|
|
495
|
+
for (const node of tree) {
|
|
496
|
+
if (isElementNode(node))
|
|
497
|
+
return node;
|
|
498
|
+
}
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Extract help_text promotion metadata from an SCXML string.
|
|
503
|
+
*
|
|
504
|
+
* @param {string} xmlStr - Raw SCXML string.
|
|
505
|
+
* @returns {{cleanedXml: string, addressMap: Map<string, string[]>, addressMapJson: Array<{address: Array<[string,number]>, comments: string[]}>}}
|
|
506
|
+
* The comment-free XML serialization and the address map.
|
|
507
|
+
* ``addressMapJson`` provides a stable JSON-friendly projection for
|
|
508
|
+
* downstream callers or cross-language fixture comparison.
|
|
509
|
+
* On parse failure returns the original string and an empty map.
|
|
510
|
+
*/
|
|
511
|
+
function extractHelpTextFromXml(xmlStr) {
|
|
512
|
+
if (typeof xmlStr !== 'string' || xmlStr.length === 0) {
|
|
513
|
+
return { cleanedXml: xmlStr, addressMap: new Map(), addressMapJson: [] };
|
|
514
|
+
}
|
|
515
|
+
const parser = new XMLParser({
|
|
516
|
+
ignoreAttributes: false,
|
|
517
|
+
trimValues: false,
|
|
518
|
+
preserveOrder: true,
|
|
519
|
+
commentPropName: '#comment',
|
|
520
|
+
ignoreDeclaration: false,
|
|
521
|
+
ignorePiTags: false,
|
|
522
|
+
parseTagValue: false,
|
|
523
|
+
});
|
|
524
|
+
let tree;
|
|
525
|
+
try {
|
|
526
|
+
tree = parser.parse(xmlStr);
|
|
527
|
+
}
|
|
528
|
+
catch (e) {
|
|
529
|
+
return { cleanedXml: xmlStr, addressMap: new Map(), addressMapJson: [] };
|
|
530
|
+
}
|
|
531
|
+
if (!Array.isArray(tree)) {
|
|
532
|
+
return { cleanedXml: xmlStr, addressMap: new Map(), addressMapJson: [] };
|
|
533
|
+
}
|
|
534
|
+
const root = findRootElement(tree);
|
|
535
|
+
const addressMap = new Map();
|
|
536
|
+
if (root === null) {
|
|
537
|
+
return { cleanedXml: xmlStr, addressMap, addressMapJson: [] };
|
|
538
|
+
}
|
|
539
|
+
const rootLocal = localName(nodeDataKey(root));
|
|
540
|
+
const rootAddr = [[rootLocal, 0]];
|
|
541
|
+
// CONV-F rule 4: pre-root and post-root comments attach to root.
|
|
542
|
+
if (APPLIES_TO_TAGS.has(rootLocal)) {
|
|
543
|
+
const preRoot = [];
|
|
544
|
+
const postRoot = [];
|
|
545
|
+
let sawRoot = false;
|
|
546
|
+
for (const node of tree) {
|
|
547
|
+
if (node === root) {
|
|
548
|
+
sawRoot = true;
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (isCommentNode(node)) {
|
|
552
|
+
const repaired = repairCommentText(commentBody(node));
|
|
553
|
+
if (sawRoot) {
|
|
554
|
+
postRoot.push(repaired);
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
preRoot.push(repaired);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
if (preRoot.length || postRoot.length) {
|
|
562
|
+
const key = addressKey(rootAddr);
|
|
563
|
+
const existing = addressMap.get(key) || [];
|
|
564
|
+
addressMap.set(key, existing.concat(preRoot).concat(postRoot));
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
walkPromotion(root, rootAddr, addressMap, false, false);
|
|
568
|
+
// Strip comments and re-serialize.
|
|
569
|
+
const cleaned = buildCleanedTree(tree);
|
|
570
|
+
const builder = new XMLBuilder({
|
|
571
|
+
ignoreAttributes: false,
|
|
572
|
+
preserveOrder: true,
|
|
573
|
+
commentPropName: '#comment',
|
|
574
|
+
format: false,
|
|
575
|
+
suppressEmptyNode: true,
|
|
576
|
+
});
|
|
577
|
+
let cleanedXml;
|
|
578
|
+
try {
|
|
579
|
+
cleanedXml = builder.build(cleaned);
|
|
580
|
+
}
|
|
581
|
+
catch (e) {
|
|
582
|
+
cleanedXml = xmlStr;
|
|
583
|
+
}
|
|
584
|
+
const addressMapJson = [];
|
|
585
|
+
for (const [k, v] of addressMap.entries()) {
|
|
586
|
+
addressMapJson.push({ address: JSON.parse(k), comments: v.slice() });
|
|
587
|
+
}
|
|
588
|
+
return { cleanedXml, addressMap, addressMapJson };
|
|
589
|
+
}
|
|
590
|
+
// ---------------------------------------------------------------------------
|
|
591
|
+
// Attach helper: walk the JS model in lockstep with the address map.
|
|
592
|
+
// ---------------------------------------------------------------------------
|
|
593
|
+
/**
|
|
594
|
+
* Locate a child by ``(local_tag, index)`` segment on a JS model object.
|
|
595
|
+
*
|
|
596
|
+
* Handles both array-shaped collections and singleton transition fields
|
|
597
|
+
* (``History.transition`` / ``Initial.transition``).
|
|
598
|
+
*
|
|
599
|
+
* @param {object} parent - Parent model node.
|
|
600
|
+
* @param {string} parentTag - Local XML tag of the parent.
|
|
601
|
+
* @param {string} tag - Child local XML tag.
|
|
602
|
+
* @param {number} index - Sibling index.
|
|
603
|
+
* @returns {object|null}
|
|
604
|
+
*/
|
|
605
|
+
function resolveChild(parent, parentTag, tag, index) {
|
|
606
|
+
if (parent === null || typeof parent !== 'object')
|
|
607
|
+
return null;
|
|
608
|
+
const attr = modelAttrForTag(tag);
|
|
609
|
+
const coll = parent[attr];
|
|
610
|
+
if (coll === undefined || coll === null)
|
|
611
|
+
return null;
|
|
612
|
+
if (Array.isArray(coll)) {
|
|
613
|
+
if (index >= 0 && index < coll.length)
|
|
614
|
+
return coll[index];
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
// Non-array: a singleton object. Only index 0 maps to it.
|
|
618
|
+
if (typeof coll === 'object' && index === 0)
|
|
619
|
+
return coll;
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Navigate ``modelRoot`` to the element at ``address``.
|
|
624
|
+
*
|
|
625
|
+
* Address ``[["scxml", 0]]`` resolves to ``modelRoot`` itself.
|
|
626
|
+
*
|
|
627
|
+
* @param {object} modelRoot - Root SCJSON object.
|
|
628
|
+
* @param {Array<[string, number]>} address - Address tuple.
|
|
629
|
+
* @returns {object|null}
|
|
630
|
+
*/
|
|
631
|
+
function resolveAddress(modelRoot, address) {
|
|
632
|
+
if (!Array.isArray(address) || address.length === 0)
|
|
633
|
+
return null;
|
|
634
|
+
let node = modelRoot;
|
|
635
|
+
let parentTag = address[0][0];
|
|
636
|
+
for (let i = 1; i < address.length; i++) {
|
|
637
|
+
const [tag, idx] = address[i];
|
|
638
|
+
node = resolveChild(node, parentTag, tag, idx);
|
|
639
|
+
if (node === null)
|
|
640
|
+
return null;
|
|
641
|
+
parentTag = tag;
|
|
642
|
+
}
|
|
643
|
+
return node;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Append each address's comment list to the corresponding model element's
|
|
647
|
+
* ``help_text`` array.
|
|
648
|
+
*
|
|
649
|
+
* Comments are appended to any existing ``help_text`` entries per CONV-F
|
|
650
|
+
* rule 7.
|
|
651
|
+
*
|
|
652
|
+
* @param {object} model - Root SCJSON object.
|
|
653
|
+
* @param {Map<string, string[]>} addressMap - Map produced by
|
|
654
|
+
* ``extractHelpTextFromXml``.
|
|
655
|
+
*/
|
|
656
|
+
function attachHelpTextToModel(model, addressMap) {
|
|
657
|
+
if (!model || typeof model !== 'object')
|
|
658
|
+
return;
|
|
659
|
+
if (!addressMap || addressMap.size === 0)
|
|
660
|
+
return;
|
|
661
|
+
for (const [key, comments] of addressMap.entries()) {
|
|
662
|
+
if (!comments || comments.length === 0)
|
|
663
|
+
continue;
|
|
664
|
+
const address = JSON.parse(key);
|
|
665
|
+
const target = resolveAddress(model, address);
|
|
666
|
+
if (target === null || typeof target !== 'object')
|
|
667
|
+
continue;
|
|
668
|
+
if (Array.isArray(target.help_text)) {
|
|
669
|
+
target.help_text = target.help_text.concat(comments);
|
|
670
|
+
}
|
|
671
|
+
else if (target.help_text === undefined) {
|
|
672
|
+
target.help_text = comments.slice();
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
// Scalar / non-array — coerce to array preserving existing.
|
|
676
|
+
target.help_text = [String(target.help_text)].concat(comments);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
// ---------------------------------------------------------------------------
|
|
681
|
+
// Post-pass: inject comments into already-serialized SCXML.
|
|
682
|
+
// ---------------------------------------------------------------------------
|
|
683
|
+
/**
|
|
684
|
+
* Walk a model and record every non-empty ``help_text`` keyed by address.
|
|
685
|
+
*
|
|
686
|
+
* @param {object} node - Model node.
|
|
687
|
+
* @param {Array<[string, number]>} address - Address of ``node``.
|
|
688
|
+
* @param {Map<string, string[]>} out - Output map.
|
|
689
|
+
*/
|
|
690
|
+
function collectHelpTextAddresses(node, address, out) {
|
|
691
|
+
if (node === null || typeof node !== 'object')
|
|
692
|
+
return;
|
|
693
|
+
if (Array.isArray(node.help_text) && node.help_text.length > 0) {
|
|
694
|
+
out.set(addressKey(address), node.help_text.slice());
|
|
695
|
+
}
|
|
696
|
+
// Recurse into typed child fields.
|
|
697
|
+
// We discover children by enumerating the model's own keys and treating
|
|
698
|
+
// any value that is an array of objects or a singleton object as a
|
|
699
|
+
// potential XML child collection — but we map JS attr name back to XML
|
|
700
|
+
// local tag name.
|
|
701
|
+
const reverseTagMap = Object.create(null);
|
|
702
|
+
for (const [tag, attr] of Object.entries(TAG_TO_ATTR)) {
|
|
703
|
+
reverseTagMap[attr] = tag;
|
|
704
|
+
}
|
|
705
|
+
for (const [attr, value] of Object.entries(node)) {
|
|
706
|
+
if (attr === 'help_text')
|
|
707
|
+
continue;
|
|
708
|
+
if (attr === 'other_attributes')
|
|
709
|
+
continue;
|
|
710
|
+
if (attr.startsWith('@_'))
|
|
711
|
+
continue;
|
|
712
|
+
if (attr.endsWith('_attribute'))
|
|
713
|
+
continue;
|
|
714
|
+
// Skip scalar attributes; only objects/arrays-of-objects are XML
|
|
715
|
+
// children we care about for addressing.
|
|
716
|
+
const tag = Object.prototype.hasOwnProperty.call(reverseTagMap, attr)
|
|
717
|
+
? reverseTagMap[attr]
|
|
718
|
+
: attr;
|
|
719
|
+
// Only descend into collections whose attr name is plausibly an SCXML
|
|
720
|
+
// child element tag (or its renamed JS form).
|
|
721
|
+
if (!APPLIES_TO_TAGS.has(tag))
|
|
722
|
+
continue;
|
|
723
|
+
if (Array.isArray(value)) {
|
|
724
|
+
for (let i = 0; i < value.length; i++) {
|
|
725
|
+
const child = value[i];
|
|
726
|
+
if (child && typeof child === 'object') {
|
|
727
|
+
collectHelpTextAddresses(child, address.concat([[tag, i]]), out);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
else if (value && typeof value === 'object') {
|
|
732
|
+
collectHelpTextAddresses(value, address.concat([[tag, 0]]), out);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* Find a preserveOrder element at an address under ``rootNode``.
|
|
738
|
+
*
|
|
739
|
+
* @param {object} rootNode - Root preserveOrder element node.
|
|
740
|
+
* @param {Array<[string, number]>} address - Address tuple.
|
|
741
|
+
* @returns {{node: object, parent: Array<object>|null, indexInParent: number}|null}
|
|
742
|
+
*/
|
|
743
|
+
function navigateXml(rootNode, address) {
|
|
744
|
+
if (!Array.isArray(address) || address.length === 0)
|
|
745
|
+
return null;
|
|
746
|
+
let node = rootNode;
|
|
747
|
+
let parentList = null;
|
|
748
|
+
let indexInParent = -1;
|
|
749
|
+
for (let i = 1; i < address.length; i++) {
|
|
750
|
+
const [tag, idx] = address[i];
|
|
751
|
+
const dk = nodeDataKey(node);
|
|
752
|
+
if (dk === null)
|
|
753
|
+
return null;
|
|
754
|
+
const children = node[dk];
|
|
755
|
+
if (!Array.isArray(children))
|
|
756
|
+
return null;
|
|
757
|
+
let count = 0;
|
|
758
|
+
let found = -1;
|
|
759
|
+
for (let j = 0; j < children.length; j++) {
|
|
760
|
+
const child = children[j];
|
|
761
|
+
if (!isElementNode(child))
|
|
762
|
+
continue;
|
|
763
|
+
if (localName(nodeDataKey(child)) === tag) {
|
|
764
|
+
if (count === idx) {
|
|
765
|
+
found = j;
|
|
766
|
+
break;
|
|
767
|
+
}
|
|
768
|
+
count++;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
if (found < 0)
|
|
772
|
+
return null;
|
|
773
|
+
parentList = children;
|
|
774
|
+
indexInParent = found;
|
|
775
|
+
node = children[found];
|
|
776
|
+
}
|
|
777
|
+
return { node, parent: parentList, indexInParent };
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Build a ``#comment`` preserveOrder node with safe body text.
|
|
781
|
+
*
|
|
782
|
+
* @param {string} text - Repaired help_text entry.
|
|
783
|
+
* @returns {object}
|
|
784
|
+
*/
|
|
785
|
+
function makeCommentNode(text) {
|
|
786
|
+
return { '#comment': [{ '#text': emitSafeCommentText(text) }] };
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Re-emit ``xmlStr`` with each model element's ``help_text`` entries as
|
|
790
|
+
* leading XML comments.
|
|
791
|
+
*
|
|
792
|
+
* @param {string} xmlStr - Comment-free SCXML produced by the existing
|
|
793
|
+
* ``jsonToXml`` pipeline.
|
|
794
|
+
* @param {object} model - The SCJSON model whose ``help_text`` lists drive
|
|
795
|
+
* injection.
|
|
796
|
+
* @returns {string} SCXML with leading comments inserted.
|
|
797
|
+
*/
|
|
798
|
+
function injectHelpTextCommentsIntoXml(xmlStr, model) {
|
|
799
|
+
if (!model || typeof model !== 'object')
|
|
800
|
+
return xmlStr;
|
|
801
|
+
const collected = new Map();
|
|
802
|
+
// Determine the root local tag by parsing once. xsdata-equivalent JS
|
|
803
|
+
// output always starts with ``<scxml`` (or `<?xml ... ?><scxml`); for
|
|
804
|
+
// safety we read the actual root from the parsed tree.
|
|
805
|
+
const parser = new XMLParser({
|
|
806
|
+
ignoreAttributes: false,
|
|
807
|
+
trimValues: false,
|
|
808
|
+
preserveOrder: true,
|
|
809
|
+
commentPropName: '#comment',
|
|
810
|
+
ignoreDeclaration: false,
|
|
811
|
+
ignorePiTags: false,
|
|
812
|
+
parseTagValue: false,
|
|
813
|
+
});
|
|
814
|
+
let tree;
|
|
815
|
+
try {
|
|
816
|
+
tree = parser.parse(xmlStr);
|
|
817
|
+
}
|
|
818
|
+
catch (e) {
|
|
819
|
+
return xmlStr;
|
|
820
|
+
}
|
|
821
|
+
if (!Array.isArray(tree))
|
|
822
|
+
return xmlStr;
|
|
823
|
+
const root = findRootElement(tree);
|
|
824
|
+
if (root === null)
|
|
825
|
+
return xmlStr;
|
|
826
|
+
const rootLocal = localName(nodeDataKey(root));
|
|
827
|
+
const rootAddr = [[rootLocal, 0]];
|
|
828
|
+
collectHelpTextAddresses(model, rootAddr, collected);
|
|
829
|
+
if (collected.size === 0)
|
|
830
|
+
return xmlStr;
|
|
831
|
+
// Pop the root entries (special handling: insert as document-level
|
|
832
|
+
// siblings before the root element).
|
|
833
|
+
const rootKey = addressKey(rootAddr);
|
|
834
|
+
const rootEntries = collected.get(rootKey);
|
|
835
|
+
collected.delete(rootKey);
|
|
836
|
+
// For deterministic order, sort the remaining addresses lexically so
|
|
837
|
+
// re-emit order is stable regardless of Map iteration order.
|
|
838
|
+
const sortedKeys = Array.from(collected.keys()).sort();
|
|
839
|
+
// To preserve correct sibling indexes while inserting, process each
|
|
840
|
+
// address fresh by re-navigating after each insertion at that address.
|
|
841
|
+
for (const key of sortedKeys) {
|
|
842
|
+
const address = JSON.parse(key);
|
|
843
|
+
const found = navigateXml(root, address);
|
|
844
|
+
if (found === null)
|
|
845
|
+
continue;
|
|
846
|
+
const { parent, indexInParent } = found;
|
|
847
|
+
if (parent === null)
|
|
848
|
+
continue;
|
|
849
|
+
const entries = collected.get(key);
|
|
850
|
+
// Determine the leading whitespace that precedes ``target`` in the
|
|
851
|
+
// already-formatted XML. The XML builder is run with format:false
|
|
852
|
+
// (below) so we explicitly reproduce input indentation between
|
|
853
|
+
// injected comments and the target element.
|
|
854
|
+
let leadingWhitespace = '';
|
|
855
|
+
if (indexInParent > 0) {
|
|
856
|
+
const prev = parent[indexInParent - 1];
|
|
857
|
+
if (isTextNode(prev)) {
|
|
858
|
+
const t = textBody(prev);
|
|
859
|
+
if (/\n/.test(t)) {
|
|
860
|
+
leadingWhitespace = '\n' + t.split('\n').pop();
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
leadingWhitespace = t;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
const insertion = [];
|
|
868
|
+
for (let i = 0; i < entries.length; i++) {
|
|
869
|
+
insertion.push(makeCommentNode(entries[i]));
|
|
870
|
+
if (leadingWhitespace) {
|
|
871
|
+
insertion.push({ '#text': leadingWhitespace });
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
// Insert all comments + spacing at the same position; the target
|
|
875
|
+
// shifts right.
|
|
876
|
+
parent.splice(indexInParent, 0, ...insertion);
|
|
877
|
+
}
|
|
878
|
+
// Insert root-level comments as document-level siblings BEFORE the root.
|
|
879
|
+
if (rootEntries && rootEntries.length) {
|
|
880
|
+
const rootIdx = tree.indexOf(root);
|
|
881
|
+
if (rootIdx >= 0) {
|
|
882
|
+
const insertion = [];
|
|
883
|
+
for (let i = 0; i < rootEntries.length; i++) {
|
|
884
|
+
insertion.push(makeCommentNode(rootEntries[i]));
|
|
885
|
+
// Insert a newline between adjacent root-level siblings so the
|
|
886
|
+
// result is human-readable.
|
|
887
|
+
insertion.push({ '#text': '\n' });
|
|
888
|
+
}
|
|
889
|
+
tree.splice(rootIdx, 0, ...insertion);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
const builder = new XMLBuilder({
|
|
893
|
+
ignoreAttributes: false,
|
|
894
|
+
preserveOrder: true,
|
|
895
|
+
commentPropName: '#comment',
|
|
896
|
+
format: false,
|
|
897
|
+
suppressEmptyNode: true,
|
|
898
|
+
});
|
|
899
|
+
let out;
|
|
900
|
+
try {
|
|
901
|
+
out = builder.build(tree);
|
|
902
|
+
}
|
|
903
|
+
catch (e) {
|
|
904
|
+
return xmlStr;
|
|
905
|
+
}
|
|
906
|
+
return out;
|
|
907
|
+
}
|
|
908
|
+
module.exports = {
|
|
909
|
+
APPLIES_TO_TAGS,
|
|
910
|
+
SOURCE_BODY_TAGS,
|
|
911
|
+
TAG_TO_ATTR,
|
|
912
|
+
SINGLETON_TRANSITION_PARENTS,
|
|
913
|
+
modelAttrForTag,
|
|
914
|
+
addressKey,
|
|
915
|
+
repairCommentText,
|
|
916
|
+
emitSafeCommentText,
|
|
917
|
+
extractHelpTextFromXml,
|
|
918
|
+
attachHelpTextToModel,
|
|
919
|
+
injectHelpTextCommentsIntoXml,
|
|
920
|
+
};
|