@ifc-lite/bcf 2.0.0 → 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/README.md +12 -1
- package/dist/ids-camera.d.ts +115 -0
- package/dist/ids-camera.d.ts.map +1 -0
- package/dist/ids-camera.js +266 -0
- package/dist/ids-camera.js.map +1 -0
- package/dist/ids-reporter.d.ts +17 -13
- package/dist/ids-reporter.d.ts.map +1 -1
- package/dist/ids-reporter.js +32 -64
- package/dist/ids-reporter.js.map +1 -1
- package/dist/numeric.d.ts +64 -1
- package/dist/numeric.d.ts.map +1 -1
- package/dist/numeric.js +81 -1
- package/dist/numeric.js.map +1 -1
- package/dist/reader-components.d.ts +1 -1
- package/dist/reader-components.d.ts.map +1 -1
- package/dist/reader-components.js +49 -10
- package/dist/reader-components.js.map +1 -1
- package/dist/reader.js +28 -30
- package/dist/reader.js.map +1 -1
- package/dist/types.d.ts +30 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/viewpoint.d.ts +10 -0
- package/dist/viewpoint.d.ts.map +1 -1
- package/dist/viewpoint.js +12 -0
- package/dist/viewpoint.js.map +1 -1
- package/dist/writer-camera.d.ts +46 -0
- package/dist/writer-camera.d.ts.map +1 -0
- package/dist/writer-camera.js +131 -0
- package/dist/writer-camera.js.map +1 -0
- package/dist/writer.d.ts.map +1 -1
- package/dist/writer.js +118 -198
- package/dist/writer.js.map +1 -1
- package/dist/xml-text.d.ts +62 -5
- package/dist/xml-text.d.ts.map +1 -1
- package/dist/xml-text.js +131 -8
- package/dist/xml-text.js.map +1 -1
- package/dist/xsd-required-string.d.ts +38 -0
- package/dist/xsd-required-string.d.ts.map +1 -0
- package/dist/xsd-required-string.js +158 -0
- package/dist/xsd-required-string.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,131 @@
|
|
|
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 { xsdDouble, xsdPointElement } from './numeric.js';
|
|
5
|
+
/**
|
|
6
|
+
* Enforce each version's rule for how many cameras a viewpoint may carry.
|
|
7
|
+
*
|
|
8
|
+
* The two schemas differ, and the emitted ORDER matters in one of them:
|
|
9
|
+
*
|
|
10
|
+
* - v2_1/visinfo.xsd puts `OrthogonalCamera` and `PerspectiveCamera` in
|
|
11
|
+
* `VisualizationInfo`'s `xs:sequence`, both `minOccurs="0"`. Either, both or
|
|
12
|
+
* neither may appear -- but an `xs:sequence` fixes the order, and the
|
|
13
|
+
* schema declares the orthogonal camera FIRST. Emitting the perspective
|
|
14
|
+
* camera first made a two-camera 2.1 viewpoint schema-invalid
|
|
15
|
+
* ("Element 'OrthogonalCamera': This element is not expected"), which is
|
|
16
|
+
* why `writer.ts`'s `writeViewpointFiles` now writes orthogonal first for both
|
|
17
|
+
* versions -- the order is required by 2.1 and harmless under 3.0's choice.
|
|
18
|
+
* - v3_0/visinfo.xsd replaced that pair with an `<xs:choice>` carrying no
|
|
19
|
+
* `minOccurs` and no `maxOccurs`, so both default to 1: EXACTLY ONE camera,
|
|
20
|
+
* and it is required. Two cameras and zero cameras are each invalid there.
|
|
21
|
+
*
|
|
22
|
+
* We refuse the 3.0 violations rather than guessing which camera the caller
|
|
23
|
+
* meant, or inventing one for a viewpoint that has none -- same policy as
|
|
24
|
+
* {@link requireAspectRatioElement} and the `Topic/@TopicType` checks in
|
|
25
|
+
* `writer.ts`'s `writeMarkupFile`. Silently dropping one of two cameras would discard
|
|
26
|
+
* a view the caller chose; silently emitting neither writes an archive that no
|
|
27
|
+
* conforming BCF 3.0 reader has to accept.
|
|
28
|
+
*/
|
|
29
|
+
export function requireCameraChoice(viewpoint, version) {
|
|
30
|
+
if (version !== '3.0')
|
|
31
|
+
return;
|
|
32
|
+
const count = (viewpoint.orthogonalCamera ? 1 : 0) + (viewpoint.perspectiveCamera ? 1 : 0);
|
|
33
|
+
if (count === 1)
|
|
34
|
+
return;
|
|
35
|
+
throw new Error(`BCF 3.0 requires exactly one camera per viewpoint (viewpoint "${viewpoint.guid}" has ` +
|
|
36
|
+
`${count === 0 ? 'none' : 'both an orthogonalCamera and a perspectiveCamera'}). ` +
|
|
37
|
+
`visinfo.xsd declares OrthogonalCamera and PerspectiveCamera as an xs:choice, so ` +
|
|
38
|
+
`set exactly one before writing a 3.0 file.`);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Require a FINITE, positive AspectRatio for a BCF 3.0 camera and return the
|
|
42
|
+
* element to append.
|
|
43
|
+
*
|
|
44
|
+
* v3_0/visinfo.xsd adds `<AspectRatio>` (type `PositiveDouble`, i.e.
|
|
45
|
+
* `xs:double` with `minExclusive value="0"`) as a REQUIRED, no-minOccurs
|
|
46
|
+
* child of both `OrthogonalCamera` and `PerspectiveCamera`. 2.1 has no such
|
|
47
|
+
* element. We refuse to invent a value (there is no safe default aspect
|
|
48
|
+
* ratio) because that would assert a value the caller never chose; instead
|
|
49
|
+
* we fail the write so the caller supplies one -- same policy as the
|
|
50
|
+
* `Topic/@TopicType`/`Topic/@TopicStatus` checks in `writer.ts`'s `writeMarkupFile`.
|
|
51
|
+
*
|
|
52
|
+
* BOTH halves of `PositiveDouble` are checked here, and the positivity test
|
|
53
|
+
* alone did not cover them: `Infinity > 0` is `true`, so `Infinity` walked
|
|
54
|
+
* past this guard and was emitted as `<AspectRatio>Infinity</AspectRatio>`,
|
|
55
|
+
* which xmllint rejects with "'Infinity' is not a valid value of the atomic
|
|
56
|
+
* type 'PositiveDouble'". `xsdDouble` is what makes the `xs:double` half of
|
|
57
|
+
* the claim true; the `> 0` below is the `minExclusive` facet.
|
|
58
|
+
*/
|
|
59
|
+
function requireAspectRatioElement(aspectRatio, viewpointGuid) {
|
|
60
|
+
const where = `viewpoint "${viewpointGuid}"`;
|
|
61
|
+
if (aspectRatio === undefined || !(aspectRatio > 0)) {
|
|
62
|
+
throw new Error(`BCF 3.0 requires a positive Camera/AspectRatio (${where} has ` +
|
|
63
|
+
`${aspectRatio === undefined ? 'none' : aspectRatio}). ` +
|
|
64
|
+
`Set the camera's aspectRatio before writing a 3.0 file.`);
|
|
65
|
+
}
|
|
66
|
+
return `\n <AspectRatio>${xsdDouble(aspectRatio, 'Camera/AspectRatio', where)}</AspectRatio>`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Require a FINITE FieldOfView within BCF 3.0's `(0, 180)` exclusive facet and
|
|
70
|
+
* return the text `xsdDouble` will accept for it.
|
|
71
|
+
*
|
|
72
|
+
* v3_0/visinfo.xsd's `FieldOfView` simpleType is `xs:double` with
|
|
73
|
+
* `minExclusive value="0"` and `maxExclusive value="180"`. `xsdDouble` alone
|
|
74
|
+
* only rejects non-finite values (Infinity/-Infinity/NaN); it says nothing
|
|
75
|
+
* about a finite value that is merely out of range, so `0`, a negative number,
|
|
76
|
+
* or `180` and above walked straight through it and were emitted as-is --
|
|
77
|
+
* schema-invalid the same way an unset `AspectRatio` was before
|
|
78
|
+
* {@link requireAspectRatioElement} started refusing it. 2.1's own
|
|
79
|
+
* `FieldOfView` facet (`[45, 60]` inclusive) is deliberately NOT enforced
|
|
80
|
+
* here: its own schema annotation says "This limitation will be dropped in
|
|
81
|
+
* the next release and viewers should expect values outside this range in
|
|
82
|
+
* current implementations", so treating it as a hard constraint would reject
|
|
83
|
+
* legitimate 2.1 input the schema authors themselves disclaim.
|
|
84
|
+
*/
|
|
85
|
+
function requireFieldOfViewElement(fieldOfView, viewpointGuid) {
|
|
86
|
+
const where = `viewpoint "${viewpointGuid}"`;
|
|
87
|
+
if (!(fieldOfView > 0 && fieldOfView < 180)) {
|
|
88
|
+
throw new Error(`BCF 3.0 requires PerspectiveCamera/FieldOfView in (0, 180) exclusive (${where} has ` +
|
|
89
|
+
`${fieldOfView}). visinfo.xsd's FieldOfView simpleType declares minExclusive="0" and ` +
|
|
90
|
+
`maxExclusive="180"; set a value inside that range before writing a 3.0 file.`);
|
|
91
|
+
}
|
|
92
|
+
return xsdDouble(fieldOfView, 'PerspectiveCamera/FieldOfView', where);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Write perspective camera XML
|
|
96
|
+
*/
|
|
97
|
+
export function writePerspectiveCamera(camera, version, viewpointGuid) {
|
|
98
|
+
const aspectRatioElement = version === '3.0' ? requireAspectRatioElement(camera.aspectRatio, viewpointGuid) : '';
|
|
99
|
+
const where = `viewpoint "${viewpointGuid}"`;
|
|
100
|
+
const fieldOfView = version === '3.0'
|
|
101
|
+
? requireFieldOfViewElement(camera.fieldOfView, viewpointGuid)
|
|
102
|
+
: xsdDouble(camera.fieldOfView, 'PerspectiveCamera/FieldOfView', where);
|
|
103
|
+
return `\n <PerspectiveCamera>${cameraVectors(camera, where)}
|
|
104
|
+
<FieldOfView>${fieldOfView}</FieldOfView>${aspectRatioElement}
|
|
105
|
+
</PerspectiveCamera>`;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Write orthogonal camera XML
|
|
109
|
+
*/
|
|
110
|
+
export function writeOrthogonalCamera(camera, version, viewpointGuid) {
|
|
111
|
+
const aspectRatioElement = version === '3.0' ? requireAspectRatioElement(camera.aspectRatio, viewpointGuid) : '';
|
|
112
|
+
const where = `viewpoint "${viewpointGuid}"`;
|
|
113
|
+
return `\n <OrthogonalCamera>${cameraVectors(camera, where)}
|
|
114
|
+
<ViewToWorldScale>${xsdDouble(camera.viewToWorldScale, 'OrthogonalCamera/ViewToWorldScale', where)}</ViewToWorldScale>${aspectRatioElement}
|
|
115
|
+
</OrthogonalCamera>`;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The three vectors both camera types open with, in the order both schemas'
|
|
119
|
+
* `xs:sequence` declares them.
|
|
120
|
+
*
|
|
121
|
+
* Identical in `OrthogonalCamera` and `PerspectiveCamera` and in 2.1 and 3.0 --
|
|
122
|
+
* the cameras differ only in the single element that follows (`ViewToWorldScale`
|
|
123
|
+
* vs `FieldOfView`). Writing them once is what makes "every camera coordinate
|
|
124
|
+
* is checked" a property of the code rather than of six copied templates.
|
|
125
|
+
*/
|
|
126
|
+
function cameraVectors(camera, where) {
|
|
127
|
+
return (xsdPointElement('CameraViewPoint', camera.cameraViewPoint, ' ', where) +
|
|
128
|
+
xsdPointElement('CameraDirection', camera.cameraDirection, ' ', where) +
|
|
129
|
+
xsdPointElement('CameraUpVector', camera.cameraUpVector, ' ', where));
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=writer-camera.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writer-camera.js","sourceRoot":"","sources":["../src/writer-camera.ts"],"names":[],"mappings":"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"}
|
package/dist/writer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"writer.d.ts","sourceRoot":"","sources":["../src/writer.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EACV,UAAU,
|
|
1
|
+
{"version":3,"file":"writer.d.ts","sourceRoot":"","sources":["../src/writer.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EACV,UAAU,EAiBX,MAAM,YAAY,CAAC;AAgBpB;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBjE"}
|
package/dist/writer.js
CHANGED
|
@@ -7,7 +7,11 @@
|
|
|
7
7
|
* Creates .bcfzip files from BCFProject structure
|
|
8
8
|
*/
|
|
9
9
|
import JSZip from 'jszip';
|
|
10
|
-
import {
|
|
10
|
+
import { requireCameraChoice, writeOrthogonalCamera, writePerspectiveCamera, } from './writer-camera.js';
|
|
11
|
+
import { xsdDouble, xsdInt, xsdPointElement } from './numeric.js';
|
|
12
|
+
import { escapeXml } from './xml-text.js';
|
|
13
|
+
import { XML_WHITESPACE_ONLY, xsdDateTime, xsdOptionalDateTime, xsdRequiredString, } from './xsd-required-string.js';
|
|
14
|
+
import { generateUuid, uuidFromSeed } from '@ifc-lite/encoding';
|
|
11
15
|
/**
|
|
12
16
|
* Write a BCFProject to a .bcfzip file
|
|
13
17
|
*
|
|
@@ -63,21 +67,34 @@ function writeVersionFile(zip, version) {
|
|
|
63
67
|
* Uses buildingSMART standard format
|
|
64
68
|
*
|
|
65
69
|
* The root element is renamed between versions: 2.1's project.xsd declares
|
|
66
|
-
* root element `<ProjectExtension>`
|
|
67
|
-
*
|
|
68
|
-
*
|
|
70
|
+
* root element `<ProjectExtension>` as the sequence `Project?`,
|
|
71
|
+
* `ExtensionSchema` -- where `ExtensionSchema` is an `xs:anyURI` with no
|
|
72
|
+
* `minOccurs`, so it is REQUIRED; 3.0's project.xsd instead declares root
|
|
69
73
|
* element `<ProjectInfo>` containing just a required `<Project>`, and has no
|
|
70
74
|
* `ProjectExtension`/`ExtensionSchema` concept at all. The inner
|
|
71
75
|
* `<Project ProjectId="...">`/`<Name>` shape is unchanged between versions.
|
|
76
|
+
*
|
|
77
|
+
* `<ExtensionSchema>` used to be omitted entirely, which made `project.bcfp`
|
|
78
|
+
* fail 2.1 validation ("Element 'ProjectExtension': Missing child element(s).
|
|
79
|
+
* Expected is ( ExtensionSchema )") in EVERY 2.1 archive this package writes
|
|
80
|
+
* -- 2.1 is the default version and `createBCFProject` always sets a project
|
|
81
|
+
* id, so the file is always present. It is now emitted empty. An empty
|
|
82
|
+
* `xs:anyURI` is a valid instance of the type (verified against the vendored
|
|
83
|
+
* schema), and it is the honest value here: this writer ships no
|
|
84
|
+
* `extensions.xsd`, and `BCFProject.extensions` is not serialized, so there is
|
|
85
|
+
* no extension schema to point at. Naming a file we do not write would trade
|
|
86
|
+
* the schema error for a dangling reference; omitting the element keeps the
|
|
87
|
+
* schema error. Empty does neither.
|
|
72
88
|
*/
|
|
73
89
|
function writeProjectFile(zip, project, version) {
|
|
74
90
|
const projectId = project.projectId || generateUuid();
|
|
75
91
|
const nameElement = project.name ? `\n <Name>${escapeXml(project.name)}</Name>` : '';
|
|
76
92
|
const rootElement = version === '3.0' ? 'ProjectInfo' : 'ProjectExtension';
|
|
93
|
+
const extensionSchema = version === '3.0' ? '' : '\n <ExtensionSchema/>';
|
|
77
94
|
const content = `<?xml version="1.0" encoding="UTF-8"?>
|
|
78
95
|
<${rootElement} xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
|
79
|
-
<Project ProjectId="${projectId}">${nameElement}
|
|
80
|
-
</Project
|
|
96
|
+
<Project ProjectId="${escapeXml(projectId)}">${nameElement}
|
|
97
|
+
</Project>${extensionSchema}
|
|
81
98
|
</${rootElement}>`;
|
|
82
99
|
zip.file('project.bcfp', content);
|
|
83
100
|
}
|
|
@@ -90,33 +107,18 @@ function shortGuidHash(guid) {
|
|
|
90
107
|
}
|
|
91
108
|
return h.toString(16).padStart(8, '0');
|
|
92
109
|
}
|
|
93
|
-
/**
|
|
94
|
-
* Sanitize a topic GUID for use as a zip folder name (zip-slip guard).
|
|
95
|
-
*
|
|
96
|
-
* A topic GUID is parsed unvalidated from untrusted markup XML on read, so it
|
|
97
|
-
* can carry path separators or `..`. Using it verbatim as a zip path component
|
|
98
|
-
* on a read-modify-save would let a crafted GUID (`../../evil`) write outside
|
|
99
|
-
* the archive root. Restrict the name to safe filename characters and collapse
|
|
100
|
-
* any dot-run so it can never traverse. The real GUID is still written verbatim
|
|
101
|
-
* as the markup `<Topic Guid>` attribute, which is what readers key off.
|
|
102
|
-
*
|
|
103
|
-
* Sanitization is lossy, so two distinct GUIDs can map to one name (`a?b` and
|
|
104
|
-
* `a:b` both give `a_b`), which would silently overwrite a topic folder. Any
|
|
105
|
-
* name that sanitization changed gets a hash of the original GUID appended,
|
|
106
|
-
* and `usedNames` catches the remaining collisions with a counter suffix.
|
|
107
|
-
*/
|
|
108
|
-
function sanitizeTopicFolderName(guid, usedNames) {
|
|
109
|
-
return sanitizeZipComponent(guid, usedNames, 'topic');
|
|
110
|
-
}
|
|
111
110
|
/**
|
|
112
111
|
* Sanitize an arbitrary GUID for use as a zip path component (zip-slip guard).
|
|
113
112
|
*
|
|
114
113
|
* Shared by both the topic-folder name and the viewpoint file base name: a
|
|
115
114
|
* viewpoint GUID is parsed just as unvalidated from untrusted markup XML on
|
|
116
115
|
* read (reader.ts `parseViewpointContent`) as a topic GUID is, so it carries
|
|
117
|
-
* the same path-traversal hazard
|
|
118
|
-
*
|
|
119
|
-
*
|
|
116
|
+
* the same path-traversal hazard (`../../evil` as a topic GUID would write
|
|
117
|
+
* outside the archive root). Restrict the name to safe filename characters and
|
|
118
|
+
* collapse any dot-run so it can never traverse. `fallback` is used when
|
|
119
|
+
* sanitization strips the name to nothing. The real GUID is still written
|
|
120
|
+
* verbatim as the markup `<Topic Guid>`/`<Viewpoint Guid>` attribute, which is
|
|
121
|
+
* what readers key off.
|
|
120
122
|
*
|
|
121
123
|
* Sanitization is lossy, so two distinct GUIDs can map to one name, which
|
|
122
124
|
* would silently overwrite an entry. Any name that sanitization changed gets
|
|
@@ -139,13 +141,10 @@ function sanitizeZipComponent(raw, usedNames, fallback) {
|
|
|
139
141
|
usedNames.add(candidate);
|
|
140
142
|
return candidate;
|
|
141
143
|
}
|
|
142
|
-
/**
|
|
143
|
-
* Write a topic folder with all its contents
|
|
144
|
-
*/
|
|
144
|
+
/** Write a topic folder with all its contents. */
|
|
145
145
|
async function writeTopicFolder(zip, topic, version, usedFolderNames) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return;
|
|
146
|
+
// Every entry below passes createFolders:false: JSZip otherwise adds a "<guid>/" directory entry, which Solibri refuses (#3612).
|
|
147
|
+
const folderName = sanitizeZipComponent(topic.guid, usedFolderNames, 'topic');
|
|
149
148
|
// Sanitize each viewpoint GUID once, up front, so the markup <Viewpoint>
|
|
150
149
|
// reference (written below) and the zip entry (written in
|
|
151
150
|
// writeViewpointFiles) always agree on the same file name. A topic-scoped
|
|
@@ -154,10 +153,10 @@ async function writeTopicFolder(zip, topic, version, usedFolderNames) {
|
|
|
154
153
|
const usedViewpointNames = new Set();
|
|
155
154
|
const viewpointBaseNames = topic.viewpoints.map((vp) => sanitizeZipComponent(vp.guid, usedViewpointNames, 'viewpoint'));
|
|
156
155
|
// Write markup.bcf
|
|
157
|
-
writeMarkupFile(
|
|
156
|
+
writeMarkupFile(zip, folderName, topic, version, viewpointBaseNames);
|
|
158
157
|
// Write viewpoints
|
|
159
158
|
for (let i = 0; i < topic.viewpoints.length; i++) {
|
|
160
|
-
await writeViewpointFiles(
|
|
159
|
+
await writeViewpointFiles(zip, folderName, topic.viewpoints[i], viewpointBaseNames[i], version);
|
|
161
160
|
}
|
|
162
161
|
}
|
|
163
162
|
/**
|
|
@@ -173,17 +172,8 @@ function snapshotExt(viewpoint) {
|
|
|
173
172
|
}
|
|
174
173
|
return 'png';
|
|
175
174
|
}
|
|
176
|
-
/**
|
|
177
|
-
|
|
178
|
-
* Uses buildingSMART standard format
|
|
179
|
-
*/
|
|
180
|
-
// BCF 3.0's markup.xsd types `Topic/@TopicType` and `Topic/@TopicStatus` as
|
|
181
|
-
// `NonEmptyOrBlankString`: after XML whitespace (#x9, #xA, #xD, #x20) is
|
|
182
|
-
// collapsed, the value must have length >= 1. A value that is present but
|
|
183
|
-
// consists entirely of XML whitespace collapses to nothing and is therefore
|
|
184
|
-
// as invalid as an absent one.
|
|
185
|
-
const XML_WHITESPACE_ONLY = /^[\t\n\r ]*$/;
|
|
186
|
-
function writeMarkupFile(folder, topic, version, viewpointBaseNames) {
|
|
175
|
+
/** Write markup.bcf -- buildingSMART standard format. */
|
|
176
|
+
function writeMarkupFile(zip, folderName, topic, version, viewpointBaseNames) {
|
|
187
177
|
// BCF 3.0's markup.xsd tightens `Topic/@TopicType` and `Topic/@TopicStatus`
|
|
188
178
|
// from optional (2.1) to `use="required"`. Omitting the attribute -
|
|
189
179
|
// which is what 2.1 output does when the value is unset - produces
|
|
@@ -229,7 +219,8 @@ function writeMarkupFile(folder, topic, version, viewpointBaseNames) {
|
|
|
229
219
|
content += `\n <Priority>${escapeXml(topic.priority)}</Priority>`;
|
|
230
220
|
}
|
|
231
221
|
if (topic.index !== undefined) {
|
|
232
|
-
|
|
222
|
+
// `Index` is the writer's only xs:int; see xsdInt for what that excludes.
|
|
223
|
+
content += `\n <Index>${xsdInt(topic.index, 'Topic/Index', `topic "${topic.guid}"`)}</Index>`;
|
|
233
224
|
}
|
|
234
225
|
if (topic.labels && topic.labels.length > 0) {
|
|
235
226
|
// BCF 3.0's markup.xsd wraps labels in ONE `<Labels>` container holding
|
|
@@ -251,16 +242,18 @@ function writeMarkupFile(folder, topic, version, viewpointBaseNames) {
|
|
|
251
242
|
}
|
|
252
243
|
}
|
|
253
244
|
}
|
|
254
|
-
content += `\n <CreationDate>${
|
|
255
|
-
content += `\n <CreationAuthor>${
|
|
256
|
-
|
|
257
|
-
|
|
245
|
+
content += `\n <CreationDate>${xsdDateTime(topic.creationDate, 'Topic/CreationDate', `topic "${topic.guid}"`)}</CreationDate>`;
|
|
246
|
+
content += `\n <CreationAuthor>${xsdRequiredString(topic.creationAuthor, 'Topic/CreationAuthor', `topic "${topic.guid}"`, version)}</CreationAuthor>`;
|
|
247
|
+
const topicModifiedDate = xsdOptionalDateTime(topic.modifiedDate);
|
|
248
|
+
if (topicModifiedDate) {
|
|
249
|
+
content += `\n <ModifiedDate>${topicModifiedDate}</ModifiedDate>`;
|
|
258
250
|
// BCF spec requires ModifiedAuthor when ModifiedDate is present
|
|
259
251
|
const modifiedAuthor = topic.modifiedAuthor || topic.creationAuthor;
|
|
260
|
-
content += `\n <ModifiedAuthor>${
|
|
252
|
+
content += `\n <ModifiedAuthor>${xsdRequiredString(modifiedAuthor, 'Topic/ModifiedAuthor', `topic "${topic.guid}"`, version)}</ModifiedAuthor>`;
|
|
261
253
|
}
|
|
262
|
-
|
|
263
|
-
|
|
254
|
+
const dueDate = xsdOptionalDateTime(topic.dueDate);
|
|
255
|
+
if (dueDate) {
|
|
256
|
+
content += `\n <DueDate>${dueDate}</DueDate>`;
|
|
264
257
|
}
|
|
265
258
|
if (topic.assignedTo) {
|
|
266
259
|
content += `\n <AssignedTo>${escapeXml(topic.assignedTo)}</AssignedTo>`;
|
|
@@ -283,8 +276,8 @@ function writeMarkupFile(folder, topic, version, viewpointBaseNames) {
|
|
|
283
276
|
// directly under <Topic> (buildingSMART/BCF-XML markup.xsd, Topic).
|
|
284
277
|
if (version === '3.0')
|
|
285
278
|
content += `\n <DocumentReferences>`;
|
|
286
|
-
for (
|
|
287
|
-
content += writeDocumentReference(
|
|
279
|
+
for (let i = 0; i < topic.documentReferences.length; i++) {
|
|
280
|
+
content += writeDocumentReference(topic.documentReferences[i], version, topic.guid, i);
|
|
288
281
|
}
|
|
289
282
|
if (version === '3.0')
|
|
290
283
|
content += `\n </DocumentReferences>`;
|
|
@@ -308,14 +301,15 @@ function writeMarkupFile(folder, topic, version, viewpointBaseNames) {
|
|
|
308
301
|
const commentXml = (indent) => topic.comments
|
|
309
302
|
.map((comment) => {
|
|
310
303
|
let c = `\n${indent}<Comment Guid="${escapeXml(comment.guid)}">`;
|
|
311
|
-
c += `\n${indent} <Date>${
|
|
312
|
-
c += `\n${indent} <Author>${
|
|
304
|
+
c += `\n${indent} <Date>${xsdDateTime(comment.date, 'Comment/Date', `comment "${comment.guid}"`)}</Date>`;
|
|
305
|
+
c += `\n${indent} <Author>${xsdRequiredString(comment.author, 'Comment/Author', `comment "${comment.guid}"`, version)}</Author>`;
|
|
313
306
|
c += `\n${indent} <Comment>${escapeXml(comment.comment)}</Comment>`;
|
|
314
307
|
if (comment.viewpointGuid) {
|
|
315
308
|
c += `\n${indent} <Viewpoint Guid="${escapeXml(comment.viewpointGuid)}"/>`;
|
|
316
309
|
}
|
|
317
|
-
|
|
318
|
-
|
|
310
|
+
const commentModifiedDate = xsdOptionalDateTime(comment.modifiedDate);
|
|
311
|
+
if (commentModifiedDate) {
|
|
312
|
+
c += `\n${indent} <ModifiedDate>${commentModifiedDate}</ModifiedDate>`;
|
|
319
313
|
}
|
|
320
314
|
if (comment.modifiedAuthor) {
|
|
321
315
|
c += `\n${indent} <ModifiedAuthor>${escapeXml(comment.modifiedAuthor)}</ModifiedAuthor>`;
|
|
@@ -375,12 +369,12 @@ function writeMarkupFile(folder, topic, version, viewpointBaseNames) {
|
|
|
375
369
|
content += viewpointXml(' ');
|
|
376
370
|
}
|
|
377
371
|
content += `\n</Markup>`;
|
|
378
|
-
|
|
372
|
+
zip.file(`${folderName}/markup.bcf`, content, { createFolders: false });
|
|
379
373
|
}
|
|
380
374
|
/**
|
|
381
375
|
* Write viewpoint files (bcfv and snapshot)
|
|
382
376
|
*/
|
|
383
|
-
async function writeViewpointFiles(
|
|
377
|
+
async function writeViewpointFiles(zip, folderName, viewpoint, baseName, version) {
|
|
384
378
|
// Use standard buildingSMART naming convention: Viewpoint_<guid>.bcfv, but
|
|
385
379
|
// the file name component is the sanitized base name (zip-slip guard) --
|
|
386
380
|
// the SAME one the caller wrote into the markup <Viewpoint> reference, so
|
|
@@ -394,19 +388,24 @@ async function writeViewpointFiles(folder, viewpoint, baseName, version) {
|
|
|
394
388
|
if (viewpoint.components) {
|
|
395
389
|
content += writeComponents(viewpoint.components, version);
|
|
396
390
|
}
|
|
397
|
-
//
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
// Write
|
|
391
|
+
// Every number below reaches an XSD numeric type, so each goes through
|
|
392
|
+
// `numeric.ts`'s write-side guards; `where` is what puts the offending
|
|
393
|
+
// viewpoint's guid in the error, as the camera checks already do.
|
|
394
|
+
const where = `viewpoint "${viewpoint.guid}"`;
|
|
395
|
+
// Write the cameras. ORTHOGONAL FIRST -- see requireCameraChoice for why the
|
|
396
|
+
// order is not free, and for the 3.0 cardinality rule enforced here.
|
|
397
|
+
requireCameraChoice(viewpoint, version);
|
|
402
398
|
if (viewpoint.orthogonalCamera) {
|
|
403
399
|
content += writeOrthogonalCamera(viewpoint.orthogonalCamera, version, viewpoint.guid);
|
|
404
400
|
}
|
|
401
|
+
if (viewpoint.perspectiveCamera) {
|
|
402
|
+
content += writePerspectiveCamera(viewpoint.perspectiveCamera, version, viewpoint.guid);
|
|
403
|
+
}
|
|
405
404
|
// Write lines
|
|
406
405
|
if (viewpoint.lines && viewpoint.lines.length > 0) {
|
|
407
406
|
content += `\n <Lines>`;
|
|
408
407
|
for (const line of viewpoint.lines) {
|
|
409
|
-
content += writeLine(line);
|
|
408
|
+
content += writeLine(line, where);
|
|
410
409
|
}
|
|
411
410
|
content += `\n </Lines>`;
|
|
412
411
|
}
|
|
@@ -414,7 +413,7 @@ async function writeViewpointFiles(folder, viewpoint, baseName, version) {
|
|
|
414
413
|
if (viewpoint.clippingPlanes && viewpoint.clippingPlanes.length > 0) {
|
|
415
414
|
content += `\n <ClippingPlanes>`;
|
|
416
415
|
for (const plane of viewpoint.clippingPlanes) {
|
|
417
|
-
content += writeClippingPlane(plane);
|
|
416
|
+
content += writeClippingPlane(plane, where);
|
|
418
417
|
}
|
|
419
418
|
content += `\n </ClippingPlanes>`;
|
|
420
419
|
}
|
|
@@ -430,21 +429,21 @@ async function writeViewpointFiles(folder, viewpoint, baseName, version) {
|
|
|
430
429
|
if (version === '3.0') {
|
|
431
430
|
content += `\n <Bitmaps>`;
|
|
432
431
|
for (const bitmap of viewpoint.bitmaps) {
|
|
433
|
-
content += writeBitmap(bitmap, version);
|
|
432
|
+
content += writeBitmap(bitmap, version, where);
|
|
434
433
|
}
|
|
435
434
|
content += `\n </Bitmaps>`;
|
|
436
435
|
}
|
|
437
436
|
else {
|
|
438
437
|
for (const bitmap of viewpoint.bitmaps) {
|
|
439
|
-
content += writeBitmap(bitmap, version);
|
|
438
|
+
content += writeBitmap(bitmap, version, where);
|
|
440
439
|
}
|
|
441
440
|
}
|
|
442
441
|
}
|
|
443
442
|
content += `\n</VisualizationInfo>`;
|
|
444
|
-
|
|
443
|
+
zip.file(`${folderName}/${filename}`, content, { createFolders: false });
|
|
445
444
|
// Write snapshot
|
|
446
445
|
if (viewpoint.snapshotData) {
|
|
447
|
-
|
|
446
|
+
zip.file(`${folderName}/${snapshotName}`, viewpoint.snapshotData, { createFolders: false });
|
|
448
447
|
}
|
|
449
448
|
else if (viewpoint.snapshot && viewpoint.snapshot.startsWith('data:')) {
|
|
450
449
|
// Convert data URL to binary
|
|
@@ -456,7 +455,7 @@ async function writeViewpointFiles(folder, viewpoint, baseName, version) {
|
|
|
456
455
|
for (let i = 0; i < binaryString.length; i++) {
|
|
457
456
|
bytes[i] = binaryString.charCodeAt(i);
|
|
458
457
|
}
|
|
459
|
-
|
|
458
|
+
zip.file(`${folderName}/${snapshotName}`, bytes, { createFolders: false });
|
|
460
459
|
}
|
|
461
460
|
catch (e) {
|
|
462
461
|
// Skip a single malformed snapshot data URL rather than aborting the export
|
|
@@ -600,105 +599,18 @@ function writeColoringEntry(coloring, version) {
|
|
|
600
599
|
content += `\n </Color>`;
|
|
601
600
|
return content;
|
|
602
601
|
}
|
|
603
|
-
/**
|
|
604
|
-
* Require a positive AspectRatio for a BCF 3.0 camera and return the element
|
|
605
|
-
* to append.
|
|
606
|
-
*
|
|
607
|
-
* v3_0/visinfo.xsd adds `<AspectRatio>` (type `PositiveDouble`, i.e.
|
|
608
|
-
* `xs:double` with `minExclusive value="0"`) as a REQUIRED, no-minOccurs
|
|
609
|
-
* child of both `OrthogonalCamera` and `PerspectiveCamera`. 2.1 has no such
|
|
610
|
-
* element. We refuse to invent a value (there is no safe default aspect
|
|
611
|
-
* ratio) because that would assert a value the caller never chose; instead
|
|
612
|
-
* we fail the write so the caller supplies one -- same policy as the
|
|
613
|
-
* `Topic/@TopicType`/`Topic/@TopicStatus` checks in {@link writeMarkupFile}.
|
|
614
|
-
*/
|
|
615
|
-
function requireAspectRatioElement(aspectRatio, viewpointGuid) {
|
|
616
|
-
if (aspectRatio === undefined || !(aspectRatio > 0)) {
|
|
617
|
-
throw new Error(`BCF 3.0 requires a positive Camera/AspectRatio (viewpoint "${viewpointGuid}" has none). ` +
|
|
618
|
-
`Set the camera's aspectRatio before writing a 3.0 file.`);
|
|
619
|
-
}
|
|
620
|
-
return `\n <AspectRatio>${aspectRatio}</AspectRatio>`;
|
|
621
|
-
}
|
|
622
|
-
/**
|
|
623
|
-
* Write perspective camera XML
|
|
624
|
-
*/
|
|
625
|
-
function writePerspectiveCamera(camera, version, viewpointGuid) {
|
|
626
|
-
const aspectRatioElement = version === '3.0' ? requireAspectRatioElement(camera.aspectRatio, viewpointGuid) : '';
|
|
627
|
-
return `\n <PerspectiveCamera>
|
|
628
|
-
<CameraViewPoint>
|
|
629
|
-
<X>${camera.cameraViewPoint.x}</X>
|
|
630
|
-
<Y>${camera.cameraViewPoint.y}</Y>
|
|
631
|
-
<Z>${camera.cameraViewPoint.z}</Z>
|
|
632
|
-
</CameraViewPoint>
|
|
633
|
-
<CameraDirection>
|
|
634
|
-
<X>${camera.cameraDirection.x}</X>
|
|
635
|
-
<Y>${camera.cameraDirection.y}</Y>
|
|
636
|
-
<Z>${camera.cameraDirection.z}</Z>
|
|
637
|
-
</CameraDirection>
|
|
638
|
-
<CameraUpVector>
|
|
639
|
-
<X>${camera.cameraUpVector.x}</X>
|
|
640
|
-
<Y>${camera.cameraUpVector.y}</Y>
|
|
641
|
-
<Z>${camera.cameraUpVector.z}</Z>
|
|
642
|
-
</CameraUpVector>
|
|
643
|
-
<FieldOfView>${camera.fieldOfView}</FieldOfView>${aspectRatioElement}
|
|
644
|
-
</PerspectiveCamera>`;
|
|
645
|
-
}
|
|
646
|
-
/**
|
|
647
|
-
* Write orthogonal camera XML
|
|
648
|
-
*/
|
|
649
|
-
function writeOrthogonalCamera(camera, version, viewpointGuid) {
|
|
650
|
-
const aspectRatioElement = version === '3.0' ? requireAspectRatioElement(camera.aspectRatio, viewpointGuid) : '';
|
|
651
|
-
return `\n <OrthogonalCamera>
|
|
652
|
-
<CameraViewPoint>
|
|
653
|
-
<X>${camera.cameraViewPoint.x}</X>
|
|
654
|
-
<Y>${camera.cameraViewPoint.y}</Y>
|
|
655
|
-
<Z>${camera.cameraViewPoint.z}</Z>
|
|
656
|
-
</CameraViewPoint>
|
|
657
|
-
<CameraDirection>
|
|
658
|
-
<X>${camera.cameraDirection.x}</X>
|
|
659
|
-
<Y>${camera.cameraDirection.y}</Y>
|
|
660
|
-
<Z>${camera.cameraDirection.z}</Z>
|
|
661
|
-
</CameraDirection>
|
|
662
|
-
<CameraUpVector>
|
|
663
|
-
<X>${camera.cameraUpVector.x}</X>
|
|
664
|
-
<Y>${camera.cameraUpVector.y}</Y>
|
|
665
|
-
<Z>${camera.cameraUpVector.z}</Z>
|
|
666
|
-
</CameraUpVector>
|
|
667
|
-
<ViewToWorldScale>${camera.viewToWorldScale}</ViewToWorldScale>${aspectRatioElement}
|
|
668
|
-
</OrthogonalCamera>`;
|
|
669
|
-
}
|
|
670
602
|
/**
|
|
671
603
|
* Write line XML
|
|
672
604
|
*/
|
|
673
|
-
function writeLine(line) {
|
|
674
|
-
return `\n <Line
|
|
675
|
-
<StartPoint>
|
|
676
|
-
<X>${line.startPoint.x}</X>
|
|
677
|
-
<Y>${line.startPoint.y}</Y>
|
|
678
|
-
<Z>${line.startPoint.z}</Z>
|
|
679
|
-
</StartPoint>
|
|
680
|
-
<EndPoint>
|
|
681
|
-
<X>${line.endPoint.x}</X>
|
|
682
|
-
<Y>${line.endPoint.y}</Y>
|
|
683
|
-
<Z>${line.endPoint.z}</Z>
|
|
684
|
-
</EndPoint>
|
|
605
|
+
function writeLine(line, where) {
|
|
606
|
+
return `\n <Line>${xsdPointElement('StartPoint', line.startPoint, ' ', where)}${xsdPointElement('EndPoint', line.endPoint, ' ', where)}
|
|
685
607
|
</Line>`;
|
|
686
608
|
}
|
|
687
609
|
/**
|
|
688
610
|
* Write clipping plane XML
|
|
689
611
|
*/
|
|
690
|
-
function writeClippingPlane(plane) {
|
|
691
|
-
return `\n <ClippingPlane
|
|
692
|
-
<Location>
|
|
693
|
-
<X>${plane.location.x}</X>
|
|
694
|
-
<Y>${plane.location.y}</Y>
|
|
695
|
-
<Z>${plane.location.z}</Z>
|
|
696
|
-
</Location>
|
|
697
|
-
<Direction>
|
|
698
|
-
<X>${plane.direction.x}</X>
|
|
699
|
-
<Y>${plane.direction.y}</Y>
|
|
700
|
-
<Z>${plane.direction.z}</Z>
|
|
701
|
-
</Direction>
|
|
612
|
+
function writeClippingPlane(plane, where) {
|
|
613
|
+
return `\n <ClippingPlane>${xsdPointElement('Location', plane.location, ' ', where)}${xsdPointElement('Direction', plane.direction, ' ', where)}
|
|
702
614
|
</ClippingPlane>`;
|
|
703
615
|
}
|
|
704
616
|
/**
|
|
@@ -716,28 +628,13 @@ function writeClippingPlane(plane) {
|
|
|
716
628
|
* otherwise. `BCFBitmap.format` stays typed `'PNG' | 'JPG'`; we only
|
|
717
629
|
* lowercase it on the wire for 3.0.
|
|
718
630
|
*/
|
|
719
|
-
function writeBitmap(bitmap, version) {
|
|
631
|
+
function writeBitmap(bitmap, version, where) {
|
|
720
632
|
const formatTag = version === '3.0' ? 'Format' : 'Bitmap';
|
|
721
633
|
const formatValue = version === '3.0' ? bitmap.format.toLowerCase() : bitmap.format;
|
|
722
634
|
return `\n <Bitmap>
|
|
723
635
|
<${formatTag}>${formatValue}</${formatTag}>
|
|
724
|
-
<Reference>${escapeXml(bitmap.reference)}</Reference
|
|
725
|
-
<
|
|
726
|
-
<X>${bitmap.location.x}</X>
|
|
727
|
-
<Y>${bitmap.location.y}</Y>
|
|
728
|
-
<Z>${bitmap.location.z}</Z>
|
|
729
|
-
</Location>
|
|
730
|
-
<Normal>
|
|
731
|
-
<X>${bitmap.normal.x}</X>
|
|
732
|
-
<Y>${bitmap.normal.y}</Y>
|
|
733
|
-
<Z>${bitmap.normal.z}</Z>
|
|
734
|
-
</Normal>
|
|
735
|
-
<Up>
|
|
736
|
-
<X>${bitmap.up.x}</X>
|
|
737
|
-
<Y>${bitmap.up.y}</Y>
|
|
738
|
-
<Z>${bitmap.up.z}</Z>
|
|
739
|
-
</Up>
|
|
740
|
-
<Height>${bitmap.height}</Height>
|
|
636
|
+
<Reference>${escapeXml(bitmap.reference)}</Reference>${xsdPointElement('Location', bitmap.location, ' ', where)}${xsdPointElement('Normal', bitmap.normal, ' ', where)}${xsdPointElement('Up', bitmap.up, ' ', where)}
|
|
637
|
+
<Height>${xsdDouble(bitmap.height, 'Bitmap/Height', where)}</Height>
|
|
741
638
|
</Bitmap>`;
|
|
742
639
|
}
|
|
743
640
|
/**
|
|
@@ -774,8 +671,9 @@ function writeHeaderFile(file, indent, version) {
|
|
|
774
671
|
if (file.filename) {
|
|
775
672
|
content += `\n${indent} <Filename>${escapeXml(file.filename)}</Filename>`;
|
|
776
673
|
}
|
|
777
|
-
|
|
778
|
-
|
|
674
|
+
const fileDate = xsdOptionalDateTime(file.date);
|
|
675
|
+
if (fileDate) {
|
|
676
|
+
content += `\n${indent} <Date>${fileDate}</Date>`;
|
|
779
677
|
}
|
|
780
678
|
if (file.reference) {
|
|
781
679
|
content += `\n${indent} <Reference>${escapeXml(file.reference)}</Reference>`;
|
|
@@ -812,8 +710,41 @@ function writeBimSnippet(snippet, version) {
|
|
|
812
710
|
* the 2.1-shaped fallback so 2.1-authored data written as 3.0 still emits
|
|
813
711
|
* something.
|
|
814
712
|
*/
|
|
815
|
-
function writeDocumentReference(docRef, version) {
|
|
816
|
-
|
|
713
|
+
function writeDocumentReference(docRef, version, topicGuid, index) {
|
|
714
|
+
// 2.1's markup.xsd declares `Guid` with no `use`, so it is optional there;
|
|
715
|
+
// 3.0's `DocumentReferenceAttributes` declares it `use="required"`, so
|
|
716
|
+
// omitting it made every 3.0 topic carrying a guid-less document reference
|
|
717
|
+
// write an invalid `markup.bcf` -- and markup.bcf IS the issue, so a viewer
|
|
718
|
+
// that rejects it drops the topic whole (#3612). Mint one rather than
|
|
719
|
+
// refuse: this guid names only itself (nothing in the archive refers to a
|
|
720
|
+
// DocumentReference the way `RelatedTopic` refers to a topic), so a
|
|
721
|
+
// generated value loses nothing. Refusal is reserved for values that assert
|
|
722
|
+
// something only the caller knows -- `AspectRatio`, `TopicType` -- and would
|
|
723
|
+
// fail the whole export over a field 2.1 says is optional. 2.1 keeps the
|
|
724
|
+
// attribute absent: fabricating an identifier there buys no schema
|
|
725
|
+
// conformance at all, and would put an identifier the caller never chose
|
|
726
|
+
// into the file a user downloads.
|
|
727
|
+
//
|
|
728
|
+
// DERIVED, not random. `generateUuid()` here would make two exports of one
|
|
729
|
+
// unchanged project differ in bytes -- and differ again on every subsequent
|
|
730
|
+
// write, because nothing kept the value. `uuidFromSeed` is the same
|
|
731
|
+
// generator `@ifc-lite/clash` anchors its topic guids with, so the guid is a
|
|
732
|
+
// pure function of the topic, the document pointed at, and the position
|
|
733
|
+
// within the topic; all three are in the seed because two references under
|
|
734
|
+
// one topic can name the same document, and one document can appear under
|
|
735
|
+
// two topics. `docKey` names the document in the SAME precedence the 3.0
|
|
736
|
+
// body below uses -- seeding from the url alone left every `documentGuid`
|
|
737
|
+
// reference on the empty string, so its guid ignored the document entirely
|
|
738
|
+
// and two references naming different documents could collide. The result
|
|
739
|
+
// is written back onto `docRef` so the in-memory project agrees with the
|
|
740
|
+
// file rather than reporting no guid at all.
|
|
741
|
+
const docKey = docRef.documentGuid ?? docRef.url ?? docRef.referencedDocument ?? '';
|
|
742
|
+
const guid = version === '3.0'
|
|
743
|
+
? docRef.guid || uuidFromSeed(`${topicGuid}|${docKey}|${index}`)
|
|
744
|
+
: docRef.guid;
|
|
745
|
+
if (version === '3.0' && !docRef.guid)
|
|
746
|
+
docRef.guid = guid;
|
|
747
|
+
const guidAttr = guid ? ` Guid="${escapeXml(guid)}"` : '';
|
|
817
748
|
if (version === '3.0') {
|
|
818
749
|
let content = `\n <DocumentReference${guidAttr}>`;
|
|
819
750
|
if (docRef.documentGuid) {
|
|
@@ -839,15 +770,4 @@ function writeDocumentReference(docRef, version) {
|
|
|
839
770
|
content += `\n </DocumentReference>`;
|
|
840
771
|
return content;
|
|
841
772
|
}
|
|
842
|
-
/**
|
|
843
|
-
* Escape XML special characters
|
|
844
|
-
*/
|
|
845
|
-
function escapeXml(str) {
|
|
846
|
-
return str
|
|
847
|
-
.replace(/&/g, '&')
|
|
848
|
-
.replace(/</g, '<')
|
|
849
|
-
.replace(/>/g, '>')
|
|
850
|
-
.replace(/"/g, '"')
|
|
851
|
-
.replace(/'/g, ''');
|
|
852
|
-
}
|
|
853
773
|
//# sourceMappingURL=writer.js.map
|