scjson 0.3.5 → 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/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/package.json +3 -3
- package/scjson.schema.json +219 -83
package/dist/converters.d.ts
CHANGED
|
@@ -1,7 +1,46 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type XmlToJsonOptions = {
|
|
2
|
+
/**
|
|
3
|
+
* Drop null and empty containers.
|
|
4
|
+
*/
|
|
5
|
+
omitEmpty?: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Preserve
|
|
8
|
+
* ``xi:include`` as extension content or resolve includes before conversion.
|
|
9
|
+
*/
|
|
10
|
+
xinclude?: "preserve" | "resolve";
|
|
11
|
+
/**
|
|
12
|
+
* Loader used by
|
|
13
|
+
* resolved XInclude mode.
|
|
14
|
+
*/
|
|
15
|
+
xincludeLoader?: (href: string) => string;
|
|
16
|
+
/**
|
|
17
|
+
* File or directory used by the default
|
|
18
|
+
* Node loader in resolved XInclude mode.
|
|
19
|
+
*/
|
|
20
|
+
xincludeBasePath?: string;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {object} XmlToJsonOptions
|
|
24
|
+
* @property {boolean} [omitEmpty=true] Drop null and empty containers.
|
|
25
|
+
* @property {'preserve'|'resolve'} [xinclude='preserve'] Preserve
|
|
26
|
+
* ``xi:include`` as extension content or resolve includes before conversion.
|
|
27
|
+
* @property {(href: string) => string} [xincludeLoader] Loader used by
|
|
28
|
+
* resolved XInclude mode.
|
|
29
|
+
* @property {string} [xincludeBasePath] File or directory used by the default
|
|
30
|
+
* Node loader in resolved XInclude mode.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Convert SCXML to canonical SCJSON.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} xmlStr - SCXML source.
|
|
36
|
+
* @param {boolean|XmlToJsonOptions} [options=true] Backward-compatible boolean
|
|
37
|
+
* ``omitEmpty`` flag or expanded converter options.
|
|
38
|
+
* @returns {{result: string, valid: boolean, errors: object[]|null}} Conversion outcome.
|
|
39
|
+
*/
|
|
40
|
+
export function xmlToJson(xmlStr: string, options?: boolean | XmlToJsonOptions): {
|
|
2
41
|
result: string;
|
|
3
|
-
valid:
|
|
4
|
-
errors:
|
|
42
|
+
valid: boolean;
|
|
43
|
+
errors: object[] | null;
|
|
5
44
|
};
|
|
6
45
|
/**
|
|
7
46
|
* Convert a scjson string to SCXML.
|
|
@@ -110,6 +149,14 @@ export function fixSendContent(value: object | any[]): void;
|
|
|
110
149
|
* @param {object|Array} value - Parsed object to adjust in place.
|
|
111
150
|
*/
|
|
112
151
|
export function fixDonedataContent(value: object | any[]): void;
|
|
152
|
+
export function normaliseXmlToJsonOptions(options: any): {
|
|
153
|
+
omitEmpty: boolean;
|
|
154
|
+
xinclude: any;
|
|
155
|
+
xincludeLoader: any;
|
|
156
|
+
xincludeBasePath: any;
|
|
157
|
+
};
|
|
158
|
+
export function resolveXIncludesInParsed(value: any, parser: any, options: any): any;
|
|
159
|
+
export function preserveExtensionElements(value: any): void;
|
|
113
160
|
export function fixOtherAttributes(value: any): void;
|
|
114
161
|
/**
|
|
115
162
|
* Decode HTML entities in string values.
|
|
@@ -228,3 +275,27 @@ export function stripNestedDataAttrs(value: object | any[]): void;
|
|
|
228
275
|
* @param {object|Array} value - Parsed object to adjust in place.
|
|
229
276
|
*/
|
|
230
277
|
export function stripXmlns(value: object | any[]): void;
|
|
278
|
+
/**
|
|
279
|
+
* Keys that should always be represented as arrays.
|
|
280
|
+
*
|
|
281
|
+
* These are derived from `scjson.schema.json` where the corresponding
|
|
282
|
+
* properties have a ``type`` of ``array``. The parser used here will
|
|
283
|
+
* collapse single elements into objects, so we normalise the structure
|
|
284
|
+
* back to arrays in order to maintain canonical output that matches the
|
|
285
|
+
* reference Python implementation.
|
|
286
|
+
*/
|
|
287
|
+
export const ARRAY_KEYS: Set<string>;
|
|
288
|
+
/**
|
|
289
|
+
* CONV-E: Set of known structural metadata keys that live alongside the
|
|
290
|
+
* SCXML child surface but are not themselves SCXML child elements. These
|
|
291
|
+
* keys MUST round-trip through JSON-side normalization without being folded
|
|
292
|
+
* into ``other_attributes`` or generic ``content``.
|
|
293
|
+
*
|
|
294
|
+
* ``help_text`` is the only member today; future structural metadata keys
|
|
295
|
+
* (e.g. promoted authoring annotations from CONV-F) should be added here.
|
|
296
|
+
*/
|
|
297
|
+
export const STRUCTURAL_METADATA_KEYS: Set<string>;
|
|
298
|
+
import { extractHelpTextFromXml } from "./comment_promotion.js";
|
|
299
|
+
import { attachHelpTextToModel } from "./comment_promotion.js";
|
|
300
|
+
import { injectHelpTextCommentsIntoXml } from "./comment_promotion.js";
|
|
301
|
+
export { extractHelpTextFromXml, attachHelpTextToModel, injectHelpTextCommentsIntoXml };
|
package/dist/converters.js
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
const { XMLParser, XMLBuilder } = require('fast-xml-parser');
|
|
13
13
|
const Ajv = require('ajv');
|
|
14
14
|
const schema = require('../scjson.schema.json');
|
|
15
|
+
const { extractHelpTextFromXml, attachHelpTextToModel, injectHelpTextCommentsIntoXml, } = require('./comment_promotion.js');
|
|
16
|
+
const XINCLUDE_NS = 'http://www.w3.org/2001/XInclude';
|
|
17
|
+
const XINCLUDE_CLARK_INCLUDE = `{${XINCLUDE_NS}}include`;
|
|
15
18
|
/**
|
|
16
19
|
* Keys that should always be represented as arrays.
|
|
17
20
|
*
|
|
@@ -31,6 +34,7 @@ const ARRAY_KEYS = new Set([
|
|
|
31
34
|
'final',
|
|
32
35
|
'finalize',
|
|
33
36
|
'foreach',
|
|
37
|
+
'help_text',
|
|
34
38
|
'history',
|
|
35
39
|
'if_value',
|
|
36
40
|
'initial',
|
|
@@ -46,6 +50,18 @@ const ARRAY_KEYS = new Set([
|
|
|
46
50
|
'send',
|
|
47
51
|
'state',
|
|
48
52
|
]);
|
|
53
|
+
/**
|
|
54
|
+
* CONV-E: Set of known structural metadata keys that live alongside the
|
|
55
|
+
* SCXML child surface but are not themselves SCXML child elements. These
|
|
56
|
+
* keys MUST round-trip through JSON-side normalization without being folded
|
|
57
|
+
* into ``other_attributes`` or generic ``content``.
|
|
58
|
+
*
|
|
59
|
+
* ``help_text`` is the only member today; future structural metadata keys
|
|
60
|
+
* (e.g. promoted authoring annotations from CONV-F) should be added here.
|
|
61
|
+
*/
|
|
62
|
+
const STRUCTURAL_METADATA_KEYS = new Set([
|
|
63
|
+
'help_text',
|
|
64
|
+
]);
|
|
49
65
|
/// Known SCXML structural fields that should be pulled out of `content[]`
|
|
50
66
|
const STRUCTURAL_FIELDS = new Set([
|
|
51
67
|
'state', 'parallel', 'final', 'history',
|
|
@@ -750,6 +766,83 @@ function fixDonedataContent(value) {
|
|
|
750
766
|
}
|
|
751
767
|
}
|
|
752
768
|
}
|
|
769
|
+
function normaliseXmlToJsonOptions(options) {
|
|
770
|
+
if (typeof options === 'boolean' || options === undefined) {
|
|
771
|
+
return {
|
|
772
|
+
omitEmpty: options !== false,
|
|
773
|
+
xinclude: 'preserve',
|
|
774
|
+
xincludeLoader: null,
|
|
775
|
+
xincludeBasePath: null,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
return {
|
|
779
|
+
omitEmpty: options.omitEmpty !== false,
|
|
780
|
+
xinclude: options.xinclude || 'preserve',
|
|
781
|
+
xincludeLoader: options.xincludeLoader || null,
|
|
782
|
+
xincludeBasePath: options.xincludeBasePath || null,
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
function mergeParsedChild(target, key, value) {
|
|
786
|
+
if (target[key] === undefined) {
|
|
787
|
+
target[key] = value;
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
if (Array.isArray(target[key])) {
|
|
791
|
+
Array.isArray(value) ? target[key].push(...value) : target[key].push(value);
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
target[key] = Array.isArray(value)
|
|
795
|
+
? [target[key], ...value]
|
|
796
|
+
: [target[key], value];
|
|
797
|
+
}
|
|
798
|
+
function loadXInclude(href, options) {
|
|
799
|
+
if (typeof options.xincludeLoader === 'function') {
|
|
800
|
+
return options.xincludeLoader(href);
|
|
801
|
+
}
|
|
802
|
+
if (options.xincludeBasePath) {
|
|
803
|
+
const fs = require('fs');
|
|
804
|
+
const path = require('path');
|
|
805
|
+
const base = path.extname(options.xincludeBasePath)
|
|
806
|
+
? path.dirname(options.xincludeBasePath)
|
|
807
|
+
: options.xincludeBasePath;
|
|
808
|
+
return fs.readFileSync(path.resolve(base, href), 'utf8');
|
|
809
|
+
}
|
|
810
|
+
throw new Error(`No XInclude loader configured for ${href}`);
|
|
811
|
+
}
|
|
812
|
+
function isXIncludeElementName(name) {
|
|
813
|
+
return name === 'xi:include' || name === XINCLUDE_CLARK_INCLUDE || name.endsWith(':include');
|
|
814
|
+
}
|
|
815
|
+
function parsedDocumentEntries(doc) {
|
|
816
|
+
return Object.entries(doc).filter(([key]) => key !== '?xml');
|
|
817
|
+
}
|
|
818
|
+
function resolveXIncludesInParsed(value, parser, options) {
|
|
819
|
+
if (Array.isArray(value)) {
|
|
820
|
+
return value.map(item => resolveXIncludesInParsed(item, parser, options));
|
|
821
|
+
}
|
|
822
|
+
if (!value || typeof value !== 'object') {
|
|
823
|
+
return value;
|
|
824
|
+
}
|
|
825
|
+
const out = {};
|
|
826
|
+
for (const [key, child] of Object.entries(value)) {
|
|
827
|
+
if (isXIncludeElementName(key)) {
|
|
828
|
+
const includes = Array.isArray(child) ? child : [child];
|
|
829
|
+
includes.forEach(includeNode => {
|
|
830
|
+
const href = includeNode && (includeNode['@_href'] || includeNode.href);
|
|
831
|
+
if (!href) {
|
|
832
|
+
throw new Error('XInclude element is missing href');
|
|
833
|
+
}
|
|
834
|
+
const includedXml = loadXInclude(href, options);
|
|
835
|
+
const includedDoc = resolveXIncludesInParsed(parser.parse(includedXml), parser, options);
|
|
836
|
+
parsedDocumentEntries(includedDoc).forEach(([incKey, incValue]) => {
|
|
837
|
+
mergeParsedChild(out, incKey, incValue);
|
|
838
|
+
});
|
|
839
|
+
});
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
out[key] = resolveXIncludesInParsed(child, parser, options);
|
|
843
|
+
}
|
|
844
|
+
return out;
|
|
845
|
+
}
|
|
753
846
|
/**
|
|
754
847
|
* Convert arbitrary objects parsed under ``<data>`` elements into
|
|
755
848
|
* canonical content structures.
|
|
@@ -812,6 +905,53 @@ function convertDataNode(name, node) {
|
|
|
812
905
|
}
|
|
813
906
|
return { qname: name, text: String(node) };
|
|
814
907
|
}
|
|
908
|
+
function appendOtherElement(target, node) {
|
|
909
|
+
const nodes = Array.isArray(node) ? node : [node];
|
|
910
|
+
if (!Array.isArray(target.other_element)) {
|
|
911
|
+
target.other_element = [];
|
|
912
|
+
}
|
|
913
|
+
target.other_element.push(...nodes);
|
|
914
|
+
}
|
|
915
|
+
function canonicalExtensionName(name) {
|
|
916
|
+
if (name === 'xi:include') {
|
|
917
|
+
return XINCLUDE_CLARK_INCLUDE;
|
|
918
|
+
}
|
|
919
|
+
return name;
|
|
920
|
+
}
|
|
921
|
+
function ensureExtensionNamespace(name, node) {
|
|
922
|
+
const nodes = Array.isArray(node) ? node : [node];
|
|
923
|
+
nodes.forEach(item => {
|
|
924
|
+
if (!item || typeof item !== 'object')
|
|
925
|
+
return;
|
|
926
|
+
if (name === 'xi:include' || name === XINCLUDE_CLARK_INCLUDE) {
|
|
927
|
+
item.attributes = item.attributes || {};
|
|
928
|
+
if (item.attributes['xmlns:xi'] === undefined) {
|
|
929
|
+
item.attributes['xmlns:xi'] = XINCLUDE_NS;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
return node;
|
|
934
|
+
}
|
|
935
|
+
function preserveExtensionElements(value) {
|
|
936
|
+
if (Array.isArray(value)) {
|
|
937
|
+
value.forEach(preserveExtensionElements);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
if (!value || typeof value !== 'object') {
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
for (const [key, child] of Object.entries(value)) {
|
|
944
|
+
if (key === 'other_element') {
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
if (key.includes(':') && !key.startsWith('xmlns')) {
|
|
948
|
+
appendOtherElement(value, ensureExtensionNamespace(key, convertDataNode(canonicalExtensionName(key), child)));
|
|
949
|
+
delete value[key];
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
preserveExtensionElements(child);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
815
955
|
/**
|
|
816
956
|
* Recursively normalise ``<data>`` elements that contain inline XML.
|
|
817
957
|
*
|
|
@@ -855,12 +995,19 @@ function fixDataContent(value) {
|
|
|
855
995
|
* @returns {object} XML builder structure keyed by element name.
|
|
856
996
|
*/
|
|
857
997
|
function restoreDataNode(node) {
|
|
998
|
+
let qname = node.qname;
|
|
858
999
|
const out = {};
|
|
859
1000
|
if (node.attributes) {
|
|
860
1001
|
for (const [k, v] of Object.entries(node.attributes)) {
|
|
861
1002
|
out[`@_${k}`] = v;
|
|
862
1003
|
}
|
|
863
1004
|
}
|
|
1005
|
+
if (qname === XINCLUDE_CLARK_INCLUDE) {
|
|
1006
|
+
qname = 'xi:include';
|
|
1007
|
+
if (out['@_xmlns:xi'] === undefined) {
|
|
1008
|
+
out['@_xmlns:xi'] = XINCLUDE_NS;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
864
1011
|
if (node.text !== undefined && node.text !== '') {
|
|
865
1012
|
out['#text'] = node.text;
|
|
866
1013
|
}
|
|
@@ -881,10 +1028,10 @@ function restoreDataNode(node) {
|
|
|
881
1028
|
}
|
|
882
1029
|
});
|
|
883
1030
|
}
|
|
884
|
-
if (!
|
|
1031
|
+
if (!qname.includes(':') && !qname.startsWith('{') && qname !== 'scxml') {
|
|
885
1032
|
out['@_xmlns'] = '';
|
|
886
1033
|
}
|
|
887
|
-
return { [
|
|
1034
|
+
return { [qname]: out };
|
|
888
1035
|
}
|
|
889
1036
|
/**
|
|
890
1037
|
* Remove namespace URIs from ``qname`` fields.
|
|
@@ -899,7 +1046,9 @@ function stripQnameNs(value) {
|
|
|
899
1046
|
if (value && typeof value === 'object') {
|
|
900
1047
|
for (const [k, v] of Object.entries(value)) {
|
|
901
1048
|
if (k === 'qname' && typeof v === 'string') {
|
|
902
|
-
|
|
1049
|
+
if (v !== XINCLUDE_CLARK_INCLUDE) {
|
|
1050
|
+
value[k] = v.replace(/^\{[^}]+\}/, '');
|
|
1051
|
+
}
|
|
903
1052
|
continue;
|
|
904
1053
|
}
|
|
905
1054
|
stripQnameNs(v);
|
|
@@ -1046,18 +1195,60 @@ function stripNestedDataAttrs(value) {
|
|
|
1046
1195
|
}
|
|
1047
1196
|
}
|
|
1048
1197
|
}
|
|
1049
|
-
|
|
1198
|
+
/**
|
|
1199
|
+
* @typedef {object} XmlToJsonOptions
|
|
1200
|
+
* @property {boolean} [omitEmpty=true] Drop null and empty containers.
|
|
1201
|
+
* @property {'preserve'|'resolve'} [xinclude='preserve'] Preserve
|
|
1202
|
+
* ``xi:include`` as extension content or resolve includes before conversion.
|
|
1203
|
+
* @property {(href: string) => string} [xincludeLoader] Loader used by
|
|
1204
|
+
* resolved XInclude mode.
|
|
1205
|
+
* @property {string} [xincludeBasePath] File or directory used by the default
|
|
1206
|
+
* Node loader in resolved XInclude mode.
|
|
1207
|
+
*/
|
|
1208
|
+
/**
|
|
1209
|
+
* Convert SCXML to canonical SCJSON.
|
|
1210
|
+
*
|
|
1211
|
+
* @param {string} xmlStr - SCXML source.
|
|
1212
|
+
* @param {boolean|XmlToJsonOptions} [options=true] Backward-compatible boolean
|
|
1213
|
+
* ``omitEmpty`` flag or expanded converter options.
|
|
1214
|
+
* @returns {{result: string, valid: boolean, errors: object[]|null}} Conversion outcome.
|
|
1215
|
+
*/
|
|
1216
|
+
function xmlToJson(xmlStr, options = true) {
|
|
1217
|
+
const xmlOptions = normaliseXmlToJsonOptions(options);
|
|
1218
|
+
if (!['preserve', 'resolve'].includes(xmlOptions.xinclude)) {
|
|
1219
|
+
throw new Error("xinclude must be 'preserve' or 'resolve'");
|
|
1220
|
+
}
|
|
1221
|
+
// CONV-F pre-pass: harvest XML comments into deterministic address ->
|
|
1222
|
+
// help_text lists and continue the rest of the pipeline on a
|
|
1223
|
+
// comment-free XML string. The pre-pass uses local element names so
|
|
1224
|
+
// addresses survive the namespace-attribute insertion below.
|
|
1225
|
+
let promotionAddressMap = null;
|
|
1226
|
+
try {
|
|
1227
|
+
const promo = extractHelpTextFromXml(xmlStr);
|
|
1228
|
+
if (promo && promo.addressMap && promo.addressMap.size > 0) {
|
|
1229
|
+
promotionAddressMap = promo.addressMap;
|
|
1230
|
+
xmlStr = promo.cleanedXml;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
catch (_promotionErr) {
|
|
1234
|
+
// Pre-pass failures fall through to the existing parser, which will
|
|
1235
|
+
// surface a descriptive error if the XML itself is malformed.
|
|
1236
|
+
}
|
|
1050
1237
|
const parser = new XMLParser({
|
|
1051
1238
|
ignoreAttributes: false,
|
|
1052
1239
|
trimValues: false,
|
|
1053
1240
|
parseTagValue: false,
|
|
1054
1241
|
});
|
|
1055
1242
|
let obj = parser.parse(xmlStr);
|
|
1243
|
+
if (xmlOptions.xinclude === 'resolve') {
|
|
1244
|
+
obj = resolveXIncludesInParsed(obj, parser, xmlOptions);
|
|
1245
|
+
}
|
|
1056
1246
|
if (obj.scxml) {
|
|
1057
1247
|
obj = obj.scxml;
|
|
1058
1248
|
}
|
|
1059
1249
|
obj = normaliseKeys(obj);
|
|
1060
1250
|
obj = decodeEntities(obj);
|
|
1251
|
+
preserveExtensionElements(obj);
|
|
1061
1252
|
fixNestedScxml(obj);
|
|
1062
1253
|
fixEmptyElse(obj);
|
|
1063
1254
|
obj = collapseWhitespace(obj);
|
|
@@ -1075,7 +1266,7 @@ function xmlToJson(xmlStr, omitEmpty = true) {
|
|
|
1075
1266
|
flattenContent(obj);
|
|
1076
1267
|
stripRootTransitions(obj);
|
|
1077
1268
|
obj = collapseWhitespace(obj);
|
|
1078
|
-
if (omitEmpty) {
|
|
1269
|
+
if (xmlOptions.omitEmpty) {
|
|
1079
1270
|
obj = removeEmpty(obj) || {};
|
|
1080
1271
|
}
|
|
1081
1272
|
if (obj.initial_attribute !== undefined && obj.initial === undefined) {
|
|
@@ -1116,13 +1307,24 @@ function xmlToJson(xmlStr, omitEmpty = true) {
|
|
|
1116
1307
|
stripXmlns(obj);
|
|
1117
1308
|
const valid = validate(obj);
|
|
1118
1309
|
const errors = valid ? null : validate.errors;
|
|
1119
|
-
if (omitEmpty) {
|
|
1310
|
+
if (xmlOptions.omitEmpty) {
|
|
1120
1311
|
obj = removeEmpty(obj) || {};
|
|
1121
1312
|
fixDataContent(obj);
|
|
1122
1313
|
stripQnameNs(obj);
|
|
1123
1314
|
stripNestedDataAttrs(obj);
|
|
1124
1315
|
stripXmlns(obj);
|
|
1125
1316
|
}
|
|
1317
|
+
// CONV-F: attach promoted help_text after the model surface is stable
|
|
1318
|
+
// and after removeEmpty() so empty arrays from earlier passes do not
|
|
1319
|
+
// clobber what we add here.
|
|
1320
|
+
if (promotionAddressMap !== null) {
|
|
1321
|
+
try {
|
|
1322
|
+
attachHelpTextToModel(obj, promotionAddressMap);
|
|
1323
|
+
}
|
|
1324
|
+
catch (_attachErr) {
|
|
1325
|
+
// Promotion attach is best-effort; converter output remains valid.
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1126
1328
|
let out = JSON.stringify(obj, null, 2);
|
|
1127
1329
|
out = out.replace(/"version": 1(?=[,\n])/g, '"version": 1.0');
|
|
1128
1330
|
return { result: out, valid, errors };
|
|
@@ -1167,7 +1369,7 @@ function jsonToXml(jsonStr) {
|
|
|
1167
1369
|
if (Object.prototype.hasOwnProperty.call(value, 'qname')) {
|
|
1168
1370
|
return restoreDataNode(value);
|
|
1169
1371
|
}
|
|
1170
|
-
if (Object.keys(value).every(k => k === 'content' || k.endsWith('_value') || k === 'location' || k === 'expr' || k === 'src') &&
|
|
1372
|
+
if (Object.keys(value).every(k => k === 'content' || k === 'help_text' || k.endsWith('_value') || k === 'location' || k === 'expr' || k === 'src') &&
|
|
1171
1373
|
Array.isArray(value.content) &&
|
|
1172
1374
|
value.content.length === 1 &&
|
|
1173
1375
|
value.content[0] &&
|
|
@@ -1179,7 +1381,7 @@ function jsonToXml(jsonStr) {
|
|
|
1179
1381
|
value.content[0].datamodel_attribute !== undefined)) {
|
|
1180
1382
|
const outObj = {};
|
|
1181
1383
|
for (const [k, v] of Object.entries(value)) {
|
|
1182
|
-
if (k !== 'content') {
|
|
1384
|
+
if (k !== 'content' && k !== 'help_text') {
|
|
1183
1385
|
outObj[k.startsWith('@_') ? k : `@_${k}`] = v;
|
|
1184
1386
|
}
|
|
1185
1387
|
}
|
|
@@ -1206,6 +1408,31 @@ function jsonToXml(jsonStr) {
|
|
|
1206
1408
|
}
|
|
1207
1409
|
continue;
|
|
1208
1410
|
}
|
|
1411
|
+
if (nk === 'help_text') {
|
|
1412
|
+
// CONV-E: help_text is first-class authoring metadata, not an SCXML
|
|
1413
|
+
// attribute or element. CONV-F injects it as leading XML comments
|
|
1414
|
+
// after XMLBuilder emits the comment-free structural tree.
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1417
|
+
if (nk === 'other_element') {
|
|
1418
|
+
const elements = Array.isArray(v) ? v : [v];
|
|
1419
|
+
elements.forEach(item => {
|
|
1420
|
+
const r = restoreDataNode(item);
|
|
1421
|
+
const [ck, cv] = Object.entries(r)[0];
|
|
1422
|
+
if (out[ck]) {
|
|
1423
|
+
if (Array.isArray(out[ck])) {
|
|
1424
|
+
out[ck].push(cv);
|
|
1425
|
+
}
|
|
1426
|
+
else {
|
|
1427
|
+
out[ck] = [out[ck], cv];
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
else {
|
|
1431
|
+
out[ck] = cv;
|
|
1432
|
+
}
|
|
1433
|
+
});
|
|
1434
|
+
continue;
|
|
1435
|
+
}
|
|
1209
1436
|
for (const [attr, prop] of Object.entries(ATTRIBUTE_MAP)) {
|
|
1210
1437
|
if (prop === nk) {
|
|
1211
1438
|
nk = `@_${attr}`;
|
|
@@ -1343,7 +1570,18 @@ function jsonToXml(jsonStr) {
|
|
|
1343
1570
|
if (cleaned['@_xmlns'] === undefined) {
|
|
1344
1571
|
cleaned['@_xmlns'] = 'http://www.w3.org/2005/07/scxml';
|
|
1345
1572
|
}
|
|
1346
|
-
|
|
1573
|
+
let xmlOut = builder.build({ scxml: cleaned });
|
|
1574
|
+
// CONV-F post-pass: inject leading XML comments for every model element
|
|
1575
|
+
// with a non-empty help_text array. ``obj`` (the input model) is the
|
|
1576
|
+
// source of truth for help_text; the XMLBuilder above already stripped
|
|
1577
|
+
// it via the ``nk === 'help_text'`` guard.
|
|
1578
|
+
try {
|
|
1579
|
+
xmlOut = injectHelpTextCommentsIntoXml(xmlOut, obj);
|
|
1580
|
+
}
|
|
1581
|
+
catch (_postPassErr) {
|
|
1582
|
+
// Best-effort: emit existing XML if injection fails.
|
|
1583
|
+
}
|
|
1584
|
+
return { result: xmlOut, valid, errors };
|
|
1347
1585
|
}
|
|
1348
1586
|
module.exports = {
|
|
1349
1587
|
xmlToJson,
|
|
@@ -1357,6 +1595,9 @@ module.exports = {
|
|
|
1357
1595
|
fixSendDefaults,
|
|
1358
1596
|
fixSendContent,
|
|
1359
1597
|
fixDonedataContent,
|
|
1598
|
+
normaliseXmlToJsonOptions,
|
|
1599
|
+
resolveXIncludesInParsed,
|
|
1600
|
+
preserveExtensionElements,
|
|
1360
1601
|
fixOtherAttributes,
|
|
1361
1602
|
decodeEntities,
|
|
1362
1603
|
restoreDataNode,
|
|
@@ -1369,4 +1610,9 @@ module.exports = {
|
|
|
1369
1610
|
reorderScxml,
|
|
1370
1611
|
stripNestedDataAttrs,
|
|
1371
1612
|
stripXmlns,
|
|
1613
|
+
ARRAY_KEYS,
|
|
1614
|
+
STRUCTURAL_METADATA_KEYS,
|
|
1615
|
+
extractHelpTextFromXml,
|
|
1616
|
+
attachHelpTextToModel,
|
|
1617
|
+
injectHelpTextCommentsIntoXml,
|
|
1372
1618
|
};
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scjson",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "A JSON-based serialization of SCXML (State Chart XML) with SCXML/SCML execution tooling and converters.",
|
|
5
5
|
"author": "Softoboros Technology Inc. <ira@softoboros.com>",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/SoftOboros/scjson"
|
|
8
|
+
"url": "git+https://github.com/SoftOboros/scjson.git"
|
|
9
9
|
},
|
|
10
10
|
"license": "BSD-1-Clause",
|
|
11
11
|
"main": "dist/index.js",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"fast-xml-parser": "^5.2.5"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
|
-
"scion-core": "
|
|
70
|
+
"scion-core": "*"
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
73
|
"@babel/parser": "^7.28.0",
|