@ifc-lite/ids 1.14.11 → 1.15.2
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/audit/coherence/index.d.ts +12 -0
- package/dist/audit/coherence/index.d.ts.map +1 -0
- package/dist/audit/coherence/index.js +392 -0
- package/dist/audit/coherence/index.js.map +1 -0
- package/dist/audit/coherence/regex.d.ts +57 -0
- package/dist/audit/coherence/regex.d.ts.map +1 -0
- package/dist/audit/coherence/regex.js +111 -0
- package/dist/audit/coherence/regex.js.map +1 -0
- package/dist/audit/ifc-schema/index.d.ts +4 -0
- package/dist/audit/ifc-schema/index.d.ts.map +1 -0
- package/dist/audit/ifc-schema/index.js +637 -0
- package/dist/audit/ifc-schema/index.js.map +1 -0
- package/dist/audit/index.d.ts +37 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +60 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/audit/parser-shim.d.ts +13 -0
- package/dist/audit/parser-shim.d.ts.map +1 -0
- package/dist/audit/parser-shim.js +66 -0
- package/dist/audit/parser-shim.js.map +1 -0
- package/dist/audit/structural/index.d.ts +22 -0
- package/dist/audit/structural/index.d.ts.map +1 -0
- package/dist/audit/structural/index.js +441 -0
- package/dist/audit/structural/index.js.map +1 -0
- package/dist/audit/types.d.ts +91 -0
- package/dist/audit/types.d.ts.map +1 -0
- package/dist/audit/types.js +5 -0
- package/dist/audit/types.js.map +1 -0
- package/dist/audit/xsd/index.d.ts +13 -0
- package/dist/audit/xsd/index.d.ts.map +1 -0
- package/dist/audit/xsd/index.js +203 -0
- package/dist/audit/xsd/index.js.map +1 -0
- package/dist/bridge/classifications.d.ts +15 -0
- package/dist/bridge/classifications.d.ts.map +1 -0
- package/dist/bridge/classifications.js +114 -0
- package/dist/bridge/classifications.js.map +1 -0
- package/dist/bridge/data-accessor.d.ts +16 -0
- package/dist/bridge/data-accessor.d.ts.map +1 -0
- package/dist/bridge/data-accessor.js +205 -0
- package/dist/bridge/data-accessor.js.map +1 -0
- package/dist/bridge/data-types.d.ts +11 -0
- package/dist/bridge/data-types.d.ts.map +1 -0
- package/dist/bridge/data-types.js +47 -0
- package/dist/bridge/data-types.js.map +1 -0
- package/dist/bridge/index.d.ts +17 -0
- package/dist/bridge/index.d.ts.map +1 -0
- package/dist/bridge/index.js +20 -0
- package/dist/bridge/index.js.map +1 -0
- package/dist/bridge/materials.d.ts +11 -0
- package/dist/bridge/materials.d.ts.map +1 -0
- package/dist/bridge/materials.js +68 -0
- package/dist/bridge/materials.js.map +1 -0
- package/dist/bridge/predefined-types.d.ts +19 -0
- package/dist/bridge/predefined-types.d.ts.map +1 -0
- package/dist/bridge/predefined-types.js +84 -0
- package/dist/bridge/predefined-types.js.map +1 -0
- package/dist/bridge/properties.d.ts +20 -0
- package/dist/bridge/properties.d.ts.map +1 -0
- package/dist/bridge/properties.js +168 -0
- package/dist/bridge/properties.js.map +1 -0
- package/dist/bridge/schema-version.d.ts +10 -0
- package/dist/bridge/schema-version.d.ts.map +1 -0
- package/dist/bridge/schema-version.js +25 -0
- package/dist/bridge/schema-version.js.map +1 -0
- package/dist/bridge/units.d.ts +18 -0
- package/dist/bridge/units.d.ts.map +1 -0
- package/dist/bridge/units.js +55 -0
- package/dist/bridge/units.js.map +1 -0
- package/dist/constraints/comparators.d.ts +32 -0
- package/dist/constraints/comparators.d.ts.map +1 -0
- package/dist/constraints/comparators.js +94 -0
- package/dist/constraints/comparators.js.map +1 -0
- package/dist/constraints/index.d.ts.map +1 -1
- package/dist/constraints/index.js +68 -54
- package/dist/constraints/index.js.map +1 -1
- package/dist/constraints/xsd-cast.d.ts +20 -0
- package/dist/constraints/xsd-cast.d.ts.map +1 -0
- package/dist/constraints/xsd-cast.js +89 -0
- package/dist/constraints/xsd-cast.js.map +1 -0
- package/dist/facets/attribute-facet.d.ts.map +1 -1
- package/dist/facets/attribute-facet.js +51 -4
- package/dist/facets/attribute-facet.js.map +1 -1
- package/dist/facets/entity-facet.d.ts.map +1 -1
- package/dist/facets/entity-facet.js +55 -5
- package/dist/facets/entity-facet.js.map +1 -1
- package/dist/facets/partof-facet.d.ts.map +1 -1
- package/dist/facets/partof-facet.js +33 -3
- package/dist/facets/partof-facet.js.map +1 -1
- package/dist/facets/property-facet.d.ts.map +1 -1
- package/dist/facets/property-facet.js +119 -25
- package/dist/facets/property-facet.js.map +1 -1
- package/dist/facets/test-helpers.d.ts +7 -1
- package/dist/facets/test-helpers.d.ts.map +1 -1
- package/dist/facets/test-helpers.js +4 -0
- package/dist/facets/test-helpers.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/parser/xml-parser.d.ts.map +1 -1
- package/dist/parser/xml-parser.js +166 -64
- package/dist/parser/xml-parser.js.map +1 -1
- package/dist/translation/locales/de.d.ts +1 -0
- package/dist/translation/locales/de.d.ts.map +1 -1
- package/dist/translation/locales/de.js +1 -0
- package/dist/translation/locales/de.js.map +1 -1
- package/dist/translation/locales/en.d.ts +1 -0
- package/dist/translation/locales/en.d.ts.map +1 -1
- package/dist/translation/locales/en.js +1 -0
- package/dist/translation/locales/en.js.map +1 -1
- package/dist/translation/locales/fr.d.ts +1 -0
- package/dist/translation/locales/fr.d.ts.map +1 -1
- package/dist/translation/locales/fr.js +1 -0
- package/dist/translation/locales/fr.js.map +1 -1
- package/dist/types.d.ts +115 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/validation/validator.js +45 -3
- package/dist/validation/validator.js.map +1 -1
- package/package.json +7 -2
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IDS document auditing.
|
|
3
|
+
*
|
|
4
|
+
* Authoring tools (`ids-flow`, `ids-light-editor`) historically delegated
|
|
5
|
+
* IDS-document validation to buildingSMART's `IfcTester-Service` HTTP
|
|
6
|
+
* endpoint. This module brings that capability in-process so consumers
|
|
7
|
+
* can drop the network round-trip.
|
|
8
|
+
*
|
|
9
|
+
* Three layers of checks run by default:
|
|
10
|
+
* 1. Permissive parse — a wrapper around `parseIDS` that turns each
|
|
11
|
+
* `IDSParseError` into a structured issue.
|
|
12
|
+
* 2. XSD-level checks — required attributes, enum membership, structural
|
|
13
|
+
* shape (cf. `ids.xsd`).
|
|
14
|
+
* 3. IFC schema cross-checks — entities, predefined types, property
|
|
15
|
+
* sets/properties, attribute names against the seed dataset in
|
|
16
|
+
* `@ifc-lite/data`.
|
|
17
|
+
* 4. Restriction & cardinality coherence — empty enumerations, inverted
|
|
18
|
+
* bounds, regex patterns that don't compile, etc.
|
|
19
|
+
*
|
|
20
|
+
* All four are independently togglable via `IDSAuditOptions`.
|
|
21
|
+
*/
|
|
22
|
+
import type { IDSDocument } from '../types.js';
|
|
23
|
+
import type { IDSAuditOptions, IDSAuditReport } from './types.js';
|
|
24
|
+
export type { IDSAuditCode, IDSAuditIssue, IDSAuditOptions, IDSAuditReport, IDSAuditSeverity, } from './types.js';
|
|
25
|
+
/**
|
|
26
|
+
* Audit an IDS XML document. The report aggregates issues from every
|
|
27
|
+
* enabled check phase. Even when parsing fails, the returned report
|
|
28
|
+
* contains the parse error as a structured issue.
|
|
29
|
+
*/
|
|
30
|
+
export declare function auditIDSDocument(xml: string | ArrayBuffer, options?: IDSAuditOptions): Promise<IDSAuditReport>;
|
|
31
|
+
/**
|
|
32
|
+
* Audit an already-parsed IDS document. Skips the parse step — useful
|
|
33
|
+
* when the caller already has an `IDSDocument` (e.g., from an in-app
|
|
34
|
+
* editor that mutates the structure directly).
|
|
35
|
+
*/
|
|
36
|
+
export declare function auditIDSStructure(doc: IDSDocument, options?: IDSAuditOptions): Promise<IDSAuditReport>;
|
|
37
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/audit/index.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK/C,OAAO,KAAK,EAEV,eAAe,EACf,cAAc,EACf,MAAM,YAAY,CAAC;AAGpB,YAAY,EACV,YAAY,EACZ,aAAa,EACb,eAAe,EACf,cAAc,EACd,gBAAgB,GACjB,MAAM,YAAY,CAAC;AAEpB;;;;GAIG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,MAAM,GAAG,WAAW,EACzB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,cAAc,CAAC,CAiBzB;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,WAAW,EAChB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,cAAc,CAAC,CAczB"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
import { runCoherenceAudit } from './coherence/index.js';
|
|
5
|
+
import { runIfcSchemaAudit } from './ifc-schema/index.js';
|
|
6
|
+
import { permissiveParse } from './parser-shim.js';
|
|
7
|
+
import { runStructuralAudit } from './structural/index.js';
|
|
8
|
+
import { runXsdAudit } from './xsd/index.js';
|
|
9
|
+
/**
|
|
10
|
+
* Audit an IDS XML document. The report aggregates issues from every
|
|
11
|
+
* enabled check phase. Even when parsing fails, the returned report
|
|
12
|
+
* contains the parse error as a structured issue.
|
|
13
|
+
*/
|
|
14
|
+
export async function auditIDSDocument(xml, options = {}) {
|
|
15
|
+
const { document, issues } = permissiveParse(xml);
|
|
16
|
+
if (!document) {
|
|
17
|
+
return finalise(issues);
|
|
18
|
+
}
|
|
19
|
+
// The structural shape walk needs the raw XML DOM, so it sits in the
|
|
20
|
+
// top-level entry point (alongside parse), not in
|
|
21
|
+
// `auditIDSStructure(parsed)`.
|
|
22
|
+
const structuralIssues = options.xsdValidation === false
|
|
23
|
+
? []
|
|
24
|
+
: await runStructuralAudit(xml);
|
|
25
|
+
const downstream = await auditIDSStructure(document, options);
|
|
26
|
+
return finalise([...issues, ...structuralIssues, ...downstream.issues], document);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Audit an already-parsed IDS document. Skips the parse step — useful
|
|
30
|
+
* when the caller already has an `IDSDocument` (e.g., from an in-app
|
|
31
|
+
* editor that mutates the structure directly).
|
|
32
|
+
*/
|
|
33
|
+
export async function auditIDSStructure(doc, options = {}) {
|
|
34
|
+
const xsdValidation = options.xsdValidation !== false;
|
|
35
|
+
const ifcSchemaChecks = options.ifcSchemaChecks !== false;
|
|
36
|
+
const coherenceChecks = options.coherenceChecks !== false;
|
|
37
|
+
const issues = [];
|
|
38
|
+
if (xsdValidation)
|
|
39
|
+
issues.push(...runXsdAudit(doc));
|
|
40
|
+
if (coherenceChecks)
|
|
41
|
+
issues.push(...runCoherenceAudit(doc));
|
|
42
|
+
if (ifcSchemaChecks) {
|
|
43
|
+
issues.push(...(await runIfcSchemaAudit(doc, { ifcVersion: options.ifcVersion })));
|
|
44
|
+
}
|
|
45
|
+
return finalise(issues, doc);
|
|
46
|
+
}
|
|
47
|
+
function finalise(issues, parsedDocument) {
|
|
48
|
+
let status = 'valid';
|
|
49
|
+
for (const issue of issues) {
|
|
50
|
+
if (issue.severity === 'error') {
|
|
51
|
+
status = 'error';
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
if (issue.severity === 'warning' || issue.severity === 'info') {
|
|
55
|
+
status = 'warning';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { status, issues, parsedDocument };
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/audit/index.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAyB/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAM3D,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAU7C;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,GAAyB,EACzB,UAA2B,EAAE;IAE7B,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;IAClD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC;IAC1B,CAAC;IACD,qEAAqE;IACrE,kDAAkD;IAClD,+BAA+B;IAC/B,MAAM,gBAAgB,GACpB,OAAO,CAAC,aAAa,KAAK,KAAK;QAC7B,CAAC,CAAC,EAAE;QACJ,CAAC,CAAC,MAAM,kBAAkB,CAAC,GAAG,CAAC,CAAC;IACpC,MAAM,UAAU,GAAG,MAAM,iBAAiB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAC9D,OAAO,QAAQ,CACb,CAAC,GAAG,MAAM,EAAE,GAAG,gBAAgB,EAAE,GAAG,UAAU,CAAC,MAAM,CAAC,EACtD,QAAQ,CACT,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,GAAgB,EAChB,UAA2B,EAAE;IAE7B,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,KAAK,KAAK,CAAC;IACtD,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,KAAK,KAAK,CAAC;IAC1D,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,KAAK,KAAK,CAAC;IAE1D,MAAM,MAAM,GAAoB,EAAE,CAAC;IACnC,IAAI,aAAa;QAAE,MAAM,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;IACpD,IAAI,eAAe;QAAE,MAAM,CAAC,IAAI,CAAC,GAAG,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5D,IAAI,eAAe,EAAE,CAAC;QACpB,MAAM,CAAC,IAAI,CACT,GAAG,CAAC,MAAM,iBAAiB,CAAC,GAAG,EAAE,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,CACtE,CAAC;IACJ,CAAC;IACD,OAAO,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAC/B,CAAC;AAED,SAAS,QAAQ,CACf,MAAuB,EACvB,cAA4B;IAE5B,IAAI,MAAM,GAA6B,OAAO,CAAC;IAC/C,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,KAAK,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YAC/B,MAAM,GAAG,OAAO,CAAC;YACjB,MAAM;QACR,CAAC;QACD,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS,IAAI,KAAK,CAAC,QAAQ,KAAK,MAAM,EAAE,CAAC;YAC9D,MAAM,GAAG,SAAS,CAAC;QACrB,CAAC;IACH,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;AAC5C,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { IDSDocument } from '../types.js';
|
|
2
|
+
import type { IDSAuditIssue } from './types.js';
|
|
3
|
+
export interface PermissiveParseResult {
|
|
4
|
+
document?: IDSDocument;
|
|
5
|
+
issues: IDSAuditIssue[];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Try to parse `xml`. On success, returns the document with no issues. On
|
|
9
|
+
* failure, returns an empty issue list with the original `IDSParseError`
|
|
10
|
+
* mapped onto a structured `IDSAuditIssue`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function permissiveParse(xml: string | ArrayBuffer): PermissiveParseResult;
|
|
13
|
+
//# sourceMappingURL=parser-shim.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parser-shim.d.ts","sourceRoot":"","sources":["../../src/audit/parser-shim.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,EAAE,WAAW,CAAC;IACvB,MAAM,EAAE,aAAa,EAAE,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,MAAM,GAAG,WAAW,GACxB,qBAAqB,CAOvB"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
/**
|
|
5
|
+
* Permissive parse layer for the auditor.
|
|
6
|
+
*
|
|
7
|
+
* `parseIDS` is intentionally strict — it throws on the first structural
|
|
8
|
+
* problem so application code never sees a half-parsed document. The audit
|
|
9
|
+
* use case is the opposite: we want to surface every issue at once, and
|
|
10
|
+
* keep going even when individual specs fail to parse. This module wraps
|
|
11
|
+
* `parseIDS` and converts an `IDSParseError` into an `IDSAuditIssue`.
|
|
12
|
+
*
|
|
13
|
+
* For richer salvaging (e.g., parsing each `<specification>` independently
|
|
14
|
+
* so one bad spec doesn't sink the whole document), see Phase 2+ — for now
|
|
15
|
+
* we collect a single parse error and stop.
|
|
16
|
+
*/
|
|
17
|
+
import { IDSParseError, parseIDS } from '../parser/xml-parser.js';
|
|
18
|
+
/**
|
|
19
|
+
* Try to parse `xml`. On success, returns the document with no issues. On
|
|
20
|
+
* failure, returns an empty issue list with the original `IDSParseError`
|
|
21
|
+
* mapped onto a structured `IDSAuditIssue`.
|
|
22
|
+
*/
|
|
23
|
+
export function permissiveParse(xml) {
|
|
24
|
+
try {
|
|
25
|
+
const document = parseIDS(xml);
|
|
26
|
+
return { document, issues: [] };
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
return { issues: [parseErrorToIssue(err)] };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function parseErrorToIssue(err) {
|
|
33
|
+
if (err instanceof IDSParseError) {
|
|
34
|
+
const message = err.details
|
|
35
|
+
? `${err.message}: ${err.details}`
|
|
36
|
+
: err.message;
|
|
37
|
+
// Heuristic mapping — distinguish the few error sites in `parseIDS` so
|
|
38
|
+
// consumers can dispatch on `code`. The parser throws three families
|
|
39
|
+
// of errors, all surfaced as `IDSParseError`.
|
|
40
|
+
let code = 'E_PARSE_UNKNOWN';
|
|
41
|
+
if (err.message.includes('Failed to parse IDS XML') ||
|
|
42
|
+
err.message.includes('Invalid XML format') ||
|
|
43
|
+
err.message.includes('No DOMParser')) {
|
|
44
|
+
code = 'E_PARSE_XML';
|
|
45
|
+
}
|
|
46
|
+
else if (err.message.includes('Invalid root element')) {
|
|
47
|
+
code = 'E_PARSE_ROOT';
|
|
48
|
+
}
|
|
49
|
+
else if (err.message.includes('facet')) {
|
|
50
|
+
code = 'E_PARSE_FACET';
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
severity: 'error',
|
|
54
|
+
code,
|
|
55
|
+
message,
|
|
56
|
+
path: '',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
severity: 'error',
|
|
61
|
+
code: 'E_PARSE_UNKNOWN',
|
|
62
|
+
message: err instanceof Error ? err.message : String(err),
|
|
63
|
+
path: '',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=parser-shim.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parser-shim.js","sourceRoot":"","sources":["../../src/audit/parser-shim.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AASlE;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAC7B,GAAyB;IAEzB,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC/B,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IAClC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,EAAE,MAAM,EAAE,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;IAC9C,CAAC;AACH,CAAC;AAED,SAAS,iBAAiB,CAAC,GAAY;IACrC,IAAI,GAAG,YAAY,aAAa,EAAE,CAAC;QACjC,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO;YACzB,CAAC,CAAC,GAAG,GAAG,CAAC,OAAO,KAAK,GAAG,CAAC,OAAO,EAAE;YAClC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC;QAChB,uEAAuE;QACvE,qEAAqE;QACrE,8CAA8C;QAC9C,IAAI,IAAI,GAA0B,iBAAiB,CAAC;QACpD,IACE,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAAC;YAC/C,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAC;YAC1C,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,EACpC,CAAC;YACD,IAAI,GAAG,aAAa,CAAC;QACvB,CAAC;aAAM,IAAI,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAC,EAAE,CAAC;YACxD,IAAI,GAAG,cAAc,CAAC;QACxB,CAAC;aAAM,IAAI,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YACzC,IAAI,GAAG,eAAe,CAAC;QACzB,CAAC;QACD,OAAO;YACL,QAAQ,EAAE,OAAO;YACjB,IAAI;YACJ,OAAO;YACP,IAAI,EAAE,EAAE;SACT,CAAC;IACJ,CAAC;IACD,OAAO;QACL,QAAQ,EAAE,OAAO;QACjB,IAAI,EAAE,iBAAiB;QACvB,OAAO,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;QACzD,IAAI,EAAE,EAAE;KACT,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural shape audit — walks the raw IDS XML DOM and verifies every
|
|
3
|
+
* element/attribute against the IDS 1.0 XSD's element shapes.
|
|
4
|
+
*
|
|
5
|
+
* This is the precise contract upstream IDS-Audit-tool encodes in
|
|
6
|
+
* `IdsXmlNode.cs` / `IdsRootElement.cs`: rather than running a generic
|
|
7
|
+
* XSD validator, the auditor walks each known element and rejects
|
|
8
|
+
* attributes/children that aren't part of that element's signature.
|
|
9
|
+
*
|
|
10
|
+
* The shapes encoded here mirror `Resources/XsdSchemas/ids.xsd`. When
|
|
11
|
+
* upstream evolves the IDS schema, regenerate this table from the XSD.
|
|
12
|
+
*/
|
|
13
|
+
import type { IDSAuditIssue } from '../types.js';
|
|
14
|
+
/**
|
|
15
|
+
* Walks the parsed XML root and emits issues for any element/attribute
|
|
16
|
+
* outside the IDS XSD's shape. Re-uses xmldom/native DOMParser for the
|
|
17
|
+
* walk so we have access to the original element tree (the high-level
|
|
18
|
+
* `IDSDocument` shape can't tell us about extra attributes/elements
|
|
19
|
+
* stripped during parsing).
|
|
20
|
+
*/
|
|
21
|
+
export declare function runStructuralAudit(xml: string | ArrayBuffer): Promise<IDSAuditIssue[]>;
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/audit/structural/index.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AA0MjD;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,MAAM,GAAG,WAAW,GACxB,OAAO,CAAC,aAAa,EAAE,CAAC,CAoB1B"}
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
const XS_NS = 'http://www.w3.org/2001/XMLSchema';
|
|
5
|
+
// `xmlns`, `xmlns:*`, `xsi:*`, `xml:*` are tolerated as global hooks —
|
|
6
|
+
// they're not specific to any IDS element but XML allows them anywhere.
|
|
7
|
+
const TOLERATED_GLOBAL_ATTR_PREFIXES = new Set(['xmlns', 'xsi', 'xml']);
|
|
8
|
+
const SHAPE_IDS = {
|
|
9
|
+
attrs: [],
|
|
10
|
+
children: ['info', 'specifications'],
|
|
11
|
+
};
|
|
12
|
+
const SHAPE_INFO = {
|
|
13
|
+
attrs: [],
|
|
14
|
+
children: [
|
|
15
|
+
'title',
|
|
16
|
+
'copyright',
|
|
17
|
+
'version',
|
|
18
|
+
'description',
|
|
19
|
+
'author',
|
|
20
|
+
'date',
|
|
21
|
+
'purpose',
|
|
22
|
+
'milestone',
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
const SHAPE_SPECIFICATIONS = {
|
|
26
|
+
attrs: [],
|
|
27
|
+
children: ['specification'],
|
|
28
|
+
};
|
|
29
|
+
const SHAPE_SPECIFICATION = {
|
|
30
|
+
attrs: ['name', 'ifcVersion', 'identifier', 'description', 'instructions'],
|
|
31
|
+
requiredAttrs: ['name', 'ifcVersion'],
|
|
32
|
+
children: ['applicability', 'requirements'],
|
|
33
|
+
};
|
|
34
|
+
const SHAPE_APPLICABILITY = {
|
|
35
|
+
// The XSD attaches the xs:occurs attribute group → minOccurs, maxOccurs.
|
|
36
|
+
attrs: ['minOccurs', 'maxOccurs'],
|
|
37
|
+
children: [
|
|
38
|
+
'entity',
|
|
39
|
+
'partOf',
|
|
40
|
+
'classification',
|
|
41
|
+
'attribute',
|
|
42
|
+
'property',
|
|
43
|
+
'material',
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
const SHAPE_REQUIREMENTS = {
|
|
47
|
+
attrs: ['description'],
|
|
48
|
+
children: [
|
|
49
|
+
'entity',
|
|
50
|
+
'partOf',
|
|
51
|
+
'classification',
|
|
52
|
+
'attribute',
|
|
53
|
+
'property',
|
|
54
|
+
'material',
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
// Per-facet shapes in *applicability* context (no cardinality / uri /
|
|
58
|
+
// instructions) vs *requirements* context (extension types per XSD).
|
|
59
|
+
const SHAPE_ENTITY_BODY = {
|
|
60
|
+
attrs: [],
|
|
61
|
+
children: ['name', 'predefinedType'],
|
|
62
|
+
};
|
|
63
|
+
const SHAPE_ATTRIBUTE_BODY = {
|
|
64
|
+
attrs: [],
|
|
65
|
+
children: ['name', 'value'],
|
|
66
|
+
};
|
|
67
|
+
const SHAPE_CLASSIFICATION_BODY = {
|
|
68
|
+
attrs: [],
|
|
69
|
+
children: ['value', 'system'],
|
|
70
|
+
};
|
|
71
|
+
const SHAPE_PARTOF_BODY = {
|
|
72
|
+
attrs: ['relation'],
|
|
73
|
+
children: ['entity'],
|
|
74
|
+
};
|
|
75
|
+
const SHAPE_PROPERTY_BODY = {
|
|
76
|
+
attrs: ['dataType'],
|
|
77
|
+
children: ['propertySet', 'baseName', 'value'],
|
|
78
|
+
};
|
|
79
|
+
const SHAPE_MATERIAL_BODY = {
|
|
80
|
+
attrs: [],
|
|
81
|
+
children: ['value'],
|
|
82
|
+
};
|
|
83
|
+
// In *requirements* context, the schema extends each facet with
|
|
84
|
+
// cardinality / instructions / uri (and entity gets just instructions).
|
|
85
|
+
function shapeInRequirements(facetTag) {
|
|
86
|
+
const base = facetBaseShape(facetTag);
|
|
87
|
+
switch (facetTag.toLowerCase()) {
|
|
88
|
+
case 'entity':
|
|
89
|
+
return { ...base, attrs: [...base.attrs, 'instructions'] };
|
|
90
|
+
case 'partof':
|
|
91
|
+
return {
|
|
92
|
+
...base,
|
|
93
|
+
attrs: [...base.attrs, 'cardinality', 'instructions'],
|
|
94
|
+
};
|
|
95
|
+
case 'attribute':
|
|
96
|
+
return {
|
|
97
|
+
...base,
|
|
98
|
+
attrs: [...base.attrs, 'cardinality', 'instructions'],
|
|
99
|
+
};
|
|
100
|
+
case 'classification':
|
|
101
|
+
case 'property':
|
|
102
|
+
case 'material':
|
|
103
|
+
return {
|
|
104
|
+
...base,
|
|
105
|
+
attrs: [...base.attrs, 'cardinality', 'instructions', 'uri'],
|
|
106
|
+
};
|
|
107
|
+
default:
|
|
108
|
+
return base;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function facetBaseShape(tag) {
|
|
112
|
+
// Tags arrive lowercased from `localName.toLowerCase()`. Match
|
|
113
|
+
// case-insensitively so `partof` resolves to the partOf shape.
|
|
114
|
+
switch (tag.toLowerCase()) {
|
|
115
|
+
case 'entity':
|
|
116
|
+
return SHAPE_ENTITY_BODY;
|
|
117
|
+
case 'attribute':
|
|
118
|
+
return SHAPE_ATTRIBUTE_BODY;
|
|
119
|
+
case 'classification':
|
|
120
|
+
return SHAPE_CLASSIFICATION_BODY;
|
|
121
|
+
case 'partof':
|
|
122
|
+
return SHAPE_PARTOF_BODY;
|
|
123
|
+
case 'property':
|
|
124
|
+
return SHAPE_PROPERTY_BODY;
|
|
125
|
+
case 'material':
|
|
126
|
+
return SHAPE_MATERIAL_BODY;
|
|
127
|
+
default:
|
|
128
|
+
return { attrs: [], children: [] };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// idsValue (used for <name>, <value>, <baseName>, <propertySet>,
|
|
132
|
+
// <system>, <predefinedType>): choice of simpleValue OR xs:restriction.
|
|
133
|
+
const SHAPE_IDS_VALUE = {
|
|
134
|
+
attrs: [],
|
|
135
|
+
children: ['simpleValue'],
|
|
136
|
+
xsChildren: ['restriction'],
|
|
137
|
+
};
|
|
138
|
+
const SHAPE_SIMPLE_VALUE = {
|
|
139
|
+
attrs: [],
|
|
140
|
+
children: [],
|
|
141
|
+
textOnly: true,
|
|
142
|
+
};
|
|
143
|
+
// xs:restriction child facets the IDS XSD admits.
|
|
144
|
+
const XS_RESTRICTION_FACETS = [
|
|
145
|
+
'enumeration',
|
|
146
|
+
'pattern',
|
|
147
|
+
'minInclusive',
|
|
148
|
+
'maxInclusive',
|
|
149
|
+
'minExclusive',
|
|
150
|
+
'maxExclusive',
|
|
151
|
+
'length',
|
|
152
|
+
'minLength',
|
|
153
|
+
'maxLength',
|
|
154
|
+
'totalDigits',
|
|
155
|
+
'fractionDigits',
|
|
156
|
+
'whiteSpace',
|
|
157
|
+
];
|
|
158
|
+
const SHAPE_XS_RESTRICTION = {
|
|
159
|
+
attrs: ['base'],
|
|
160
|
+
children: [],
|
|
161
|
+
xsChildren: XS_RESTRICTION_FACETS,
|
|
162
|
+
};
|
|
163
|
+
// Each xs:enumeration/xs:pattern/etc carries a `value` attribute.
|
|
164
|
+
const SHAPE_XS_FACET = {
|
|
165
|
+
attrs: ['value'],
|
|
166
|
+
children: [],
|
|
167
|
+
textOnly: true,
|
|
168
|
+
};
|
|
169
|
+
/**
|
|
170
|
+
* Walks the parsed XML root and emits issues for any element/attribute
|
|
171
|
+
* outside the IDS XSD's shape. Re-uses xmldom/native DOMParser for the
|
|
172
|
+
* walk so we have access to the original element tree (the high-level
|
|
173
|
+
* `IDSDocument` shape can't tell us about extra attributes/elements
|
|
174
|
+
* stripped during parsing).
|
|
175
|
+
*/
|
|
176
|
+
export async function runStructuralAudit(xml) {
|
|
177
|
+
const issues = [];
|
|
178
|
+
const xmlString = typeof xml === 'string' ? xml : new TextDecoder().decode(xml);
|
|
179
|
+
// BOM strip — same fix as parser.
|
|
180
|
+
const bomStripped = xmlString.charCodeAt(0) === 0xfeff ? xmlString.slice(1) : xmlString;
|
|
181
|
+
const parser = await getParser();
|
|
182
|
+
let doc;
|
|
183
|
+
try {
|
|
184
|
+
doc = parser.parseFromString(bomStripped, 'text/xml');
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// Parse failure already surfaced by the permissive parser.
|
|
188
|
+
return issues;
|
|
189
|
+
}
|
|
190
|
+
const root = doc.documentElement;
|
|
191
|
+
if (!root || (root.localName ?? '').toLowerCase() !== 'ids')
|
|
192
|
+
return issues;
|
|
193
|
+
walkIds(root, issues);
|
|
194
|
+
return issues;
|
|
195
|
+
}
|
|
196
|
+
let parserPromise;
|
|
197
|
+
function getParser() {
|
|
198
|
+
if (parserPromise)
|
|
199
|
+
return parserPromise;
|
|
200
|
+
parserPromise = (async () => {
|
|
201
|
+
const browserCtor = globalThis.DOMParser;
|
|
202
|
+
if (typeof browserCtor === 'function')
|
|
203
|
+
return new browserCtor();
|
|
204
|
+
// Hide the dynamic import behind a runtime-computed specifier so
|
|
205
|
+
// browser bundlers don't pull xmldom into the client bundle.
|
|
206
|
+
const moduleName = '@xmldom/xmldom';
|
|
207
|
+
const xmldom = (await import(/* @vite-ignore */ moduleName));
|
|
208
|
+
return new xmldom.DOMParser();
|
|
209
|
+
})();
|
|
210
|
+
return parserPromise;
|
|
211
|
+
}
|
|
212
|
+
function walkIds(el, issues) {
|
|
213
|
+
checkShape(el, SHAPE_IDS, 'ids', issues);
|
|
214
|
+
for (const child of childElements(el)) {
|
|
215
|
+
const ln = (child.localName ?? '').toLowerCase();
|
|
216
|
+
if (ln === 'info')
|
|
217
|
+
walkInfo(child, 'ids.info', issues);
|
|
218
|
+
else if (ln === 'specifications')
|
|
219
|
+
walkSpecifications(child, 'ids.specifications', issues);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function walkInfo(el, path, issues) {
|
|
223
|
+
checkShape(el, SHAPE_INFO, path, issues);
|
|
224
|
+
// Info children are leaf text nodes; don't recurse.
|
|
225
|
+
}
|
|
226
|
+
function walkSpecifications(el, path, issues) {
|
|
227
|
+
checkShape(el, SHAPE_SPECIFICATIONS, path, issues);
|
|
228
|
+
let i = 0;
|
|
229
|
+
for (const child of childElements(el)) {
|
|
230
|
+
const ln = (child.localName ?? '').toLowerCase();
|
|
231
|
+
if (ln === 'specification')
|
|
232
|
+
walkSpecification(child, `${path}.specification[${i++}]`, issues);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function walkSpecification(el, path, issues) {
|
|
236
|
+
checkShape(el, SHAPE_SPECIFICATION, path, issues);
|
|
237
|
+
for (const child of childElements(el)) {
|
|
238
|
+
const ln = (child.localName ?? '').toLowerCase();
|
|
239
|
+
if (ln === 'applicability')
|
|
240
|
+
walkFacetContainer(child, `${path}.applicability`, false, issues);
|
|
241
|
+
else if (ln === 'requirements')
|
|
242
|
+
walkFacetContainer(child, `${path}.requirements`, true, issues);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function walkFacetContainer(el, path, isRequirements, issues) {
|
|
246
|
+
checkShape(el, isRequirements ? SHAPE_REQUIREMENTS : SHAPE_APPLICABILITY, path, issues);
|
|
247
|
+
let i = 0;
|
|
248
|
+
for (const child of childElements(el)) {
|
|
249
|
+
const ln = (child.localName ?? '').toLowerCase();
|
|
250
|
+
walkFacet(child, ln, `${path}.facets[${i++}]`, isRequirements, issues);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function walkFacet(el, tag, path, inRequirements, issues) {
|
|
254
|
+
const shape = inRequirements
|
|
255
|
+
? shapeInRequirements(tag)
|
|
256
|
+
: facetBaseShape(tag);
|
|
257
|
+
checkShape(el, shape, path, issues);
|
|
258
|
+
for (const child of childElements(el)) {
|
|
259
|
+
const ln = (child.localName ?? '').toLowerCase();
|
|
260
|
+
walkIdsValueOrFacet(child, ln, `${path}.${ln}`, inRequirements, issues);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function walkIdsValueOrFacet(el, tag, path, inRequirements, issues) {
|
|
264
|
+
switch (tag) {
|
|
265
|
+
case 'entity':
|
|
266
|
+
// `partOf > entity` is a nested entity facet (applicability shape).
|
|
267
|
+
walkFacet(el, 'entity', path, false, issues);
|
|
268
|
+
return;
|
|
269
|
+
case 'name':
|
|
270
|
+
case 'value':
|
|
271
|
+
case 'baseName':
|
|
272
|
+
case 'propertySet':
|
|
273
|
+
case 'system':
|
|
274
|
+
case 'predefinedType':
|
|
275
|
+
walkIdsValue(el, path, issues);
|
|
276
|
+
return;
|
|
277
|
+
default:
|
|
278
|
+
// Unknown child of a facet — already flagged by the parent's checkShape.
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function walkIdsValue(el, path, issues) {
|
|
283
|
+
checkShape(el, SHAPE_IDS_VALUE, path, issues);
|
|
284
|
+
for (const child of childElements(el)) {
|
|
285
|
+
const ln = (child.localName ?? '').toLowerCase();
|
|
286
|
+
if (ln === 'simpleValue') {
|
|
287
|
+
checkShape(child, SHAPE_SIMPLE_VALUE, `${path}.simpleValue`, issues);
|
|
288
|
+
}
|
|
289
|
+
else if (ln === 'restriction' &&
|
|
290
|
+
(child.namespaceURI === XS_NS || child.namespaceURI === null)) {
|
|
291
|
+
walkXsRestriction(child, `${path}.restriction`, issues);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function walkXsRestriction(el, path, issues) {
|
|
296
|
+
checkShape(el, SHAPE_XS_RESTRICTION, path, issues);
|
|
297
|
+
for (const child of childElements(el)) {
|
|
298
|
+
const ln = (child.localName ?? '').toLowerCase();
|
|
299
|
+
if (XS_RESTRICTION_FACETS.includes(ln)) {
|
|
300
|
+
checkShape(child, SHAPE_XS_FACET, `${path}.${ln}`, issues);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
// Shape enforcement primitives
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
function checkShape(el, shape, path, issues) {
|
|
308
|
+
// Attributes
|
|
309
|
+
const allowedAttrs = new Set(shape.attrs.map((a) => a.toLowerCase()));
|
|
310
|
+
for (const attr of Array.from(el.attributes ?? [])) {
|
|
311
|
+
if (isNamespaceAttribute(attr))
|
|
312
|
+
continue;
|
|
313
|
+
const name = attr.nodeName ?? '';
|
|
314
|
+
if (isToleratedGlobalAttr(name))
|
|
315
|
+
continue;
|
|
316
|
+
const rawLocalName = attr.localName ?? attr.nodeName ?? '';
|
|
317
|
+
// happy-dom sometimes keeps the prefix (e.g. `xsi:schemaLocation`)
|
|
318
|
+
// in localName with namespaceURI=null. Strip the prefix before
|
|
319
|
+
// matching shape rules.
|
|
320
|
+
const colonIdx = rawLocalName.indexOf(':');
|
|
321
|
+
const localName = colonIdx === -1
|
|
322
|
+
? rawLocalName.toLowerCase()
|
|
323
|
+
: rawLocalName.slice(colonIdx + 1).toLowerCase();
|
|
324
|
+
const prefix = colonIdx === -1 ? '' : rawLocalName.slice(0, colonIdx).toLowerCase();
|
|
325
|
+
// `xmlns="..."` and `xmlns:*` declarations.
|
|
326
|
+
if (!localName || localName === 'xmlns' || prefix === 'xmlns')
|
|
327
|
+
continue;
|
|
328
|
+
// Globally tolerated XML/XSI attributes (e.g. xsi:schemaLocation,
|
|
329
|
+
// xml:lang) when the prefix is known.
|
|
330
|
+
if (TOLERATED_GLOBAL_ATTR_PREFIXES.has(prefix))
|
|
331
|
+
continue;
|
|
332
|
+
if (!allowedAttrs.has(localName)) {
|
|
333
|
+
issues.push({
|
|
334
|
+
severity: 'error',
|
|
335
|
+
code: 'E_XSD_STRUCTURE',
|
|
336
|
+
message: `unexpected attribute "${rawLocalName}" on <${el.localName}>`,
|
|
337
|
+
path,
|
|
338
|
+
detail: {
|
|
339
|
+
attribute: rawLocalName,
|
|
340
|
+
element: el.localName ?? '',
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// Required attributes
|
|
346
|
+
if (shape.requiredAttrs) {
|
|
347
|
+
for (const req of shape.requiredAttrs) {
|
|
348
|
+
if (!hasAttribute(el, req)) {
|
|
349
|
+
issues.push({
|
|
350
|
+
severity: 'error',
|
|
351
|
+
code: 'E_XSD_REQUIRED_ATTR',
|
|
352
|
+
message: `<${el.localName}> is missing required @${req}`,
|
|
353
|
+
path,
|
|
354
|
+
detail: { attribute: req, element: el.localName ?? '' },
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// Children
|
|
360
|
+
const allowedChildren = new Set(shape.children.map((c) => c.toLowerCase()));
|
|
361
|
+
const allowedXsChildren = new Set((shape.xsChildren ?? []).map((c) => c.toLowerCase()));
|
|
362
|
+
for (const child of childElements(el)) {
|
|
363
|
+
const ln = (child.localName ?? '').toLowerCase();
|
|
364
|
+
const isXs = child.namespaceURI === XS_NS;
|
|
365
|
+
if (isXs) {
|
|
366
|
+
if (!allowedXsChildren.has(ln)) {
|
|
367
|
+
issues.push({
|
|
368
|
+
severity: 'error',
|
|
369
|
+
code: 'E_XSD_STRUCTURE',
|
|
370
|
+
message: `unexpected XSD child <xs:${ln}> in <${el.localName}>`,
|
|
371
|
+
path,
|
|
372
|
+
detail: { child: `xs:${ln}`, parent: el.localName ?? '' },
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (!allowedChildren.has(ln)) {
|
|
378
|
+
issues.push({
|
|
379
|
+
severity: 'error',
|
|
380
|
+
code: 'E_XSD_STRUCTURE',
|
|
381
|
+
message: `unexpected child <${child.localName}> in <${el.localName}>`,
|
|
382
|
+
path,
|
|
383
|
+
detail: { child: child.localName ?? '', parent: el.localName ?? '' },
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (shape.textOnly) {
|
|
388
|
+
// Element should have no element children — text already checked
|
|
389
|
+
// implicitly by children loop above.
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function hasAttribute(el, name) {
|
|
393
|
+
if (typeof el.hasAttribute === 'function' && el.hasAttribute(name))
|
|
394
|
+
return true;
|
|
395
|
+
// xmldom returns attribute objects under `attributes`; iterate.
|
|
396
|
+
for (const attr of Array.from(el.attributes ?? [])) {
|
|
397
|
+
const ln = (attr.localName ?? attr.nodeName ?? '').toLowerCase();
|
|
398
|
+
if (ln === name.toLowerCase())
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
function isToleratedGlobalAttr(name) {
|
|
404
|
+
if (!name)
|
|
405
|
+
return false;
|
|
406
|
+
if (name === 'xmlns')
|
|
407
|
+
return true;
|
|
408
|
+
const colon = name.indexOf(':');
|
|
409
|
+
if (colon === -1)
|
|
410
|
+
return false;
|
|
411
|
+
return TOLERATED_GLOBAL_ATTR_PREFIXES.has(name.slice(0, colon));
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* XML and XML-derived namespaces whose attributes are tolerated on any
|
|
415
|
+
* IDS element regardless of the element's shape — `xmlns`, `xml:lang`,
|
|
416
|
+
* `xsi:schemaLocation`, etc.
|
|
417
|
+
*/
|
|
418
|
+
const TOLERATED_ATTR_NAMESPACES = new Set([
|
|
419
|
+
'http://www.w3.org/2000/xmlns/',
|
|
420
|
+
'http://www.w3.org/XML/1998/namespace',
|
|
421
|
+
'http://www.w3.org/2001/XMLSchema-instance',
|
|
422
|
+
]);
|
|
423
|
+
function isNamespaceAttribute(attr) {
|
|
424
|
+
return (attr.namespaceURI !== null &&
|
|
425
|
+
TOLERATED_ATTR_NAMESPACES.has(attr.namespaceURI));
|
|
426
|
+
}
|
|
427
|
+
function childElements(el) {
|
|
428
|
+
// Some xmldom versions populate `children`; others only `childNodes`.
|
|
429
|
+
if (el.children && el.children.length !== undefined) {
|
|
430
|
+
return Array.from(el.children);
|
|
431
|
+
}
|
|
432
|
+
const out = [];
|
|
433
|
+
const nodes = el.childNodes ?? [];
|
|
434
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
435
|
+
const n = nodes[i];
|
|
436
|
+
if (n && n.nodeType === 1)
|
|
437
|
+
out.push(n);
|
|
438
|
+
}
|
|
439
|
+
return out;
|
|
440
|
+
}
|
|
441
|
+
//# sourceMappingURL=index.js.map
|