@ifc-lite/bcf 2.0.1 → 3.0.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/xml-text.js CHANGED
@@ -2,24 +2,79 @@
2
2
  * License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
4
  /**
5
- * Reading text back out of BCF's XML.
5
+ * Reading text out of BCF's XML, and escaping it back in.
6
6
  *
7
7
  * Split out of `reader.ts` so the component parsers can share it without a
8
- * cycle. `writer.ts::escapeXml` is the inverse of `unescapeXml` here, and the
9
- * two are pinned against each other in `writer.test.ts`.
8
+ * cycle. `escapeXml` here is the inverse of `unescapeXml`, and the two are
9
+ * pinned against each other in `writer.test.ts`.
10
10
  */
11
11
  /**
12
12
  * Extract a simple element value from XML
13
13
  *
14
- * Values are unescaped so writer.ts's escapeXml() round-trips correctly
15
- * (see escapeXml/unescapeXml regression: & < > " ' in titles/descriptions/
14
+ * Values are unescaped so {@link escapeXml} round-trips correctly (see
15
+ * escapeXml/unescapeXml regression: & < > " ' in titles/descriptions/
16
16
  * comments must come back exactly as written, not as literal entities).
17
+ *
18
+ * The content between the tags is read with {@link decodeXmlCharData}
19
+ * rather than a bare `[^<]*` match, so a CDATA-wrapped value (whose content
20
+ * starts with the literal `<![CDATA[`) is read instead of silently
21
+ * rejected -- the same defect `parseLabels` was fixed for, but present on
22
+ * every other caller of this function (Title, Comment, and the rest).
23
+ * `decodeXmlCharData` still returns `null`, and this still returns
24
+ * `undefined`, when the content holds real child-element markup.
17
25
  */
18
26
  export function extractElement(content, elementName) {
19
- const match = content.match(new RegExp(`<${elementName}>([^<]*)<\\/${elementName}>`));
20
- return match?.[1] !== undefined ? unescapeXml(match[1]) : undefined;
27
+ const match = content.match(new RegExp(`<${elementName}>([\\s\\S]*?)<\\/${elementName}>`));
28
+ if (match?.[1] === undefined)
29
+ return undefined;
30
+ const decoded = decodeXmlCharData(match[1]);
31
+ return decoded ?? undefined;
32
+ }
33
+ /**
34
+ * Parse a captured `xs:boolean` value per its lexical space
35
+ * `{true, false, 1, 0}` (whiteSpace=collapse, so leading/trailing
36
+ * whitespace is ignored; matched case-sensitively, per the XSD's literal
37
+ * lexical space -- BCF markup never emits `True`/`TRUE`).
38
+ *
39
+ * Does NOT decide what an *absent* attribute means, nor a *blank* one: a
40
+ * value that collapses to the empty string is schema-invalid for
41
+ * `xs:boolean` and is treated the same as absent, not as recognised-false or
42
+ * as garbage -- so every caller checks `raw === undefined` (or blank, after
43
+ * a `.trim()`) itself and supplies its own absent-case default, which
44
+ * differs by call site (`DefaultVisibility` leans true on BCF 2.1, false on
45
+ * 3.0; the others are `undefined`-optional or lean false). This unifies the
46
+ * four hand-rolled xs:boolean parses this package had (`parseVisibility`,
47
+ * `parseViewSetupHints`'s `flag()`, and the two `isExternal` sites in
48
+ * `reader.ts`), which is exactly the drift #3713 warns about: one behaviour,
49
+ * several hand-rolled implementations, each patched independently.
50
+ *
51
+ * A present value outside the lexical space (garbage, e.g. `"yes"`) resolves
52
+ * to `ifUnrecognized` rather than a hardcoded pick, because callers
53
+ * genuinely disagree on which way an unrecognised value should lean.
54
+ *
55
+ * @param raw a non-blank, already-captured attribute or element value (the
56
+ * caller has already ruled out absent/blank)
57
+ */
58
+ export function parseXsBoolean(raw, { ifUnrecognized }) {
59
+ const trimmed = raw.trim();
60
+ if (trimmed === 'true' || trimmed === '1')
61
+ return true;
62
+ if (trimmed === 'false' || trimmed === '0')
63
+ return false;
64
+ return ifUnrecognized;
65
+ }
66
+ /**
67
+ * Escape XML special characters
68
+ */
69
+ export function escapeXml(str) {
70
+ return str
71
+ .replace(/&/g, '&amp;')
72
+ .replace(/</g, '&lt;')
73
+ .replace(/>/g, '&gt;')
74
+ .replace(/"/g, '&quot;')
75
+ .replace(/'/g, '&apos;');
21
76
  }
22
- /** The five entities `writer.ts::escapeXml` produces. */
77
+ /** The five entities {@link escapeXml} produces. */
23
78
  const NAMED_ENTITIES = {
24
79
  lt: '<',
25
80
  gt: '>',
@@ -52,4 +107,72 @@ export function unescapeXml(str) {
52
107
  : whole;
53
108
  });
54
109
  }
110
+ /**
111
+ * Extract topic `<Labels>` values, tolerant of both BCF markup.xsd shapes.
112
+ *
113
+ * BCF 2.1 repeats the label element itself: `<Labels>Structural</Labels>
114
+ * <Labels>Urgent</Labels>`. BCF 3.0 instead wraps ONE `<Labels>` container
115
+ * around repeated `<Label>` children: `<Labels><Label>Structural</Label>
116
+ * <Label>Urgent</Label></Labels>`. A regex that only matched the 2.1 shape
117
+ * (immediate non-tag text) silently dropped every label from a conformant
118
+ * 3.0 archive, since its `<Labels>` is immediately followed by a nested `<`,
119
+ * not text. This walks each `<Labels>` block and prefers nested `<Label>`
120
+ * children when present, falling back to the 2.1 direct-text shape.
121
+ *
122
+ * Label content goes through {@link decodeXmlCharData} rather than a bare
123
+ * `[^<]*` match, so a CDATA label (`<Label><![CDATA[Urgent & Important]]>
124
+ * </Label>` — whose content starts with `<`) is read instead of dropped.
125
+ */
126
+ export function parseLabels(topicContent) {
127
+ const labels = [];
128
+ // One emptiness rule for both shapes, so `<Labels><Label></Label></Labels>`
129
+ // and `<Labels></Labels>` agree. The 3.0 branch used to push
130
+ // decodeXmlCharData's '' straight through while the 2.1 branch skipped it,
131
+ // so only the 3.0 shape produced a phantom empty label. Whitespace-only
132
+ // content is NOT empty and is kept: markup.xsd types both elements as
133
+ // xs:string (whiteSpace=preserve), so ' ' is a legal value, and that is
134
+ // what the 2.1 path has always returned.
135
+ const push = (raw) => {
136
+ const text = decodeXmlCharData(raw);
137
+ if (text !== null && text.length > 0)
138
+ labels.push(text);
139
+ };
140
+ for (const block of topicContent.matchAll(/<Labels>([\s\S]*?)<\/Labels>/g)) {
141
+ const inner = block[1];
142
+ const nested = [...inner.matchAll(/<Label>([\s\S]*?)<\/Label>/g)];
143
+ if (nested.length > 0) {
144
+ for (const label of nested)
145
+ push(label[1]);
146
+ }
147
+ else {
148
+ push(inner);
149
+ }
150
+ }
151
+ return labels;
152
+ }
153
+ /**
154
+ * Decode element content that should be XML character data, tolerating CDATA
155
+ * sections.
156
+ *
157
+ * Per the XML spec, CDATA content is literal — `&amp;` inside CDATA stays
158
+ * `&amp;` — while text outside CDATA is entity-decoded via
159
+ * {@link unescapeXml}. Returns `null` when the content holds real markup (a
160
+ * `<` outside any CDATA section): that is child elements, not character
161
+ * data, and the caller should not treat it as a text value.
162
+ */
163
+ function decodeXmlCharData(raw) {
164
+ let out = '';
165
+ let last = 0;
166
+ for (const m of raw.matchAll(/<!\[CDATA\[([\s\S]*?)\]\]>/g)) {
167
+ const text = raw.slice(last, m.index);
168
+ if (text.includes('<'))
169
+ return null;
170
+ out += unescapeXml(text) + m[1];
171
+ last = m.index + m[0].length;
172
+ }
173
+ const tail = raw.slice(last);
174
+ if (tail.includes('<'))
175
+ return null;
176
+ return out + unescapeXml(tail);
177
+ }
55
178
  //# sourceMappingURL=xml-text.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"xml-text.js","sourceRoot":"","sources":["../src/xml-text.ts"],"names":[],"mappings":"AAAA;;+DAE+D;AAE/D;;;;;;GAMG;AAEH;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,OAAe,EAAE,WAAmB;IACjE,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,IAAI,WAAW,eAAe,WAAW,GAAG,CAAC,CAAC,CAAC;IACtF,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACtE,CAAC;AAED,yDAAyD;AACzD,MAAM,cAAc,GAA2B;IAC7C,EAAE,EAAE,GAAG;IACP,EAAE,EAAE,GAAG;IACP,IAAI,EAAE,GAAG;IACT,IAAI,EAAE,GAAG;IACT,GAAG,EAAE,GAAG;CACT,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,OAAO,GAAG,CAAC,OAAO,CAChB,yDAAyD,EACzD,CAAC,KAAK,EAAE,GAAuB,EAAE,GAAuB,EAAE,IAAwB,EAAE,EAAE;QACpF,IAAI,IAAI,KAAK,SAAS;YAAE,OAAO,cAAc,CAAC,IAAI,CAAC,CAAC;QACpD,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,IAAI,GAAG,IAAI,EAAE,EAAE,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC5E,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,QAAQ;YAC3D,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC;YAC5B,CAAC,CAAC,KAAK,CAAC;IACZ,CAAC,CACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"xml-text.js","sourceRoot":"","sources":["../src/xml-text.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Normalize a date-ish string to a valid `xs:dateTime`, or `undefined` if it
3
+ * can't be made into one. A value already matching `xs:dateTime` is returned
4
+ * trimmed but otherwise UNCHANGED -- never reformatted or shifted. A bare
5
+ * `YYYY-MM-DD` becomes midnight UTC on that date. Anything else (an empty
6
+ * string, free text, a malformed timestamp) is unparseable and comes back
7
+ * `undefined`; the caller decides whether that means "omit the element" or
8
+ * "refuse to write".
9
+ */
10
+ export declare function normalizeXsdDateTime(value: string): string | undefined;
11
+ export declare const XML_WHITESPACE_ONLY: RegExp;
12
+ /**
13
+ * A required `xs:dateTime` on its way into an archive: normalized (a bare
14
+ * date becomes midnight UTC; an already-valid dateTime passes through
15
+ * unchanged), escaped -- or an error. `minOccurs="1"` with no default means
16
+ * there is no valid element to omit here, so an absent or unparseable value
17
+ * both refuse rather than write something invalid or invented.
18
+ */
19
+ export declare function xsdDateTime(value: string | undefined, element: string, where: string): string;
20
+ /**
21
+ * An optional `xs:dateTime` on its way into an archive: normalized the same
22
+ * way as `xsdDateTime`, but `undefined` (never a throw) for anything absent
23
+ * or unparseable. `minOccurs="0"` makes omitting the element schema-valid,
24
+ * and that is always preferable to writing invalid text that poisons the
25
+ * whole file -- the caller checks the return value and skips the element
26
+ * when it is `undefined`.
27
+ */
28
+ export declare function xsdOptionalDateTime(value: string | undefined): string | undefined;
29
+ /**
30
+ * A required author-like string on its way into an archive, escaped -- or an error.
31
+ *
32
+ * BCF 2.1 uses `UserIdType`, an unrestricted `xs:string`, so an explicitly
33
+ * supplied empty or whitespace-only author is schema-valid. BCF 3.0 replaced
34
+ * it with `NonEmptyOrBlankString`, whose collapsed value must be non-empty.
35
+ * Absence is invalid in both versions because the element itself is required.
36
+ */
37
+ export declare function xsdRequiredString(value: string | undefined, element: string, where: string, version: '2.1' | '3.0'): string;
38
+ //# sourceMappingURL=xsd-required-string.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xsd-required-string.d.ts","sourceRoot":"","sources":["../src/xsd-required-string.ts"],"names":[],"mappings":"AA8EA;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAOtE;AAQD,eAAO,MAAM,mBAAmB,QAAiB,CAAC;AAElD;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAkB7F;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAIjF;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,KAAK,GAAG,KAAK,GACrB,MAAM,CASR"}
@@ -0,0 +1,158 @@
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
+ * Write-side guards for `markup.xsd` elements declared `minOccurs="1"` with
6
+ * no schema default: `Topic/CreationDate`, `Comment/Date`,
7
+ * `Topic/CreationAuthor`, `Topic/ModifiedAuthor` and `Comment/Author`. Also
8
+ * the normalizer shared by every `xs:dateTime` element in `markup.xsd`,
9
+ * required or not: `Topic/CreationDate`, `Topic/ModifiedDate`,
10
+ * `Topic/DueDate`, `Comment/Date`, `Comment/ModifiedDate` and
11
+ * `Header/File/Date`.
12
+ *
13
+ * Split out of `writer.ts`, which was pushing its module-size budget. Mirrors
14
+ * `numeric.ts`'s split for the numeric xsd guards (`xsdDouble`/`xsdInt`).
15
+ *
16
+ * `reader.ts` no longer invents a value for these fields when a source
17
+ * `.bcfzip` omits them (a wall-clock `CreationDate`, a placeholder `'Unknown'`
18
+ * author) -- so a `BCFTopic`/`BCFComment` read back from a non-conformant
19
+ * archive can arrive with any of them unset. Inventing one HERE, on write,
20
+ * would be that same fabrication under a different name; dropping the element
21
+ * entirely hands the caller an archive that already fails the schema. So both
22
+ * refuse, the same way the `TopicType`/`TopicStatus` check in `writer.ts`
23
+ * does for BCF 3.0.
24
+ *
25
+ * `xs:dateTime` is stricter than the callers ever validated: an HTML
26
+ * `<input type="date">` yields a bare `YYYY-MM-DD`, which every one of the
27
+ * elements above rejects as "not a valid value of the atomic type
28
+ * xs:dateTime" (buildingSMART/BCF-XML `markup.xsd`, confirmed against a real
29
+ * reported archive -- issue #3612's `markup.bcf` carried exactly this on
30
+ * `DueDate`). `normalizeXsdDateTime` turns a bare date into midnight UTC,
31
+ * passes an already-valid `xs:dateTime` through UNCHANGED (no reformatting,
32
+ * no timezone shift), and returns `undefined` for anything else.
33
+ */
34
+ import { escapeXml } from './xml-text.js';
35
+ // A full `xs:dateTime`: date, mandatory 'T', time, optional fractional
36
+ // seconds, optional timezone (Z or ±HH:MM). The lexical match only decides
37
+ // pass-through-unchanged vs normalize-or-reject and never reformats a value
38
+ // that already matches; `isCalendarValid` below then rejects lexically-valid
39
+ // but calendar-impossible values the xs:dateTime VALUE space excludes.
40
+ const XSD_DATETIME_RE = /^-?(\d{4,})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;
41
+ // A bare xs:date, e.g. what an HTML `<input type="date">` yields.
42
+ const BARE_DATE_RE = /^-?(\d{4,})-(\d{2})-(\d{2})$/;
43
+ const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
44
+ /**
45
+ * Value-space check behind the lexical regexes: the xs:dateTime/xs:date VALUE
46
+ * space excludes calendar-impossible lexemes (month 13, February 30, minute
47
+ * 99, a timezone beyond ±14:00), and a schema validator rejects them — so a
48
+ * writer that emitted one would hand the caller an archive that fails
49
+ * validation, exactly what this module exists to refuse. Hour 24 is legal in
50
+ * XSD only as 24:00:00 (end of day).
51
+ */
52
+ function isCalendarValid(m, hasTime) {
53
+ const year = Number(m[1]);
54
+ const month = Number(m[2]);
55
+ const day = Number(m[3]);
56
+ if (month < 1 || month > 12 || day < 1)
57
+ return false;
58
+ const leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
59
+ const maxDay = month === 2 && leap ? 29 : DAYS_IN_MONTH[month - 1];
60
+ if (day > maxDay)
61
+ return false;
62
+ if (!hasTime)
63
+ return true;
64
+ const hour = Number(m[4]);
65
+ const minute = Number(m[5]);
66
+ const second = Number(m[6]);
67
+ if (minute > 59 || second > 59)
68
+ return false;
69
+ if (hour > 24 || (hour === 24 && (minute !== 0 || second !== 0)))
70
+ return false;
71
+ const tz = m[7];
72
+ if (tz !== undefined && tz !== 'Z') {
73
+ const tzHour = Number(tz.slice(1, 3));
74
+ const tzMinute = Number(tz.slice(4, 6));
75
+ if (tzHour > 14 || tzMinute > 59 || (tzHour === 14 && tzMinute !== 0))
76
+ return false;
77
+ }
78
+ return true;
79
+ }
80
+ /**
81
+ * Normalize a date-ish string to a valid `xs:dateTime`, or `undefined` if it
82
+ * can't be made into one. A value already matching `xs:dateTime` is returned
83
+ * trimmed but otherwise UNCHANGED -- never reformatted or shifted. A bare
84
+ * `YYYY-MM-DD` becomes midnight UTC on that date. Anything else (an empty
85
+ * string, free text, a malformed timestamp) is unparseable and comes back
86
+ * `undefined`; the caller decides whether that means "omit the element" or
87
+ * "refuse to write".
88
+ */
89
+ export function normalizeXsdDateTime(value) {
90
+ const trimmed = value.trim();
91
+ const dateTime = trimmed.match(XSD_DATETIME_RE);
92
+ if (dateTime)
93
+ return isCalendarValid(dateTime, true) ? trimmed : undefined;
94
+ const bareDate = trimmed.match(BARE_DATE_RE);
95
+ if (bareDate)
96
+ return isCalendarValid(bareDate, false) ? `${trimmed}T00:00:00Z` : undefined;
97
+ return undefined;
98
+ }
99
+ // BCF 3.0's markup.xsd types `Topic/@TopicType` and `Topic/@TopicStatus` as
100
+ // `NonEmptyOrBlankString`: after XML whitespace (#x9, #xA, #xD, #x20) is
101
+ // collapsed, the value must have length >= 1. A value that is present but
102
+ // consists entirely of XML whitespace collapses to nothing and is therefore
103
+ // as invalid as an absent one -- which is why the guards below reuse it, and
104
+ // why `writer.ts`'s own TopicType/TopicStatus checks import it too.
105
+ export const XML_WHITESPACE_ONLY = /^[\t\n\r ]*$/;
106
+ /**
107
+ * A required `xs:dateTime` on its way into an archive: normalized (a bare
108
+ * date becomes midnight UTC; an already-valid dateTime passes through
109
+ * unchanged), escaped -- or an error. `minOccurs="1"` with no default means
110
+ * there is no valid element to omit here, so an absent or unparseable value
111
+ * both refuse rather than write something invalid or invented.
112
+ */
113
+ export function xsdDateTime(value, element, where) {
114
+ if (!value || XML_WHITESPACE_ONLY.test(value)) {
115
+ throw new Error(`BCF requires ${element} (${where} has none). markup.xsd declares it ` +
116
+ `minOccurs="1" with no default, and a time the source never stated is ` +
117
+ `not the writer's to invent. Set it before writing.`);
118
+ }
119
+ const normalized = normalizeXsdDateTime(value);
120
+ if (normalized === undefined) {
121
+ throw new Error(`BCF requires ${element} to be a valid xs:dateTime (${where} has ` +
122
+ `"${value}", which is neither a dateTime nor a bare YYYY-MM-DD date). ` +
123
+ `markup.xsd types it xs:dateTime with no default, so an unparseable ` +
124
+ `value is not the writer's to invent a fallback for. Fix it before writing.`);
125
+ }
126
+ return escapeXml(normalized);
127
+ }
128
+ /**
129
+ * An optional `xs:dateTime` on its way into an archive: normalized the same
130
+ * way as `xsdDateTime`, but `undefined` (never a throw) for anything absent
131
+ * or unparseable. `minOccurs="0"` makes omitting the element schema-valid,
132
+ * and that is always preferable to writing invalid text that poisons the
133
+ * whole file -- the caller checks the return value and skips the element
134
+ * when it is `undefined`.
135
+ */
136
+ export function xsdOptionalDateTime(value) {
137
+ if (!value || XML_WHITESPACE_ONLY.test(value))
138
+ return undefined;
139
+ const normalized = normalizeXsdDateTime(value);
140
+ return normalized === undefined ? undefined : escapeXml(normalized);
141
+ }
142
+ /**
143
+ * A required author-like string on its way into an archive, escaped -- or an error.
144
+ *
145
+ * BCF 2.1 uses `UserIdType`, an unrestricted `xs:string`, so an explicitly
146
+ * supplied empty or whitespace-only author is schema-valid. BCF 3.0 replaced
147
+ * it with `NonEmptyOrBlankString`, whose collapsed value must be non-empty.
148
+ * Absence is invalid in both versions because the element itself is required.
149
+ */
150
+ export function xsdRequiredString(value, element, where, version) {
151
+ if (value === undefined || (version === '3.0' && XML_WHITESPACE_ONLY.test(value))) {
152
+ throw new Error(`BCF requires ${element} (${where} has none). markup.xsd declares it ` +
153
+ `minOccurs="1" with no default, and a value the source never stated is ` +
154
+ `not the writer's to invent. Set it before writing.`);
155
+ }
156
+ return escapeXml(value);
157
+ }
158
+ //# sourceMappingURL=xsd-required-string.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xsd-required-string.js","sourceRoot":"","sources":["../src/xsd-required-string.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ifc-lite/bcf",
3
- "version": "2.0.1",
3
+ "version": "3.0.0",
4
4
  "description": "BIM Collaboration Format (BCF) support for IFC-Lite",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -14,7 +14,7 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "jszip": "^3.10.0",
17
- "@ifc-lite/encoding": "^2.1.0"
17
+ "@ifc-lite/encoding": "^2.2.0"
18
18
  },
19
19
  "devDependencies": {
20
20
  "typescript": "^6.0.3",