visio-react-viewer 0.0.2 → 0.1.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/LICENSE +202 -0
- package/NOTICE +10 -0
- package/README.md +5 -3
- package/dist/assets/edit-worker-DRgXgRud.js +55992 -0
- package/dist/assets/parse-worker-B8EBQQpD.js +53971 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4131 -0
- package/dist/types/packages/bindings/src/common.d.ts +29 -0
- package/dist/types/packages/bindings/src/react.d.ts +10 -0
- package/dist/types/packages/react/src/index.d.ts +3 -0
- package/dist/types/src/arrowheads.d.ts +3 -0
- package/dist/types/src/binding.d.ts +27 -0
- package/dist/types/src/contract.d.ts +32 -0
- package/dist/types/src/controller.d.ts +66 -0
- package/dist/types/src/demo-document.d.ts +3 -0
- package/dist/types/src/diagnostics.d.ts +9 -0
- package/dist/types/src/document-history.d.ts +42 -0
- package/dist/types/src/document-text-search.d.ts +44 -0
- package/dist/types/src/edit-commands.d.ts +3 -0
- package/dist/types/src/edit-error.d.ts +4 -0
- package/dist/types/src/edit-worker.d.ts +1 -0
- package/dist/types/src/export-svg.d.ts +25 -0
- package/dist/types/src/foreign-vector-budget.d.ts +24 -0
- package/dist/types/src/index.d.ts +14 -0
- package/dist/types/src/paragraph-layout.d.ts +18 -0
- package/dist/types/src/parse-worker.d.ts +1 -0
- package/dist/types/src/print-snapshot.d.ts +87 -0
- package/dist/types/src/render-fill.d.ts +2 -0
- package/dist/types/src/render-foreign-vector-shape.d.ts +4 -0
- package/dist/types/src/render-foreign-vector.d.ts +3 -0
- package/dist/types/src/render-image.d.ts +4 -0
- package/dist/types/src/render-resources.d.ts +10 -0
- package/dist/types/src/render-svg.d.ts +17 -0
- package/dist/types/src/render-text.d.ts +4 -0
- package/dist/types/src/scene-details.d.ts +7 -0
- package/dist/types/src/scene-validation.d.ts +4 -0
- package/dist/types/src/shape-inspector.d.ts +5 -0
- package/dist/types/src/snapshot-scene.d.ts +8 -0
- package/dist/types/src/styles.d.ts +2 -0
- package/dist/types/src/text-layout.d.ts +45 -0
- package/dist/types/src/viewer-chrome.d.ts +10 -0
- package/dist/types/src/viewer-edit-controls.d.ts +12 -0
- package/dist/types/src/viewer-element.d.ts +44 -0
- package/dist/types/src/viewer-geometry-controls.d.ts +14 -0
- package/dist/types/src/viewer-input.d.ts +10 -0
- package/dist/types/src/viewer-layer-controls.d.ts +5 -0
- package/dist/types/src/viewer-layers.d.ts +22 -0
- package/dist/types/src/viewer-search.d.ts +10 -0
- package/dist/types/src/worker-editor.d.ts +23 -0
- package/dist/types/src/worker-parser.d.ts +13 -0
- package/package.json +22 -7
- package/index.js +0 -1
package/dist/index.js
ADDED
|
@@ -0,0 +1,4131 @@
|
|
|
1
|
+
import { VISIO_RASTER_IMAGE_LIMITS, VISIO_VISIBILITY_LIMITS, getVisioPageLayers, inspectVisioRasterImage, parseVsdx, resolveVisioPageVisibility, validateVisioForeignVector } from "ooxml-core/visio";
|
|
2
|
+
import { createElement, forwardRef, useEffect, useImperativeHandle, useRef } from "react";
|
|
3
|
+
export * from "visio-core";
|
|
4
|
+
//#region src/document-history.ts
|
|
5
|
+
var EMPTY_EDIT_STATE = Object.freeze({
|
|
6
|
+
sourceAvailable: false,
|
|
7
|
+
busy: false,
|
|
8
|
+
dirty: false,
|
|
9
|
+
canUndo: false,
|
|
10
|
+
canRedo: false,
|
|
11
|
+
historyTruncated: false,
|
|
12
|
+
error: null,
|
|
13
|
+
diagnostics: Object.freeze([])
|
|
14
|
+
});
|
|
15
|
+
var EDIT_HISTORY_LIMITS = Object.freeze({
|
|
16
|
+
maxBytes: 100663296,
|
|
17
|
+
maxEntries: 20
|
|
18
|
+
});
|
|
19
|
+
/** Private controller-owned bytes; transient worker copies and models are separate from this budget. */
|
|
20
|
+
var DocumentHistory = class {
|
|
21
|
+
original;
|
|
22
|
+
#entries;
|
|
23
|
+
#index = 0;
|
|
24
|
+
#truncated = false;
|
|
25
|
+
constructor(bytes) {
|
|
26
|
+
this.original = {
|
|
27
|
+
bytes: Uint8Array.from(bytes),
|
|
28
|
+
diagnostics: Object.freeze([])
|
|
29
|
+
};
|
|
30
|
+
this.#entries = [this.original];
|
|
31
|
+
}
|
|
32
|
+
get current() {
|
|
33
|
+
return this.#entries[this.#index];
|
|
34
|
+
}
|
|
35
|
+
get undoTarget() {
|
|
36
|
+
return this.#entries[this.#index - 1];
|
|
37
|
+
}
|
|
38
|
+
get redoTarget() {
|
|
39
|
+
return this.#entries[this.#index + 1];
|
|
40
|
+
}
|
|
41
|
+
get state() {
|
|
42
|
+
return Object.freeze({
|
|
43
|
+
...EMPTY_EDIT_STATE,
|
|
44
|
+
sourceAvailable: true,
|
|
45
|
+
dirty: this.current !== this.original,
|
|
46
|
+
canUndo: !!this.undoTarget,
|
|
47
|
+
canRedo: !!this.redoTarget,
|
|
48
|
+
historyTruncated: this.#truncated,
|
|
49
|
+
diagnostics: this.current.diagnostics
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
append(bytes, diagnostics) {
|
|
53
|
+
const snapshot = {
|
|
54
|
+
bytes: Uint8Array.from(bytes),
|
|
55
|
+
diagnostics: Object.freeze(diagnostics.map((note) => Object.freeze({
|
|
56
|
+
code: note.code,
|
|
57
|
+
message: note.message
|
|
58
|
+
})))
|
|
59
|
+
};
|
|
60
|
+
this.#entries = this.#entries.slice(0, this.#index + 1);
|
|
61
|
+
this.#entries.push(snapshot);
|
|
62
|
+
this.#index++;
|
|
63
|
+
const retained = () => /* @__PURE__ */ new Set([this.original, ...this.#entries]);
|
|
64
|
+
while (this.#entries.length > EDIT_HISTORY_LIMITS.maxEntries || [...retained()].reduce((sum, entry) => sum + entry.bytes.byteLength, 0) > EDIT_HISTORY_LIMITS.maxBytes) {
|
|
65
|
+
if (this.#index === 0) throw new Error("Source snapshots exceed the retained history limit.");
|
|
66
|
+
this.#entries.shift();
|
|
67
|
+
this.#index--;
|
|
68
|
+
this.#truncated = true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
move(target) {
|
|
72
|
+
const index = this.#entries.indexOf(target);
|
|
73
|
+
if (index < 0 || Math.abs(index - this.#index) !== 1) throw new Error("The history target is stale.");
|
|
74
|
+
this.#index = index;
|
|
75
|
+
}
|
|
76
|
+
export() {
|
|
77
|
+
return {
|
|
78
|
+
bytes: Uint8Array.from(this.current.bytes),
|
|
79
|
+
dirty: this.current !== this.original,
|
|
80
|
+
diagnostics: this.current.diagnostics
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region src/edit-commands.ts
|
|
86
|
+
/** Bound cloning and strip arbitrary host properties. Semantic validation belongs to core. */
|
|
87
|
+
function snapshotEdits(edits) {
|
|
88
|
+
if (!Array.isArray(edits) || edits.length > 1e3) throw new Error("At most 1000 edits are accepted per operation.");
|
|
89
|
+
let characters = 0;
|
|
90
|
+
return edits.map((command) => {
|
|
91
|
+
if (!command || typeof command.pageId !== "string" || !command.pageId || command.pageId.length > 256 || typeof command.shapeId !== "string" || !command.shapeId || command.shapeId.length > 256) throw new Error("Invalid edit command.");
|
|
92
|
+
const target = {
|
|
93
|
+
pageId: command.pageId,
|
|
94
|
+
shapeId: command.shapeId
|
|
95
|
+
};
|
|
96
|
+
const text = (value) => {
|
|
97
|
+
if (typeof value !== "string") throw new Error("Invalid edit text.");
|
|
98
|
+
characters += value.length;
|
|
99
|
+
if (characters > 1e6) throw new Error("Edit text exceeds the one-million-character limit.");
|
|
100
|
+
return value;
|
|
101
|
+
};
|
|
102
|
+
const numbers = (...values) => {
|
|
103
|
+
if (values.some((value) => typeof value !== "number" || !Number.isFinite(value))) throw new Error("Invalid geometry edit coordinates or dimensions.");
|
|
104
|
+
};
|
|
105
|
+
switch (command.type) {
|
|
106
|
+
case "replace-plain-text": return {
|
|
107
|
+
type: command.type,
|
|
108
|
+
...target,
|
|
109
|
+
text: text(command.text)
|
|
110
|
+
};
|
|
111
|
+
case "delete-shape": return {
|
|
112
|
+
type: command.type,
|
|
113
|
+
...target
|
|
114
|
+
};
|
|
115
|
+
case "move-shape":
|
|
116
|
+
numbers(command.x, command.y);
|
|
117
|
+
return {
|
|
118
|
+
type: command.type,
|
|
119
|
+
...target,
|
|
120
|
+
x: command.x,
|
|
121
|
+
y: command.y
|
|
122
|
+
};
|
|
123
|
+
case "resize-shape":
|
|
124
|
+
numbers(command.width, command.height);
|
|
125
|
+
return {
|
|
126
|
+
type: command.type,
|
|
127
|
+
...target,
|
|
128
|
+
width: command.width,
|
|
129
|
+
height: command.height
|
|
130
|
+
};
|
|
131
|
+
case "create-rectangle":
|
|
132
|
+
numbers(command.x, command.y, command.width, command.height);
|
|
133
|
+
return {
|
|
134
|
+
type: command.type,
|
|
135
|
+
...target,
|
|
136
|
+
x: command.x,
|
|
137
|
+
y: command.y,
|
|
138
|
+
width: command.width,
|
|
139
|
+
height: command.height,
|
|
140
|
+
...command.text === void 0 ? {} : { text: text(command.text) }
|
|
141
|
+
};
|
|
142
|
+
default: throw new Error("Invalid edit command type.");
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/scene-details.ts
|
|
148
|
+
/** Runtime model checks prevent implicit DOM stringification of host-provided objects. */
|
|
149
|
+
function assertShapeDetails(shape, budget) {
|
|
150
|
+
const data = shape.shapeData ?? [], links = shape.hyperlinks ?? [];
|
|
151
|
+
if (!Array.isArray(data) || !Array.isArray(links) || data.length + links.length > 1024 || (budget.rows += data.length + links.length) > 1e5) throw new Error("The scene exceeds shape metadata row limits.");
|
|
152
|
+
const strings = (record, names, required = false) => {
|
|
153
|
+
for (const name of names) if ((required || record[name] !== void 0) && typeof record[name] !== "string") throw new Error("The scene has invalid shape metadata text.");
|
|
154
|
+
};
|
|
155
|
+
const booleans = (record, names) => {
|
|
156
|
+
for (const name of names) if (record[name] !== void 0 && typeof record[name] !== "boolean") throw new Error("The scene has invalid shape metadata flags.");
|
|
157
|
+
};
|
|
158
|
+
const count = (record, skipTarget = false) => {
|
|
159
|
+
const values = Object.entries(record);
|
|
160
|
+
if (values.length > 32) throw new Error("The scene has excessive shape metadata fields.");
|
|
161
|
+
for (const [name, value] of values) {
|
|
162
|
+
if (skipTarget && name === "target") continue;
|
|
163
|
+
if (typeof value === "string") {
|
|
164
|
+
if (value.length > 8192 || (budget.characters += value.length) > 5e6) throw new Error("The scene exceeds shape metadata string limits.");
|
|
165
|
+
} else if (typeof value === "number") {
|
|
166
|
+
if (!Number.isFinite(value)) throw new Error("The scene has invalid numeric metadata.");
|
|
167
|
+
} else if (value !== void 0 && value !== null && typeof value !== "boolean") throw new Error("The scene has invalid shape metadata values.");
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
for (const row of data) {
|
|
171
|
+
const record = row;
|
|
172
|
+
strings(record, [
|
|
173
|
+
"id",
|
|
174
|
+
"name",
|
|
175
|
+
"valueKind"
|
|
176
|
+
], true);
|
|
177
|
+
strings(record, [
|
|
178
|
+
"label",
|
|
179
|
+
"rawType",
|
|
180
|
+
"rawValue",
|
|
181
|
+
"unit",
|
|
182
|
+
"error",
|
|
183
|
+
"format",
|
|
184
|
+
"prompt",
|
|
185
|
+
"sortKey"
|
|
186
|
+
]);
|
|
187
|
+
booleans(record, ["invisible"]);
|
|
188
|
+
count(row);
|
|
189
|
+
}
|
|
190
|
+
for (const row of links) {
|
|
191
|
+
const record = row;
|
|
192
|
+
strings(record, ["id", "name"], true);
|
|
193
|
+
strings(record, [
|
|
194
|
+
"description",
|
|
195
|
+
"address",
|
|
196
|
+
"subAddress",
|
|
197
|
+
"sortKey"
|
|
198
|
+
]);
|
|
199
|
+
booleans(record, [
|
|
200
|
+
"default",
|
|
201
|
+
"newWindow",
|
|
202
|
+
"invisible"
|
|
203
|
+
]);
|
|
204
|
+
count(row, true);
|
|
205
|
+
if (!row.target || typeof row.target !== "object" || Array.isArray(row.target)) throw new Error("The scene has invalid hyperlink metadata.");
|
|
206
|
+
const target = row.target;
|
|
207
|
+
const key = row.target.kind === "external" ? "href" : row.target.kind === "internal" ? "subAddress" : row.target.kind === "unresolved" ? "reason" : void 0;
|
|
208
|
+
if (!key) throw new Error("The scene has invalid hyperlink metadata.");
|
|
209
|
+
strings(target, [key], true);
|
|
210
|
+
count(row.target);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
//#endregion
|
|
214
|
+
//#region src/foreign-vector-budget.ts
|
|
215
|
+
/** UI allocation limits, charged for every instance, including repeated shared resources. */
|
|
216
|
+
var FOREIGN_VECTOR_SCENE_LIMITS = Object.freeze({
|
|
217
|
+
maxNodes: 1e5,
|
|
218
|
+
maxCommands: 4e5,
|
|
219
|
+
maxOperands: 1e6
|
|
220
|
+
});
|
|
221
|
+
/** Count work only after canonical validation. Interpretation and safety remain in core. */
|
|
222
|
+
function inspectForeignVectorResource(source) {
|
|
223
|
+
const vector = validateVisioForeignVector(source);
|
|
224
|
+
let nodes = 1, commands = 0, operands = 0, svgBytes = 1536;
|
|
225
|
+
const count = (item, retained) => {
|
|
226
|
+
nodes++;
|
|
227
|
+
if (retained) svgBytes += 1024;
|
|
228
|
+
if ("commands" in item) {
|
|
229
|
+
commands += item.commands.length;
|
|
230
|
+
if (retained) svgBytes += item.commands.length * 4;
|
|
231
|
+
for (const command of item.commands) {
|
|
232
|
+
operands += command.values.length;
|
|
233
|
+
if (retained) svgBytes += command.values.length * 32;
|
|
234
|
+
}
|
|
235
|
+
} else for (const child of item.items) count(child, retained);
|
|
236
|
+
if (item.clipIndex !== void 0) count(vector.clips[item.clipIndex], false);
|
|
237
|
+
};
|
|
238
|
+
for (const clip of vector.clips) count(clip, true);
|
|
239
|
+
for (const item of vector.items) count(item, true);
|
|
240
|
+
return {
|
|
241
|
+
vector,
|
|
242
|
+
nodes,
|
|
243
|
+
commands,
|
|
244
|
+
operands,
|
|
245
|
+
svgBytes,
|
|
246
|
+
validationWork: nodes * 128 + commands * 32 + operands * 8
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/** Invocation-local budget. Never trusts frozen host objects or caches across calls. */
|
|
250
|
+
var ForeignVectorBudget = class {
|
|
251
|
+
nodes = 0;
|
|
252
|
+
commands = 0;
|
|
253
|
+
operands = 0;
|
|
254
|
+
take(source) {
|
|
255
|
+
const resource = inspectForeignVectorResource(source);
|
|
256
|
+
this.nodes += resource.nodes;
|
|
257
|
+
this.commands += resource.commands;
|
|
258
|
+
this.operands += resource.operands;
|
|
259
|
+
if (this.nodes > FOREIGN_VECTOR_SCENE_LIMITS.maxNodes || this.commands > FOREIGN_VECTOR_SCENE_LIMITS.maxCommands || this.operands > FOREIGN_VECTOR_SCENE_LIMITS.maxOperands) throw new Error("The scene exceeds aggregate foreign vector work limits.");
|
|
260
|
+
return resource;
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
var MAX_DIMENSION = 1e4;
|
|
264
|
+
var MAX_SHAPES = 25e3;
|
|
265
|
+
function finite(value, label, min = -1e4, max = MAX_DIMENSION) {
|
|
266
|
+
if (!Number.isFinite(value) || value < min || value > max) throw new Error(`The scene has an invalid ${label}.`);
|
|
267
|
+
}
|
|
268
|
+
function member(value, choices, label) {
|
|
269
|
+
if (!choices.includes(value)) throw new Error(`The scene has an invalid ${label}.`);
|
|
270
|
+
}
|
|
271
|
+
/** Defensive display limits also protect callers supplying their own typed scenes. */
|
|
272
|
+
function assertViewableDocument(model) {
|
|
273
|
+
if (model.format !== "vsdx" || !Array.isArray(model.pages) || model.pages.length > 256 || !Array.isArray(model.diagnostics) || model.diagnostics.length > 2e3) throw new Error("The scene has an invalid page list.");
|
|
274
|
+
const seen = /* @__PURE__ */ new Set();
|
|
275
|
+
let pathBytes = 0, textBytes = 0, runCount = 0, imageBytes = 0, geometryCount = 0, uniquePixels = 0, drawnPixels = 0, layerCount = 0, paragraphCount = 0, gradientStops = 0;
|
|
276
|
+
const imageResources = /* @__PURE__ */ new Map();
|
|
277
|
+
const vectorBudget = new ForeignVectorBudget();
|
|
278
|
+
let metadataBytes = 0;
|
|
279
|
+
const detailBudget = {
|
|
280
|
+
rows: 0,
|
|
281
|
+
characters: 0
|
|
282
|
+
};
|
|
283
|
+
const label = (value, limit = 4096) => {
|
|
284
|
+
if (typeof value !== "string" || value.length > limit) throw new Error("The scene exceeds safe metadata string limits.");
|
|
285
|
+
metadataBytes += value.length;
|
|
286
|
+
if (metadataBytes > 1e6) throw new Error("The scene exceeds aggregate metadata limits.");
|
|
287
|
+
};
|
|
288
|
+
for (const diagnostic of model.diagnostics) {
|
|
289
|
+
label(diagnostic.code, 256);
|
|
290
|
+
label(diagnostic.message);
|
|
291
|
+
member(diagnostic.severity, ["info", "warning"], "diagnostic severity");
|
|
292
|
+
}
|
|
293
|
+
const pageIds = /* @__PURE__ */ new Set();
|
|
294
|
+
const stack = [];
|
|
295
|
+
for (const page of model.pages) {
|
|
296
|
+
label(page.id, 256);
|
|
297
|
+
if (pageIds.has(page.id)) throw new Error("The scene has duplicate page identities.");
|
|
298
|
+
pageIds.add(page.id);
|
|
299
|
+
label(page.name);
|
|
300
|
+
if (page.layers !== void 0 && !Array.isArray(page.layers)) throw new Error("The scene has an invalid layer list.");
|
|
301
|
+
layerCount += page.layers?.length ?? 0;
|
|
302
|
+
if (layerCount > 25e3) throw new Error("The scene exceeds safe layer count limits.");
|
|
303
|
+
for (const layer of page.layers ?? []) {
|
|
304
|
+
label(layer.id, 256);
|
|
305
|
+
label(layer.name);
|
|
306
|
+
for (const flag of [
|
|
307
|
+
layer.visible,
|
|
308
|
+
layer.printable,
|
|
309
|
+
layer.locked
|
|
310
|
+
]) member(flag, [true, false], "layer flag");
|
|
311
|
+
}
|
|
312
|
+
finite(page.width, "page width", .001);
|
|
313
|
+
finite(page.height, "page height", .001);
|
|
314
|
+
if (!Array.isArray(page.shapes) || stack.length + page.shapes.length > MAX_SHAPES) throw new Error("The scene exceeds safe shape count limits or has an invalid shape list.");
|
|
315
|
+
const ids = /* @__PURE__ */ new Set();
|
|
316
|
+
for (const shape of page.shapes) stack.push({
|
|
317
|
+
shape,
|
|
318
|
+
depth: 0,
|
|
319
|
+
ids
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
while (stack.length) {
|
|
323
|
+
const { shape, depth, ids } = stack.pop();
|
|
324
|
+
if (depth > 64 || seen.has(shape) || seen.size >= MAX_SHAPES) throw new Error("The scene exceeds safe shape depth/count limits or contains a cycle.");
|
|
325
|
+
seen.add(shape);
|
|
326
|
+
if (!Array.isArray(shape.children) || seen.size + stack.length + shape.children.length > MAX_SHAPES) throw new Error("The scene exceeds safe shape count limits or has an invalid child list.");
|
|
327
|
+
label(shape.id, 1024);
|
|
328
|
+
if (ids.has(shape.id)) throw new Error("The scene has duplicate shape identities on one page.");
|
|
329
|
+
ids.add(shape.id);
|
|
330
|
+
member(shape.hidden, [true, false], "hidden flag");
|
|
331
|
+
member(shape.kind, [
|
|
332
|
+
"shape",
|
|
333
|
+
"group",
|
|
334
|
+
"connector",
|
|
335
|
+
"foreign"
|
|
336
|
+
], "shape kind");
|
|
337
|
+
if (shape.groupDisplayMode !== void 0) member(shape.groupDisplayMode, [
|
|
338
|
+
0,
|
|
339
|
+
1,
|
|
340
|
+
2
|
|
341
|
+
], "group display mode");
|
|
342
|
+
member(shape.text.horizontalAlign, [
|
|
343
|
+
"left",
|
|
344
|
+
"center",
|
|
345
|
+
"right"
|
|
346
|
+
], "text alignment");
|
|
347
|
+
member(shape.text.verticalAlign, [
|
|
348
|
+
"top",
|
|
349
|
+
"middle",
|
|
350
|
+
"bottom"
|
|
351
|
+
], "text alignment");
|
|
352
|
+
label(shape.name);
|
|
353
|
+
label(shape.text.fontFamily, 1024);
|
|
354
|
+
label(shape.style.fill, 256);
|
|
355
|
+
label(shape.style.lineColor, 256);
|
|
356
|
+
label(shape.text.color, 256);
|
|
357
|
+
if (shape.text.backgroundColor) label(shape.text.backgroundColor, 256);
|
|
358
|
+
if (shape.text.backgroundOpacity !== void 0) finite(shape.text.backgroundOpacity, "text background opacity", 0, 1);
|
|
359
|
+
assertShapeDetails(shape, detailBudget);
|
|
360
|
+
if (shape.visibility) {
|
|
361
|
+
member(shape.visibility.layerHidden, [true, false], "layer visibility metadata");
|
|
362
|
+
member(shape.visibility.guide, [true, false], "guide visibility metadata");
|
|
363
|
+
member(shape.visibility.noShow, [
|
|
364
|
+
void 0,
|
|
365
|
+
true,
|
|
366
|
+
false
|
|
367
|
+
], "NoShow visibility metadata");
|
|
368
|
+
member(shape.visibility.nonPrinting, [
|
|
369
|
+
void 0,
|
|
370
|
+
true,
|
|
371
|
+
false
|
|
372
|
+
], "NonPrinting metadata");
|
|
373
|
+
member(shape.visibility.layerPrintSummary, [
|
|
374
|
+
"unlayered",
|
|
375
|
+
"all-enabled",
|
|
376
|
+
"all-disabled",
|
|
377
|
+
"mixed",
|
|
378
|
+
"unknown"
|
|
379
|
+
], "layer print summary");
|
|
380
|
+
}
|
|
381
|
+
finite(shape.width, "shape width", 0);
|
|
382
|
+
finite(shape.height, "shape height", 0);
|
|
383
|
+
if (shape.transform.length !== 6 || shape.text.transform.length !== 6) throw new Error("The scene has an invalid transform.");
|
|
384
|
+
for (const value of [...shape.transform, ...shape.text.transform]) finite(value, "transform");
|
|
385
|
+
finite(shape.text.width, "text width", 0);
|
|
386
|
+
finite(shape.text.height, "text height", 0);
|
|
387
|
+
finite(shape.text.fontSize, "font size", 0, 100);
|
|
388
|
+
finite(shape.style.lineWidth, "line width", 0, 100);
|
|
389
|
+
if (shape.style.lineDash !== void 0) {
|
|
390
|
+
const dash = shape.style.lineDash;
|
|
391
|
+
if (!Array.isArray(dash) || dash.length < 2 || dash.length > 6 || dash.length % 2 !== 0) throw new Error("The scene has an invalid normalized line dash.");
|
|
392
|
+
for (const value of dash) finite(value, "line dash length", 0, 27);
|
|
393
|
+
if (!dash.some((value) => value > 0)) throw new Error("The scene has an empty line dash pattern.");
|
|
394
|
+
}
|
|
395
|
+
for (const value of [
|
|
396
|
+
shape.style.linePattern,
|
|
397
|
+
shape.style.startArrow,
|
|
398
|
+
shape.style.endArrow
|
|
399
|
+
]) {
|
|
400
|
+
finite(value, "line pattern or arrow code", 0, 65535);
|
|
401
|
+
if (!Number.isInteger(value)) throw new Error("The scene has an invalid line pattern or arrow code.");
|
|
402
|
+
}
|
|
403
|
+
for (const size of [shape.style.startArrowSize, shape.style.endArrowSize]) if (size !== void 0) finite(size, "arrow size", 0, 65535);
|
|
404
|
+
if (shape.style.lineCap !== void 0 && ![
|
|
405
|
+
"round",
|
|
406
|
+
"butt",
|
|
407
|
+
"square"
|
|
408
|
+
].includes(shape.style.lineCap)) throw new Error("The scene has an invalid normalized line cap.");
|
|
409
|
+
finite(shape.style.fillOpacity, "fill opacity", 0, 1);
|
|
410
|
+
finite(shape.style.lineOpacity, "line opacity", 0, 1);
|
|
411
|
+
if (shape.style.fillGradient) {
|
|
412
|
+
const gradient = shape.style.fillGradient;
|
|
413
|
+
if (gradient.type !== "linear" || !Array.isArray(gradient.stops) || gradient.stops.length < 2 || gradient.stops.length > 128 || gradient.start.length !== 2 || gradient.end.length !== 2) throw new Error("The scene has an invalid fill gradient.");
|
|
414
|
+
if ((gradientStops += gradient.stops.length) > 1e5) throw new Error("The scene exceeds aggregate gradient stop limits.");
|
|
415
|
+
for (const value of [...gradient.start, ...gradient.end]) finite(value, "gradient position", -2e4, 2e4);
|
|
416
|
+
let offset = -1;
|
|
417
|
+
for (const stop of gradient.stops) {
|
|
418
|
+
finite(stop.offset, "gradient stop", 0, 1);
|
|
419
|
+
finite(stop.opacity, "gradient opacity", 0, 1);
|
|
420
|
+
label(stop.color, 256);
|
|
421
|
+
if (stop.offset < offset) throw new Error("Gradient stops must be ordered.");
|
|
422
|
+
offset = stop.offset;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (typeof shape.text.plainText !== "string") throw new Error("The scene has invalid plain text.");
|
|
426
|
+
textBytes += shape.text.plainText.length;
|
|
427
|
+
if (!Array.isArray(shape.text.runs) || runCount + shape.text.runs.length > 1e5) throw new Error("The scene exceeds safe text run limits or has an invalid run list.");
|
|
428
|
+
for (const run of shape.text.runs) {
|
|
429
|
+
label(run.fontFamily, 1024);
|
|
430
|
+
label(run.color, 256);
|
|
431
|
+
if (typeof run.text !== "string") throw new Error("The scene has invalid run text.");
|
|
432
|
+
textBytes += run.text.length;
|
|
433
|
+
++runCount;
|
|
434
|
+
finite(run.fontSize, "run font size", 0, 100);
|
|
435
|
+
}
|
|
436
|
+
if (!Array.isArray(shape.geometry) || geometryCount + shape.geometry.length > 1e5) throw new Error("The scene exceeds safe geometry limits or has an invalid geometry list.");
|
|
437
|
+
for (const geometry of shape.geometry) {
|
|
438
|
+
if (typeof geometry.path !== "string") throw new Error("The scene has invalid path text.");
|
|
439
|
+
member(geometry.fill, [true, false], "geometry fill flag");
|
|
440
|
+
member(geometry.stroke, [true, false], "geometry stroke flag");
|
|
441
|
+
pathBytes += geometry.path.length;
|
|
442
|
+
geometryCount++;
|
|
443
|
+
}
|
|
444
|
+
if (geometryCount > 1e5) throw new Error("The scene exceeds safe geometry limits.");
|
|
445
|
+
if (pathBytes > 16e6 || textBytes > 5e6 || runCount > 1e5) throw new Error("The scene exceeds safe path/text limits.");
|
|
446
|
+
for (const value of [
|
|
447
|
+
shape.text.margins.left,
|
|
448
|
+
shape.text.margins.right,
|
|
449
|
+
shape.text.margins.top,
|
|
450
|
+
shape.text.margins.bottom
|
|
451
|
+
]) finite(value, "text margin");
|
|
452
|
+
const paragraphs = shape.text.paragraphs ?? [];
|
|
453
|
+
if (!Array.isArray(paragraphs) || paragraphs.length > 5e3 || (paragraphCount += paragraphs.length) > 5e4) throw new Error("The scene exceeds safe paragraph limits.");
|
|
454
|
+
for (const paragraph of paragraphs) {
|
|
455
|
+
member(paragraph.direction, ["ltr", "rtl"], "paragraph direction");
|
|
456
|
+
member(paragraph.horizontalAlign, [
|
|
457
|
+
"left",
|
|
458
|
+
"center",
|
|
459
|
+
"right",
|
|
460
|
+
"justify",
|
|
461
|
+
"distributed"
|
|
462
|
+
], "paragraph alignment");
|
|
463
|
+
member(paragraph.lineSpacing.kind, ["multiple", "exact"], "paragraph line spacing");
|
|
464
|
+
if (!Number.isSafeInteger(paragraph.start) || !Number.isSafeInteger(paragraph.end) || paragraph.start < 0 || paragraph.end < paragraph.start || paragraph.end > shape.text.plainText.length) throw new Error("The scene has invalid paragraph offsets.");
|
|
465
|
+
for (const value of [
|
|
466
|
+
paragraph.indentLeft,
|
|
467
|
+
paragraph.indentRight,
|
|
468
|
+
paragraph.indentFirst,
|
|
469
|
+
paragraph.spaceBefore,
|
|
470
|
+
paragraph.spaceAfter
|
|
471
|
+
]) finite(value, "paragraph position");
|
|
472
|
+
finite(paragraph.lineSpacing.value, "line spacing", 0, 100);
|
|
473
|
+
if (paragraph.bullet) {
|
|
474
|
+
label(paragraph.bullet.text, 1024);
|
|
475
|
+
label(paragraph.bullet.fontFamily, 1024);
|
|
476
|
+
finite(paragraph.bullet.fontSize, "bullet size", 0, 100);
|
|
477
|
+
finite(paragraph.bullet.offset, "bullet offset");
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (shape.foreignVector !== void 0) {
|
|
481
|
+
const foreign = shape.foreignVector;
|
|
482
|
+
if (!foreign || typeof foreign !== "object") throw new Error("The scene has an invalid foreign vector placement.");
|
|
483
|
+
finite(foreign.x, "foreign vector position");
|
|
484
|
+
finite(foreign.y, "foreign vector position");
|
|
485
|
+
finite(foreign.width, "foreign vector width", 0);
|
|
486
|
+
finite(foreign.height, "foreign vector height", 0);
|
|
487
|
+
finite(foreign.opacity, "foreign vector opacity", 0, 1);
|
|
488
|
+
vectorBudget.take(foreign.vector);
|
|
489
|
+
}
|
|
490
|
+
if (shape.image) {
|
|
491
|
+
const image = shape.image;
|
|
492
|
+
if (![
|
|
493
|
+
"image/png",
|
|
494
|
+
"image/jpeg",
|
|
495
|
+
"image/gif"
|
|
496
|
+
].includes(image.mimeType) || !(image.bytes instanceof Uint8Array)) throw new Error("Unsupported embedded image.");
|
|
497
|
+
imageBytes += image.bytes.byteLength;
|
|
498
|
+
if (image.bytes.byteLength > 8388608 || imageBytes > 67108864) throw new Error("The scene exceeds safe image limits.");
|
|
499
|
+
finite(image.pixelWidth, "image width", 1, 16384);
|
|
500
|
+
finite(image.pixelHeight, "image height", 1, 16384);
|
|
501
|
+
drawnPixels += image.pixelWidth * image.pixelHeight;
|
|
502
|
+
if (!imageResources.has(image.bytes)) {
|
|
503
|
+
imageResources.set(image.bytes, image);
|
|
504
|
+
uniquePixels += image.pixelWidth * image.pixelHeight;
|
|
505
|
+
} else {
|
|
506
|
+
const previous = imageResources.get(image.bytes);
|
|
507
|
+
if (previous.mimeType !== image.mimeType || previous.pixelWidth !== image.pixelWidth || previous.pixelHeight !== image.pixelHeight) throw new Error("Shared raster bytes have inconsistent declared metadata.");
|
|
508
|
+
}
|
|
509
|
+
if (uniquePixels > 64e6 || drawnPixels > 256e6) throw new Error("The scene exceeds aggregate decoded image limits.");
|
|
510
|
+
if (image.opacity !== void 0) finite(image.opacity, "image opacity", 0, 1);
|
|
511
|
+
if (image.pixelWidth * image.pixelHeight > VISIO_RASTER_IMAGE_LIMITS.maxPixels) throw new Error("The scene exceeds safe image pixel limits.");
|
|
512
|
+
if (image.x !== void 0) finite(image.x, "image position");
|
|
513
|
+
if (image.y !== void 0) finite(image.y, "image position");
|
|
514
|
+
if (image.width !== void 0) finite(image.width, "image width", 0);
|
|
515
|
+
if (image.height !== void 0) finite(image.height, "image height", 0);
|
|
516
|
+
}
|
|
517
|
+
for (const child of shape.children) stack.push({
|
|
518
|
+
shape: child,
|
|
519
|
+
depth: depth + 1,
|
|
520
|
+
ids
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
for (const image of imageResources.values()) {
|
|
524
|
+
const actual = inspectVisioRasterImage(image.bytes);
|
|
525
|
+
if (actual.mimeType !== image.mimeType || actual.pixelWidth !== image.pixelWidth || actual.pixelHeight !== image.pixelHeight) throw new Error("Declared raster MIME or dimensions do not match the embedded bytes.");
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
//#endregion
|
|
529
|
+
//#region src/worker-editor.ts
|
|
530
|
+
function createWorkerEditor(factory, timeoutMs = 2e4) {
|
|
531
|
+
const create = factory ?? (() => {
|
|
532
|
+
if (typeof Worker === "undefined") throw new Error("Editing requires browser worker support.");
|
|
533
|
+
return new Worker(new URL(
|
|
534
|
+
/* @vite-ignore */
|
|
535
|
+
new URL("assets/edit-worker-DRgXgRud.js", import.meta.url).href,
|
|
536
|
+
"" + import.meta.url
|
|
537
|
+
), { type: "module" });
|
|
538
|
+
});
|
|
539
|
+
let cancel;
|
|
540
|
+
const edit = (bytes, edits) => {
|
|
541
|
+
cancel?.();
|
|
542
|
+
if (bytes.byteLength > 33554432) return Promise.reject(/* @__PURE__ */ new Error("This viewer accepts files up to 32 MiB."));
|
|
543
|
+
let commands;
|
|
544
|
+
try {
|
|
545
|
+
commands = snapshotEdits(edits);
|
|
546
|
+
} catch (error) {
|
|
547
|
+
return Promise.reject(error);
|
|
548
|
+
}
|
|
549
|
+
const copy = Uint8Array.from(bytes);
|
|
550
|
+
return new Promise((resolve, reject) => {
|
|
551
|
+
let worker;
|
|
552
|
+
try {
|
|
553
|
+
worker = create();
|
|
554
|
+
} catch (error) {
|
|
555
|
+
reject(error);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
let settled = false;
|
|
559
|
+
const finish = (error, result) => {
|
|
560
|
+
if (settled) return;
|
|
561
|
+
settled = true;
|
|
562
|
+
clearTimeout(timer);
|
|
563
|
+
cancel = void 0;
|
|
564
|
+
try {
|
|
565
|
+
worker.onmessage = null;
|
|
566
|
+
worker.onerror = null;
|
|
567
|
+
worker.terminate();
|
|
568
|
+
} catch (cause) {
|
|
569
|
+
error ??= cause instanceof Error ? cause : new Error(String(cause));
|
|
570
|
+
}
|
|
571
|
+
if (error) reject(error);
|
|
572
|
+
else resolve(result);
|
|
573
|
+
};
|
|
574
|
+
const timer = setTimeout(() => finish(/* @__PURE__ */ new Error("The diagram exceeded the isolated editing time limit.")), timeoutMs);
|
|
575
|
+
cancel = () => finish(new DOMException("The diagram edit was cancelled.", "AbortError"));
|
|
576
|
+
worker.onerror = (event) => finish(new Error(event.message || "The Visio edit worker failed."));
|
|
577
|
+
worker.onmessage = (event) => {
|
|
578
|
+
try {
|
|
579
|
+
const value = event.data;
|
|
580
|
+
if (value?.ok === true && value.bytes instanceof Uint8Array && value.bytes.byteLength <= 33554432 && value.document && Array.isArray(value.changedParts) && value.changedParts.length <= 1e3 && value.changedParts.every((part) => typeof part === "string" && part.length <= 4096) && Array.isArray(value.diagnostics) && value.diagnostics.length <= 1e3 && value.diagnostics.every((note) => !!note && typeof note === "object" && "code" in note && typeof note.code === "string" && note.code.length <= 256 && "message" in note && typeof note.message === "string" && note.message.length <= 4096)) finish(void 0, value);
|
|
581
|
+
else {
|
|
582
|
+
const error = new Error(typeof value?.message === "string" && value.message.length <= 4096 ? value.message : "The Visio edit worker returned an invalid response.");
|
|
583
|
+
if (typeof value?.code === "string" && value.code.length <= 256) Object.assign(error, { code: value.code });
|
|
584
|
+
finish(error);
|
|
585
|
+
}
|
|
586
|
+
} catch (cause) {
|
|
587
|
+
finish(cause instanceof Error ? cause : new Error(String(cause)));
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
try {
|
|
591
|
+
worker.postMessage({
|
|
592
|
+
bytes: copy.buffer,
|
|
593
|
+
edits: commands
|
|
594
|
+
}, [copy.buffer]);
|
|
595
|
+
} catch (cause) {
|
|
596
|
+
finish(cause instanceof Error ? cause : new Error(String(cause)));
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
};
|
|
600
|
+
edit.cancel = () => cancel?.();
|
|
601
|
+
return edit;
|
|
602
|
+
}
|
|
603
|
+
//#endregion
|
|
604
|
+
//#region src/viewer-layers.ts
|
|
605
|
+
/** UI limits. Visibility semantics and shape traversal remain in ooxml-core. */
|
|
606
|
+
var VIEWER_LAYER_LIMITS = Object.freeze({
|
|
607
|
+
overrides: VISIO_VISIBILITY_LIMITS.maxLayers,
|
|
608
|
+
controls: 200
|
|
609
|
+
});
|
|
610
|
+
var EMPTY_LAYER_OVERRIDES = Object.freeze([]);
|
|
611
|
+
/** Validate before allocating indexes; never expose mutable maps as viewer state. */
|
|
612
|
+
function layerOverrideMaps(model, overrides) {
|
|
613
|
+
if (!Array.isArray(overrides) || overrides.length > VIEWER_LAYER_LIMITS.overrides) throw new Error("The viewer exceeds the layer override limit.");
|
|
614
|
+
const pages = new Map(model?.pages.map((page) => [page.id, page]));
|
|
615
|
+
const known = /* @__PURE__ */ new Map();
|
|
616
|
+
const result = /* @__PURE__ */ new Map();
|
|
617
|
+
for (const entry of overrides) {
|
|
618
|
+
if (!entry || typeof entry.pageId !== "string" || typeof entry.layerId !== "string" || typeof entry.visible !== "boolean") throw new Error("Layer visibility overrides require page and layer IDs and a boolean.");
|
|
619
|
+
const page = pages.get(entry.pageId);
|
|
620
|
+
if (!page) throw new Error("The layer override page does not belong to this document.");
|
|
621
|
+
let ids = known.get(page.id);
|
|
622
|
+
if (!ids) known.set(page.id, ids = new Set(page.layers?.map((layer) => layer.id)));
|
|
623
|
+
if (!ids.has(entry.layerId)) throw new Error("The layer does not belong to this page.");
|
|
624
|
+
let values = result.get(page.id);
|
|
625
|
+
if (!values) result.set(page.id, values = /* @__PURE__ */ new Map());
|
|
626
|
+
if (values.has(entry.layerId)) throw new Error("Duplicate layer visibility override.");
|
|
627
|
+
values.set(entry.layerId, entry.visible);
|
|
628
|
+
}
|
|
629
|
+
return result;
|
|
630
|
+
}
|
|
631
|
+
function documentVisibility(model, overrides = EMPTY_LAYER_OVERRIDES) {
|
|
632
|
+
const maps = layerOverrideMaps(model, overrides);
|
|
633
|
+
const visible = /* @__PURE__ */ new WeakMap();
|
|
634
|
+
for (const page of model?.pages ?? []) {
|
|
635
|
+
const layerVisibilityOverrides = maps.get(page.id);
|
|
636
|
+
for (const entry of resolveVisioPageVisibility(page, layerVisibilityOverrides ? { layerVisibilityOverrides } : {})) visible.set(entry.shape, !entry.hidden);
|
|
637
|
+
}
|
|
638
|
+
return visible;
|
|
639
|
+
}
|
|
640
|
+
function visibleSelection(model, pageIndex, selection, visible) {
|
|
641
|
+
const page = model?.pages[pageIndex];
|
|
642
|
+
if (!model || !page || !selection) return false;
|
|
643
|
+
const stack = [...getVisioPageLayers(model, page.id).find((candidate) => candidate.id === (selection.pageId ?? page.id))?.shapes ?? []];
|
|
644
|
+
while (stack.length) {
|
|
645
|
+
const shape = stack.pop();
|
|
646
|
+
if (shape.id === selection.id) return hasVisibleShapeContent(shape, visible);
|
|
647
|
+
stack.push(...shape.children);
|
|
648
|
+
}
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
/** Rendering eligibility shared by selection and SVG containers; no format interpretation. */
|
|
652
|
+
function hasVisibleShapeContent(shape, visible, cache = /* @__PURE__ */ new WeakMap()) {
|
|
653
|
+
const cached = cache.get(shape);
|
|
654
|
+
if (cached !== void 0) return cached;
|
|
655
|
+
const own = !(shape.kind === "group" && shape.groupDisplayMode === 0) && (shape.geometry.length > 0 || !!shape.image || !!shape.foreignVector || !!shape.text.plainText || shape.kind === "foreign");
|
|
656
|
+
const renderable = (visible?.get(shape) ?? !shape.hidden) && (own || shape.children.some((child) => hasVisibleShapeContent(child, visible, cache)));
|
|
657
|
+
cache.set(shape, renderable);
|
|
658
|
+
return renderable;
|
|
659
|
+
}
|
|
660
|
+
//#endregion
|
|
661
|
+
//#region src/document-text-search.ts
|
|
662
|
+
/** UTF-16 character bounds, applied before copying or case conversion. */
|
|
663
|
+
var TEXT_SEARCH_LIMITS = Object.freeze({
|
|
664
|
+
queryCharacters: 256,
|
|
665
|
+
results: 500,
|
|
666
|
+
indexedCharacters: 2e6,
|
|
667
|
+
shapeCharacters: 32768,
|
|
668
|
+
previewCharacters: 160,
|
|
669
|
+
shapes: 25e3,
|
|
670
|
+
depth: 64
|
|
671
|
+
});
|
|
672
|
+
var EMPTY_TEXT_SEARCH = Object.freeze({
|
|
673
|
+
query: "",
|
|
674
|
+
results: Object.freeze([]),
|
|
675
|
+
activeIndex: -1,
|
|
676
|
+
indexTruncated: false,
|
|
677
|
+
resultsTruncated: false
|
|
678
|
+
});
|
|
679
|
+
/** Index canonical text for the current display state. No XML, DOM or layout parsing. */
|
|
680
|
+
function indexDocumentText(model, isVisible = (shape) => !shape.hidden) {
|
|
681
|
+
const entries = [];
|
|
682
|
+
const seen = /* @__PURE__ */ new Set();
|
|
683
|
+
let characters = 0, truncated = false;
|
|
684
|
+
for (const [pageIndex, page] of (model?.pages ?? []).entries()) {
|
|
685
|
+
const stack = page.shapes.map((shape) => ({
|
|
686
|
+
shape,
|
|
687
|
+
depth: 0
|
|
688
|
+
})).reverse();
|
|
689
|
+
while (stack.length) {
|
|
690
|
+
const { shape, depth } = stack.pop();
|
|
691
|
+
if (seen.has(shape) || depth > TEXT_SEARCH_LIMITS.depth) {
|
|
692
|
+
truncated = true;
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
if (seen.size >= TEXT_SEARCH_LIMITS.shapes) return Object.freeze({
|
|
696
|
+
entries: Object.freeze(entries),
|
|
697
|
+
truncated: true
|
|
698
|
+
});
|
|
699
|
+
seen.add(shape);
|
|
700
|
+
if (!isVisible(shape)) continue;
|
|
701
|
+
if (!(shape.kind === "group" && shape.groupDisplayMode === 0)) {
|
|
702
|
+
const source = shape.text.plainText;
|
|
703
|
+
const length = Math.min(source.length, TEXT_SEARCH_LIMITS.shapeCharacters, TEXT_SEARCH_LIMITS.indexedCharacters - characters);
|
|
704
|
+
if (length < source.length) truncated = true;
|
|
705
|
+
if (length) {
|
|
706
|
+
characters += length;
|
|
707
|
+
const text = source.slice(0, length);
|
|
708
|
+
const preview = text.slice(0, TEXT_SEARCH_LIMITS.previewCharacters) + (source.length > TEXT_SEARCH_LIMITS.previewCharacters ? "…" : "");
|
|
709
|
+
entries.push(Object.freeze({
|
|
710
|
+
foldedText: text.toLowerCase(),
|
|
711
|
+
result: Object.freeze({
|
|
712
|
+
pageIndex,
|
|
713
|
+
pageId: page.id,
|
|
714
|
+
pageName: page.name,
|
|
715
|
+
shapeId: shape.id,
|
|
716
|
+
shapeName: shape.name,
|
|
717
|
+
preview
|
|
718
|
+
})
|
|
719
|
+
}));
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
for (let i = shape.children.length - 1; i >= 0; i--) stack.push({
|
|
723
|
+
shape: shape.children[i],
|
|
724
|
+
depth: depth + 1
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return Object.freeze({
|
|
729
|
+
entries: Object.freeze(entries),
|
|
730
|
+
truncated
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
function validateSearchQuery(query) {
|
|
734
|
+
if (typeof query !== "string" || query.length > TEXT_SEARCH_LIMITS.queryCharacters) throw new Error(`Search queries must be strings of at most ${TEXT_SEARCH_LIMITS.queryCharacters} characters.`);
|
|
735
|
+
}
|
|
736
|
+
/** One match per shape, literal ECMAScript lowercase comparison; no regex or language collation. */
|
|
737
|
+
function searchDocumentText(index, query) {
|
|
738
|
+
validateSearchQuery(query);
|
|
739
|
+
const results = [];
|
|
740
|
+
let resultsTruncated = false;
|
|
741
|
+
if (query.trim()) {
|
|
742
|
+
const folded = query.toLowerCase();
|
|
743
|
+
for (const entry of index.entries) {
|
|
744
|
+
if (!entry.foldedText.includes(folded)) continue;
|
|
745
|
+
if (results.length === TEXT_SEARCH_LIMITS.results) {
|
|
746
|
+
resultsTruncated = true;
|
|
747
|
+
break;
|
|
748
|
+
}
|
|
749
|
+
results.push(entry.result);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return Object.freeze({
|
|
753
|
+
query,
|
|
754
|
+
results: Object.freeze(results),
|
|
755
|
+
activeIndex: -1,
|
|
756
|
+
indexTruncated: index.truncated,
|
|
757
|
+
resultsTruncated
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
function textSearchStatus(search) {
|
|
761
|
+
if (!search.query.trim()) return "Find text across pages";
|
|
762
|
+
const count = search.results.length;
|
|
763
|
+
return (search.activeIndex < 0 ? `${count}${search.resultsTruncated ? "+" : ""} matching shapes` : `${search.activeIndex + 1} of ${count}${search.resultsTruncated ? "+" : ""} matching shapes`) + (search.indexTruncated ? " · Partial text index" : "");
|
|
764
|
+
}
|
|
765
|
+
//#endregion
|
|
766
|
+
//#region src/controller.ts
|
|
767
|
+
var ViewerController = class {
|
|
768
|
+
parser;
|
|
769
|
+
listenerError;
|
|
770
|
+
editor;
|
|
771
|
+
#state = Object.freeze({
|
|
772
|
+
document: null,
|
|
773
|
+
edit: EMPTY_EDIT_STATE,
|
|
774
|
+
pageIndex: 0,
|
|
775
|
+
zoom: 1,
|
|
776
|
+
loading: false,
|
|
777
|
+
error: null,
|
|
778
|
+
selectedShape: null,
|
|
779
|
+
search: EMPTY_TEXT_SEARCH,
|
|
780
|
+
layerVisibilityOverrides: EMPTY_LAYER_OVERRIDES
|
|
781
|
+
});
|
|
782
|
+
#subscribers = /* @__PURE__ */ new Set();
|
|
783
|
+
#events = /* @__PURE__ */ new Set();
|
|
784
|
+
#loadId = 0;
|
|
785
|
+
#destroyed = false;
|
|
786
|
+
#revision = 0;
|
|
787
|
+
#documentGeneration = 0;
|
|
788
|
+
#sourceGeneration = 0;
|
|
789
|
+
#searchIndex = null;
|
|
790
|
+
#history = null;
|
|
791
|
+
#editId = 0;
|
|
792
|
+
#visible = documentVisibility(null);
|
|
793
|
+
constructor(parser = parseVsdx, listenerError = (error) => {
|
|
794
|
+
if (typeof globalThis.reportError === "function") globalThis.reportError(error);
|
|
795
|
+
else console.error("Visio viewer listener failed:", error);
|
|
796
|
+
}, editor = createWorkerEditor()) {
|
|
797
|
+
this.parser = parser;
|
|
798
|
+
this.listenerError = listenerError;
|
|
799
|
+
this.editor = editor;
|
|
800
|
+
}
|
|
801
|
+
get state() {
|
|
802
|
+
return this.#state;
|
|
803
|
+
}
|
|
804
|
+
/** Accepted document replacements, including the same object, invalidate captured artifacts. */
|
|
805
|
+
get documentGeneration() {
|
|
806
|
+
this.#assertAlive();
|
|
807
|
+
return this.#documentGeneration;
|
|
808
|
+
}
|
|
809
|
+
/** Accepted source/model replacements invalidate drafts; edit/undo/redo keep this source epoch. */
|
|
810
|
+
get sourceGeneration() {
|
|
811
|
+
this.#assertAlive();
|
|
812
|
+
return this.#sourceGeneration;
|
|
813
|
+
}
|
|
814
|
+
subscribe(listener) {
|
|
815
|
+
this.#assertAlive();
|
|
816
|
+
this.#subscribers.add(listener);
|
|
817
|
+
if (!this.#notify(() => listener(this.#state))) this.#subscribers.delete(listener);
|
|
818
|
+
return () => this.#subscribers.delete(listener);
|
|
819
|
+
}
|
|
820
|
+
onEvent(listener) {
|
|
821
|
+
this.#assertAlive();
|
|
822
|
+
this.#events.add(listener);
|
|
823
|
+
return () => this.#events.delete(listener);
|
|
824
|
+
}
|
|
825
|
+
setDocument(document) {
|
|
826
|
+
this.#assertAlive();
|
|
827
|
+
if (document) assertViewableDocument(document);
|
|
828
|
+
const visible = documentVisibility(document);
|
|
829
|
+
++this.#documentGeneration;
|
|
830
|
+
++this.#sourceGeneration;
|
|
831
|
+
this.#invalidateEdit();
|
|
832
|
+
this.#history = null;
|
|
833
|
+
this.#loadId++;
|
|
834
|
+
this.parser.cancel?.();
|
|
835
|
+
this.#searchIndex = null;
|
|
836
|
+
this.#visible = visible;
|
|
837
|
+
this.#change({
|
|
838
|
+
document,
|
|
839
|
+
edit: EMPTY_EDIT_STATE,
|
|
840
|
+
pageIndex: 0,
|
|
841
|
+
loading: false,
|
|
842
|
+
error: null,
|
|
843
|
+
selectedShape: null,
|
|
844
|
+
search: EMPTY_TEXT_SEARCH,
|
|
845
|
+
layerVisibilityOverrides: EMPTY_LAYER_OVERRIDES
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
setPage(index) {
|
|
849
|
+
this.#assertAlive();
|
|
850
|
+
const count = this.#state.document?.pages.length ?? 0;
|
|
851
|
+
const next = Math.max(0, Math.min(count - 1, Number.isFinite(index) ? Math.trunc(index) : 0));
|
|
852
|
+
if (next === this.#state.pageIndex) return;
|
|
853
|
+
if (this.#change({
|
|
854
|
+
pageIndex: next,
|
|
855
|
+
selectedShape: null,
|
|
856
|
+
search: this.#inactiveSearch()
|
|
857
|
+
})) this.#emit("page-change", next);
|
|
858
|
+
}
|
|
859
|
+
setZoom(zoom) {
|
|
860
|
+
this.#assertAlive();
|
|
861
|
+
const next = Math.max(.1, Math.min(8, Number.isFinite(zoom) ? zoom : 1));
|
|
862
|
+
if (next === this.#state.zoom) return;
|
|
863
|
+
if (this.#change({ zoom: next })) this.#emit("zoom-change", next);
|
|
864
|
+
}
|
|
865
|
+
selectShape(shape) {
|
|
866
|
+
this.#assertAlive();
|
|
867
|
+
if (shape && !visibleSelection(this.#state.document, this.#state.pageIndex, shape, this.#visible)) return;
|
|
868
|
+
if (this.#change({
|
|
869
|
+
selectedShape: shape,
|
|
870
|
+
search: this.#inactiveSearch()
|
|
871
|
+
})) this.#emit("shape-select", shape);
|
|
872
|
+
}
|
|
873
|
+
/** Override one source-page layer for viewing. null restores its saved display flag. */
|
|
874
|
+
setLayerVisibility(pageId, layerId, visible) {
|
|
875
|
+
this.#assertAlive();
|
|
876
|
+
const page = this.#state.document?.pages.find((candidate) => candidate.id === pageId);
|
|
877
|
+
if (!page) throw new Error("The layer override page does not belong to this document.");
|
|
878
|
+
if (!page.layers?.some((layer) => layer.id === layerId)) throw new Error("The layer does not belong to this page.");
|
|
879
|
+
if (visible !== null && typeof visible !== "boolean") throw new Error("Layer visibility must be boolean or null.");
|
|
880
|
+
const previous = this.#state.layerVisibilityOverrides;
|
|
881
|
+
const found = previous.find((entry) => entry.pageId === pageId && entry.layerId === layerId);
|
|
882
|
+
if (visible === null ? !found : found?.visible === visible) return;
|
|
883
|
+
const next = previous.filter((entry) => entry !== found);
|
|
884
|
+
if (visible !== null) next.push(Object.freeze({
|
|
885
|
+
pageId,
|
|
886
|
+
layerId,
|
|
887
|
+
visible
|
|
888
|
+
}));
|
|
889
|
+
this.#setLayerOverrides(Object.freeze(next));
|
|
890
|
+
}
|
|
891
|
+
/** Reset a source page, or all pages when omitted. Does not modify the document. */
|
|
892
|
+
resetLayerVisibility(pageId) {
|
|
893
|
+
this.#assertAlive();
|
|
894
|
+
if (pageId !== void 0 && !this.#state.document?.pages.some((page) => page.id === pageId)) throw new Error("The layer override page does not belong to this document.");
|
|
895
|
+
const previous = this.#state.layerVisibilityOverrides;
|
|
896
|
+
const next = pageId === void 0 ? EMPTY_LAYER_OVERRIDES : Object.freeze(previous.filter((entry) => entry.pageId !== pageId));
|
|
897
|
+
if (next.length !== previous.length) this.#setLayerOverrides(next);
|
|
898
|
+
}
|
|
899
|
+
#setLayerOverrides(layerVisibilityOverrides) {
|
|
900
|
+
const visible = documentVisibility(this.#state.document, layerVisibilityOverrides);
|
|
901
|
+
const searchIndex = this.#state.search.query ? indexDocumentText(this.#state.document, (shape) => visible.get(shape) === true) : null;
|
|
902
|
+
const search = searchIndex ? searchDocumentText(searchIndex, this.#state.search.query) : EMPTY_TEXT_SEARCH;
|
|
903
|
+
const cleared = !!this.#state.selectedShape && !visibleSelection(this.#state.document, this.#state.pageIndex, this.#state.selectedShape, visible);
|
|
904
|
+
this.#visible = visible;
|
|
905
|
+
this.#searchIndex = searchIndex;
|
|
906
|
+
if (this.#change({
|
|
907
|
+
layerVisibilityOverrides,
|
|
908
|
+
search,
|
|
909
|
+
...cleared ? { selectedShape: null } : {}
|
|
910
|
+
}) && cleared) this.#emit("shape-select", null);
|
|
911
|
+
}
|
|
912
|
+
/** Search does not change the page or selection until explicit result navigation. */
|
|
913
|
+
setSearchQuery(query) {
|
|
914
|
+
this.#assertAlive();
|
|
915
|
+
validateSearchQuery(query);
|
|
916
|
+
if (query === this.#state.search.query) return;
|
|
917
|
+
this.#searchIndex ??= indexDocumentText(this.#state.document, (shape) => this.#visible.get(shape) === true);
|
|
918
|
+
this.#change({ search: searchDocumentText(this.#searchIndex, query) });
|
|
919
|
+
}
|
|
920
|
+
selectSearchResult(index) {
|
|
921
|
+
this.#assertAlive();
|
|
922
|
+
if (!Number.isSafeInteger(index)) return;
|
|
923
|
+
const result = this.#state.search.results[index];
|
|
924
|
+
if (!result) return;
|
|
925
|
+
const pageChanged = this.#state.pageIndex !== result.pageIndex;
|
|
926
|
+
if (!this.#change({
|
|
927
|
+
search: Object.freeze({
|
|
928
|
+
...this.#state.search,
|
|
929
|
+
activeIndex: index
|
|
930
|
+
}),
|
|
931
|
+
pageIndex: result.pageIndex,
|
|
932
|
+
...pageChanged ? { selectedShape: null } : {}
|
|
933
|
+
})) return;
|
|
934
|
+
const revision = this.#revision;
|
|
935
|
+
if (pageChanged) this.#emit("page-change", result.pageIndex);
|
|
936
|
+
if (this.#destroyed || revision !== this.#revision) return;
|
|
937
|
+
const shape = {
|
|
938
|
+
id: result.shapeId,
|
|
939
|
+
name: result.shapeName,
|
|
940
|
+
pageId: result.pageId
|
|
941
|
+
};
|
|
942
|
+
if (this.#change({ selectedShape: shape })) this.#emit("shape-select", shape);
|
|
943
|
+
}
|
|
944
|
+
nextSearchResult() {
|
|
945
|
+
this.#assertAlive();
|
|
946
|
+
const { activeIndex, results } = this.#state.search;
|
|
947
|
+
if (results.length) this.selectSearchResult((activeIndex + 1) % results.length);
|
|
948
|
+
}
|
|
949
|
+
previousSearchResult() {
|
|
950
|
+
this.#assertAlive();
|
|
951
|
+
const { activeIndex, results } = this.#state.search;
|
|
952
|
+
if (results.length) this.selectSearchResult(activeIndex <= 0 ? results.length - 1 : activeIndex - 1);
|
|
953
|
+
}
|
|
954
|
+
#inactiveSearch() {
|
|
955
|
+
return this.#state.search.activeIndex < 0 ? this.#state.search : Object.freeze({
|
|
956
|
+
...this.#state.search,
|
|
957
|
+
activeIndex: -1
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
async load(bytes) {
|
|
961
|
+
this.#assertAlive();
|
|
962
|
+
if (bytes.byteLength > 33554432) throw new Error("This viewer accepts files up to 32 MiB.");
|
|
963
|
+
const owned = bytes instanceof Uint8Array ? Uint8Array.from(bytes) : new Uint8Array(bytes.slice(0));
|
|
964
|
+
return this.loadSource(() => owned);
|
|
965
|
+
}
|
|
966
|
+
/** Reading bytes shares the same request epoch and error state as parsing them. */
|
|
967
|
+
async loadSource(read) {
|
|
968
|
+
this.#assertAlive();
|
|
969
|
+
const id = ++this.#loadId;
|
|
970
|
+
this.#invalidateEdit();
|
|
971
|
+
this.parser.cancel?.();
|
|
972
|
+
this.#change({
|
|
973
|
+
loading: true,
|
|
974
|
+
error: null,
|
|
975
|
+
edit: this.#history?.state ?? EMPTY_EDIT_STATE
|
|
976
|
+
});
|
|
977
|
+
if (this.#destroyed || id !== this.#loadId) return;
|
|
978
|
+
let document;
|
|
979
|
+
let history;
|
|
980
|
+
let visible;
|
|
981
|
+
try {
|
|
982
|
+
const bytes = await read();
|
|
983
|
+
if (this.#destroyed || id !== this.#loadId) return;
|
|
984
|
+
if (bytes.byteLength > 33554432) throw new Error("This viewer accepts files up to 32 MiB.");
|
|
985
|
+
const owned = bytes instanceof Uint8Array ? Uint8Array.from(bytes) : new Uint8Array(bytes.slice(0));
|
|
986
|
+
history = new DocumentHistory(owned);
|
|
987
|
+
document = await this.parser(Uint8Array.from(owned));
|
|
988
|
+
if (this.#destroyed || id !== this.#loadId) return;
|
|
989
|
+
assertViewableDocument(document);
|
|
990
|
+
visible = documentVisibility(document);
|
|
991
|
+
} catch (cause) {
|
|
992
|
+
if (this.#destroyed || id !== this.#loadId) return;
|
|
993
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
994
|
+
this.#change({
|
|
995
|
+
loading: false,
|
|
996
|
+
error
|
|
997
|
+
});
|
|
998
|
+
if (!this.#destroyed && id === this.#loadId) this.#emit("document-error", error);
|
|
999
|
+
throw error;
|
|
1000
|
+
}
|
|
1001
|
+
this.#history = history;
|
|
1002
|
+
this.#searchIndex = null;
|
|
1003
|
+
this.#visible = visible;
|
|
1004
|
+
++this.#documentGeneration;
|
|
1005
|
+
++this.#sourceGeneration;
|
|
1006
|
+
this.#change({
|
|
1007
|
+
document,
|
|
1008
|
+
edit: history.state,
|
|
1009
|
+
pageIndex: 0,
|
|
1010
|
+
loading: false,
|
|
1011
|
+
error: null,
|
|
1012
|
+
selectedShape: null,
|
|
1013
|
+
search: EMPTY_TEXT_SEARCH,
|
|
1014
|
+
layerVisibilityOverrides: EMPTY_LAYER_OVERRIDES
|
|
1015
|
+
});
|
|
1016
|
+
if (!this.#destroyed && id === this.#loadId) this.#emit("document-load", document);
|
|
1017
|
+
}
|
|
1018
|
+
cancelLoad() {
|
|
1019
|
+
this.#assertAlive();
|
|
1020
|
+
++this.#loadId;
|
|
1021
|
+
this.parser.cancel?.();
|
|
1022
|
+
this.#invalidateEdit();
|
|
1023
|
+
this.#change({
|
|
1024
|
+
loading: false,
|
|
1025
|
+
edit: this.#history?.state ?? EMPTY_EDIT_STATE
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
destroy() {
|
|
1029
|
+
if (this.#destroyed) return;
|
|
1030
|
+
this.#destroyed = true;
|
|
1031
|
+
this.#invalidateEdit();
|
|
1032
|
+
this.#history = null;
|
|
1033
|
+
++this.#documentGeneration;
|
|
1034
|
+
++this.#sourceGeneration;
|
|
1035
|
+
++this.#loadId;
|
|
1036
|
+
this.parser.cancel?.();
|
|
1037
|
+
this.#subscribers.clear();
|
|
1038
|
+
this.#events.clear();
|
|
1039
|
+
this.#searchIndex = null;
|
|
1040
|
+
this.#visible = documentVisibility(null);
|
|
1041
|
+
this.#state = Object.freeze({
|
|
1042
|
+
document: null,
|
|
1043
|
+
edit: EMPTY_EDIT_STATE,
|
|
1044
|
+
pageIndex: 0,
|
|
1045
|
+
zoom: 1,
|
|
1046
|
+
loading: false,
|
|
1047
|
+
error: null,
|
|
1048
|
+
selectedShape: null,
|
|
1049
|
+
search: EMPTY_TEXT_SEARCH,
|
|
1050
|
+
layerVisibilityOverrides: EMPTY_LAYER_OVERRIDES
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
/** Replace one source-backed local plain-text target. Core decides target support. */
|
|
1054
|
+
async replacePlainText(pageId, shapeId, text) {
|
|
1055
|
+
return this.applyEdits([{
|
|
1056
|
+
type: "replace-plain-text",
|
|
1057
|
+
pageId,
|
|
1058
|
+
shapeId,
|
|
1059
|
+
text
|
|
1060
|
+
}]);
|
|
1061
|
+
}
|
|
1062
|
+
/** Atomic source-backed edits. Core owns protection, dependency and target validation. */
|
|
1063
|
+
async applyEdits(edits) {
|
|
1064
|
+
return this.#mutate("edit", snapshotEdits(edits));
|
|
1065
|
+
}
|
|
1066
|
+
async undo() {
|
|
1067
|
+
return this.#mutate("undo");
|
|
1068
|
+
}
|
|
1069
|
+
async redo() {
|
|
1070
|
+
return this.#mutate("redo");
|
|
1071
|
+
}
|
|
1072
|
+
cancelEdit() {
|
|
1073
|
+
this.#assertAlive();
|
|
1074
|
+
if (!this.#state.edit.busy) return;
|
|
1075
|
+
this.#invalidateEdit();
|
|
1076
|
+
this.parser.cancel?.();
|
|
1077
|
+
this.#change({ edit: this.#history?.state ?? EMPTY_EDIT_STATE });
|
|
1078
|
+
}
|
|
1079
|
+
exportVsdx() {
|
|
1080
|
+
this.#assertAlive();
|
|
1081
|
+
if (!this.#history) throw new Error("Load a VSDX file before downloading a source-backed copy.");
|
|
1082
|
+
if (this.#state.edit.busy || this.#state.loading) throw new Error("Wait for the current document operation before downloading.");
|
|
1083
|
+
return this.#history.export();
|
|
1084
|
+
}
|
|
1085
|
+
#invalidateEdit() {
|
|
1086
|
+
++this.#editId;
|
|
1087
|
+
this.editor.cancel?.();
|
|
1088
|
+
}
|
|
1089
|
+
async #mutate(kind, commands) {
|
|
1090
|
+
this.#assertAlive();
|
|
1091
|
+
const history = this.#history;
|
|
1092
|
+
if (!history) throw new Error("Load a VSDX file before editing. Model-only documents are read only.");
|
|
1093
|
+
if (this.#state.loading || this.#state.edit.busy) throw new Error("Another document operation is in progress.");
|
|
1094
|
+
const target = kind === "undo" ? history.undoTarget : kind === "redo" ? history.redoTarget : void 0;
|
|
1095
|
+
if (kind !== "edit" && !target) return;
|
|
1096
|
+
const id = ++this.#editId;
|
|
1097
|
+
const loadId = this.#loadId;
|
|
1098
|
+
const current = () => !this.#destroyed && id === this.#editId && loadId === this.#loadId && history === this.#history;
|
|
1099
|
+
const assertCurrent = () => {
|
|
1100
|
+
if (!current()) throw new DOMException("The diagram edit was superseded or cancelled.", "AbortError");
|
|
1101
|
+
};
|
|
1102
|
+
this.#change({ edit: Object.freeze({
|
|
1103
|
+
...history.state,
|
|
1104
|
+
busy: true
|
|
1105
|
+
}) });
|
|
1106
|
+
assertCurrent();
|
|
1107
|
+
try {
|
|
1108
|
+
let document;
|
|
1109
|
+
let edited;
|
|
1110
|
+
if (kind === "edit") {
|
|
1111
|
+
edited = await this.editor(Uint8Array.from(history.current.bytes), commands);
|
|
1112
|
+
assertCurrent();
|
|
1113
|
+
if (edited.bytes.byteLength > 33554432) throw new Error("The modified VSDX exceeds 32 MiB.");
|
|
1114
|
+
document = edited.document;
|
|
1115
|
+
} else document = await this.parser(Uint8Array.from(target.bytes));
|
|
1116
|
+
assertCurrent();
|
|
1117
|
+
assertViewableDocument(document);
|
|
1118
|
+
if (edited && !edited.changedParts.length) {
|
|
1119
|
+
this.#change({ edit: history.state });
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
const visible = documentVisibility(document, this.#state.layerVisibilityOverrides);
|
|
1123
|
+
const searchIndex = this.#state.search.query ? indexDocumentText(document, (shape) => visible.get(shape) === true) : null;
|
|
1124
|
+
const search = searchIndex ? searchDocumentText(searchIndex, this.#state.search.query) : EMPTY_TEXT_SEARCH;
|
|
1125
|
+
const selection = this.#state.selectedShape;
|
|
1126
|
+
const selectedShape = selection && visibleSelection(document, this.#state.pageIndex, selection, visible) ? selection : null;
|
|
1127
|
+
if (edited) history.append(edited.bytes, edited.diagnostics);
|
|
1128
|
+
else history.move(target);
|
|
1129
|
+
this.#visible = visible;
|
|
1130
|
+
this.#searchIndex = searchIndex;
|
|
1131
|
+
++this.#documentGeneration;
|
|
1132
|
+
if (this.#change({
|
|
1133
|
+
document,
|
|
1134
|
+
edit: history.state,
|
|
1135
|
+
search,
|
|
1136
|
+
selectedShape,
|
|
1137
|
+
error: null
|
|
1138
|
+
}) && current()) this.#emit("document-change", {
|
|
1139
|
+
document,
|
|
1140
|
+
dirty: history.state.dirty,
|
|
1141
|
+
kind
|
|
1142
|
+
});
|
|
1143
|
+
} catch (cause) {
|
|
1144
|
+
if (!current()) throw new DOMException("The diagram edit was superseded or cancelled.", "AbortError");
|
|
1145
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
1146
|
+
this.#change({ edit: Object.freeze({
|
|
1147
|
+
...history.state,
|
|
1148
|
+
error
|
|
1149
|
+
}) });
|
|
1150
|
+
throw error;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
#assertAlive() {
|
|
1154
|
+
if (this.#destroyed) throw new Error("The viewer has been destroyed.");
|
|
1155
|
+
}
|
|
1156
|
+
#change(change) {
|
|
1157
|
+
const revision = ++this.#revision;
|
|
1158
|
+
this.#state = Object.freeze({
|
|
1159
|
+
...this.#state,
|
|
1160
|
+
...change
|
|
1161
|
+
});
|
|
1162
|
+
for (const listener of [...this.#subscribers]) {
|
|
1163
|
+
if (this.#destroyed || revision !== this.#revision) break;
|
|
1164
|
+
if (this.#subscribers.has(listener)) this.#notify(() => listener(this.#state));
|
|
1165
|
+
}
|
|
1166
|
+
return !this.#destroyed && revision === this.#revision;
|
|
1167
|
+
}
|
|
1168
|
+
#notify(callback) {
|
|
1169
|
+
try {
|
|
1170
|
+
callback();
|
|
1171
|
+
return true;
|
|
1172
|
+
} catch (error) {
|
|
1173
|
+
try {
|
|
1174
|
+
this.listenerError(error);
|
|
1175
|
+
} catch {}
|
|
1176
|
+
return false;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
#emit(name, detail) {
|
|
1180
|
+
const revision = this.#revision;
|
|
1181
|
+
for (const listener of [...this.#events]) {
|
|
1182
|
+
if (this.#destroyed || revision !== this.#revision) break;
|
|
1183
|
+
if (this.#events.has(listener)) this.#notify(() => listener(name, detail));
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
};
|
|
1187
|
+
//#endregion
|
|
1188
|
+
//#region src/contract.ts
|
|
1189
|
+
var propertyKeys = [
|
|
1190
|
+
"document",
|
|
1191
|
+
"pageIndex",
|
|
1192
|
+
"zoom",
|
|
1193
|
+
"showToolbar"
|
|
1194
|
+
];
|
|
1195
|
+
var eventKeys = [
|
|
1196
|
+
"document-load",
|
|
1197
|
+
"document-change",
|
|
1198
|
+
"document-error",
|
|
1199
|
+
"page-change",
|
|
1200
|
+
"zoom-change",
|
|
1201
|
+
"shape-select"
|
|
1202
|
+
];
|
|
1203
|
+
//#endregion
|
|
1204
|
+
//#region src/worker-parser.ts
|
|
1205
|
+
/** One disposable worker per document prevents old parses and long CPU tasks from occupying the UI. */
|
|
1206
|
+
function createWorkerParser(factory, timeoutMs = 15e3) {
|
|
1207
|
+
if (!factory && typeof Worker === "undefined") return parseVsdx;
|
|
1208
|
+
const create = factory ?? (() => new Worker(new URL(
|
|
1209
|
+
/* @vite-ignore */
|
|
1210
|
+
new URL("assets/parse-worker-B8EBQQpD.js", import.meta.url).href,
|
|
1211
|
+
"" + import.meta.url
|
|
1212
|
+
), { type: "module" }));
|
|
1213
|
+
let cancel;
|
|
1214
|
+
const parse = (bytes) => {
|
|
1215
|
+
cancel?.();
|
|
1216
|
+
if (bytes.byteLength > 33554432) return Promise.reject(/* @__PURE__ */ new Error("This viewer accepts files up to 32 MiB."));
|
|
1217
|
+
return new Promise((resolve, reject) => {
|
|
1218
|
+
let worker;
|
|
1219
|
+
try {
|
|
1220
|
+
worker = create();
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
reject(error);
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
let settled = false;
|
|
1226
|
+
const finish = (error, document) => {
|
|
1227
|
+
if (settled) return;
|
|
1228
|
+
settled = true;
|
|
1229
|
+
clearTimeout(timer);
|
|
1230
|
+
worker.onmessage = null;
|
|
1231
|
+
worker.onerror = null;
|
|
1232
|
+
worker.terminate();
|
|
1233
|
+
cancel = void 0;
|
|
1234
|
+
if (error) reject(error);
|
|
1235
|
+
else resolve(document);
|
|
1236
|
+
};
|
|
1237
|
+
const timer = setTimeout(() => finish(/* @__PURE__ */ new Error(`The diagram exceeded the ${timeoutMs / 1e3}-second isolated parsing limit.`)), timeoutMs);
|
|
1238
|
+
cancel = () => finish(new DOMException("The diagram load was superseded or cancelled.", "AbortError"));
|
|
1239
|
+
worker.onmessage = (event) => {
|
|
1240
|
+
const result = event.data;
|
|
1241
|
+
if (result.ok && result.document) finish(void 0, result.document);
|
|
1242
|
+
else finish(new Error(result.message ?? "The Visio parser worker returned an invalid response."));
|
|
1243
|
+
};
|
|
1244
|
+
worker.onerror = (event) => finish(new Error(event.message || "The Visio parser worker failed."));
|
|
1245
|
+
try {
|
|
1246
|
+
const copy = bytes instanceof Uint8Array ? Uint8Array.from(bytes) : new Uint8Array(bytes.slice(0));
|
|
1247
|
+
worker.postMessage(copy.buffer, [copy.buffer]);
|
|
1248
|
+
} catch (cause) {
|
|
1249
|
+
finish(cause instanceof Error ? cause : new Error(String(cause)));
|
|
1250
|
+
}
|
|
1251
|
+
});
|
|
1252
|
+
};
|
|
1253
|
+
parse.cancel = () => cancel?.();
|
|
1254
|
+
return parse;
|
|
1255
|
+
}
|
|
1256
|
+
//#endregion
|
|
1257
|
+
//#region src/render-fill.ts
|
|
1258
|
+
var gradientId = 0;
|
|
1259
|
+
function fillPaint(style, defs) {
|
|
1260
|
+
if (!style.fillGradient) return safeColor(style.fill, "#fff");
|
|
1261
|
+
const gradient = svgElement("linearGradient"), paint = style.fillGradient;
|
|
1262
|
+
gradient.id = `visio-fill-${++gradientId}`;
|
|
1263
|
+
gradient.setAttribute("gradientUnits", "userSpaceOnUse");
|
|
1264
|
+
gradient.setAttribute("x1", String(paint.start[0]));
|
|
1265
|
+
gradient.setAttribute("y1", String(paint.start[1]));
|
|
1266
|
+
gradient.setAttribute("x2", String(paint.end[0]));
|
|
1267
|
+
gradient.setAttribute("y2", String(paint.end[1]));
|
|
1268
|
+
for (const color of paint.stops) {
|
|
1269
|
+
const stop = svgElement("stop");
|
|
1270
|
+
stop.setAttribute("offset", String(color.offset));
|
|
1271
|
+
stop.setAttribute("stop-color", safeColor(color.color));
|
|
1272
|
+
stop.setAttribute("stop-opacity", String(color.opacity));
|
|
1273
|
+
gradient.append(stop);
|
|
1274
|
+
}
|
|
1275
|
+
defs.append(gradient);
|
|
1276
|
+
return `url(#${gradient.id})`;
|
|
1277
|
+
}
|
|
1278
|
+
//#endregion
|
|
1279
|
+
//#region src/render-image.ts
|
|
1280
|
+
var clipId$1 = 0;
|
|
1281
|
+
/** URLs are created only from bounded embedded raster bytes, never from document strings. */
|
|
1282
|
+
function renderImage(shape, resources) {
|
|
1283
|
+
if (!shape.image) return void 0;
|
|
1284
|
+
const image = shape.image;
|
|
1285
|
+
const url = resources.imageUrl(image);
|
|
1286
|
+
const node = resources.portable ? svgElement("use") : svgElement("image");
|
|
1287
|
+
const width = image.width ?? shape.width, height = image.height ?? shape.height;
|
|
1288
|
+
const x = image.x ?? 0, y = image.y ?? 0;
|
|
1289
|
+
if (resources.portable) node.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", url);
|
|
1290
|
+
else node.setAttribute("href", url);
|
|
1291
|
+
node.setAttribute("width", String(width));
|
|
1292
|
+
node.setAttribute("height", String(height));
|
|
1293
|
+
node.setAttribute("transform", `translate(${x} ${y + height}) scale(1 -1)`);
|
|
1294
|
+
node.setAttribute("preserveAspectRatio", "none");
|
|
1295
|
+
node.setAttribute("opacity", String(image.opacity ?? 1));
|
|
1296
|
+
const group = svgElement("g"), clip = svgElement("clipPath"), rectangle = svgElement("rect");
|
|
1297
|
+
clip.id = `visio-image-clip-${++clipId$1}`;
|
|
1298
|
+
clip.setAttribute("clipPathUnits", "userSpaceOnUse");
|
|
1299
|
+
rectangle.setAttribute("width", String(shape.width));
|
|
1300
|
+
rectangle.setAttribute("height", String(shape.height));
|
|
1301
|
+
clip.append(rectangle);
|
|
1302
|
+
const defs = svgElement("defs");
|
|
1303
|
+
defs.append(clip);
|
|
1304
|
+
group.append(defs);
|
|
1305
|
+
const cropped = svgElement("g");
|
|
1306
|
+
cropped.setAttribute("clip-path", `url(#${clip.id})`);
|
|
1307
|
+
cropped.append(node);
|
|
1308
|
+
group.append(cropped);
|
|
1309
|
+
return group;
|
|
1310
|
+
}
|
|
1311
|
+
//#endregion
|
|
1312
|
+
//#region src/render-foreign-vector.ts
|
|
1313
|
+
var NS$1 = "http://www.w3.org/2000/svg";
|
|
1314
|
+
var nextResource = 0;
|
|
1315
|
+
var pathData = (commands) => commands.map(({ command, values }) => `${command}${values.length ? " " + values.join(" ") : ""}`).join(" ");
|
|
1316
|
+
/** Render an independently validated inert scene. Never forwards source tags, IDs or attributes. */
|
|
1317
|
+
function renderForeignVector(source, owner = document, budget = new ForeignVectorBudget()) {
|
|
1318
|
+
const scene = budget.take(source).vector;
|
|
1319
|
+
const element = (tag) => owner.createElementNS(NS$1, tag);
|
|
1320
|
+
const root = element("svg"), defs = element("defs"), prefix = `visio-vector-${++nextResource}-`;
|
|
1321
|
+
root.setAttribute("viewBox", `0 0 ${scene.width} ${scene.height}`);
|
|
1322
|
+
root.setAttribute("width", String(scene.width));
|
|
1323
|
+
root.setAttribute("height", String(scene.height));
|
|
1324
|
+
root.setAttribute("preserveAspectRatio", "none");
|
|
1325
|
+
root.setAttribute("overflow", "hidden");
|
|
1326
|
+
const transform = (node, matrix) => node.setAttribute("transform", `matrix(${matrix.join(" ")})`);
|
|
1327
|
+
const clip = (node, index) => {
|
|
1328
|
+
if (index !== void 0) node.setAttribute("clip-path", `url(#${prefix}${index})`);
|
|
1329
|
+
};
|
|
1330
|
+
const clipPath = (item) => {
|
|
1331
|
+
const path = element("path");
|
|
1332
|
+
path.setAttribute("d", pathData(item.commands));
|
|
1333
|
+
path.setAttribute("clip-rule", item.clipRule);
|
|
1334
|
+
transform(path, item.matrix);
|
|
1335
|
+
clip(path, item.clipIndex);
|
|
1336
|
+
return path;
|
|
1337
|
+
};
|
|
1338
|
+
scene.clips.forEach((item, index) => {
|
|
1339
|
+
const path = element("clipPath");
|
|
1340
|
+
path.id = `${prefix}${index}`;
|
|
1341
|
+
path.setAttribute("clipPathUnits", "userSpaceOnUse");
|
|
1342
|
+
transform(path, item.matrix);
|
|
1343
|
+
clip(path, item.clipIndex);
|
|
1344
|
+
for (const child of item.items) path.append(clipPath(child));
|
|
1345
|
+
defs.append(path);
|
|
1346
|
+
});
|
|
1347
|
+
root.append(defs);
|
|
1348
|
+
const draw = (item) => {
|
|
1349
|
+
if (item.kind === "group") {
|
|
1350
|
+
const group = element("g");
|
|
1351
|
+
transform(group, item.matrix);
|
|
1352
|
+
clip(group, item.clipIndex);
|
|
1353
|
+
for (const child of item.items) group.append(draw(child));
|
|
1354
|
+
return group;
|
|
1355
|
+
}
|
|
1356
|
+
const path = element("path"), paint = item.paint;
|
|
1357
|
+
path.setAttribute("d", pathData(item.commands));
|
|
1358
|
+
transform(path, item.matrix);
|
|
1359
|
+
clip(path, item.clipIndex);
|
|
1360
|
+
for (const [name, value] of [
|
|
1361
|
+
["fill", paint.fill],
|
|
1362
|
+
["stroke", paint.stroke],
|
|
1363
|
+
["fill-rule", paint.fillRule],
|
|
1364
|
+
["stroke-width", paint.strokeWidth],
|
|
1365
|
+
["stroke-miterlimit", paint.strokeMiterlimit],
|
|
1366
|
+
["stroke-linecap", paint.strokeLinecap],
|
|
1367
|
+
["stroke-linejoin", paint.strokeLinejoin],
|
|
1368
|
+
["opacity", paint.opacity],
|
|
1369
|
+
["fill-opacity", paint.fillOpacity],
|
|
1370
|
+
["stroke-opacity", paint.strokeOpacity]
|
|
1371
|
+
]) path.setAttribute(name, String(value));
|
|
1372
|
+
return path;
|
|
1373
|
+
};
|
|
1374
|
+
for (const item of scene.items) root.append(draw(item));
|
|
1375
|
+
return root;
|
|
1376
|
+
}
|
|
1377
|
+
//#endregion
|
|
1378
|
+
//#region src/render-foreign-vector-shape.ts
|
|
1379
|
+
var clipId = 0;
|
|
1380
|
+
/** Core supplies shape-local placement; converter coordinates have a downward Y axis. */
|
|
1381
|
+
function renderForeignVectorShape(shape, budget) {
|
|
1382
|
+
const foreign = shape.foreignVector;
|
|
1383
|
+
if (!foreign) return void 0;
|
|
1384
|
+
const node = renderForeignVector(foreign.vector, document, budget);
|
|
1385
|
+
node.setAttribute("width", String(foreign.width));
|
|
1386
|
+
node.setAttribute("height", String(foreign.height));
|
|
1387
|
+
node.setAttribute("transform", `translate(${foreign.x} ${foreign.y + foreign.height}) scale(1 -1)`);
|
|
1388
|
+
node.setAttribute("opacity", String(foreign.opacity));
|
|
1389
|
+
const group = svgElement("g"), defs = svgElement("defs"), clip = svgElement("clipPath"), rectangle = svgElement("rect"), cropped = svgElement("g");
|
|
1390
|
+
clip.id = `visio-vector-frame-${++clipId}`;
|
|
1391
|
+
clip.setAttribute("clipPathUnits", "userSpaceOnUse");
|
|
1392
|
+
rectangle.setAttribute("width", String(shape.width));
|
|
1393
|
+
rectangle.setAttribute("height", String(shape.height));
|
|
1394
|
+
clip.append(rectangle);
|
|
1395
|
+
defs.append(clip);
|
|
1396
|
+
cropped.setAttribute("clip-path", `url(#${clip.id})`);
|
|
1397
|
+
cropped.append(node);
|
|
1398
|
+
group.append(defs, cropped);
|
|
1399
|
+
return group;
|
|
1400
|
+
}
|
|
1401
|
+
//#endregion
|
|
1402
|
+
//#region src/render-resources.ts
|
|
1403
|
+
var MAX_EMBEDDED_BYTES = 16777216;
|
|
1404
|
+
/** One reference per shared core image resource, with explicit per-render disposal. */
|
|
1405
|
+
var RenderResources = class {
|
|
1406
|
+
definitions;
|
|
1407
|
+
#images = /* @__PURE__ */ new Map();
|
|
1408
|
+
#disposed = false;
|
|
1409
|
+
#embeddedBytes = 0;
|
|
1410
|
+
#imageId = 0;
|
|
1411
|
+
constructor(definitions) {
|
|
1412
|
+
this.definitions = definitions;
|
|
1413
|
+
}
|
|
1414
|
+
get portable() {
|
|
1415
|
+
return !!this.definitions;
|
|
1416
|
+
}
|
|
1417
|
+
imageUrl(image) {
|
|
1418
|
+
if (this.#disposed) throw new Error("Render resources have been disposed.");
|
|
1419
|
+
let types = this.#images.get(image.bytes);
|
|
1420
|
+
if (!types) {
|
|
1421
|
+
types = /* @__PURE__ */ new Map();
|
|
1422
|
+
this.#images.set(image.bytes, types);
|
|
1423
|
+
}
|
|
1424
|
+
let url = types.get(image.mimeType);
|
|
1425
|
+
if (!url) {
|
|
1426
|
+
if (this.definitions) {
|
|
1427
|
+
const size = 4 * Math.ceil(image.bytes.byteLength / 3);
|
|
1428
|
+
if (this.#embeddedBytes + size > MAX_EMBEDDED_BYTES) throw new Error("Embedded raster resources exceed the safe SVG export limit.");
|
|
1429
|
+
this.#embeddedBytes += size;
|
|
1430
|
+
const symbol = svgElement("symbol");
|
|
1431
|
+
symbol.id = `visio-export-image-${++this.#imageId}`;
|
|
1432
|
+
symbol.setAttribute("viewBox", "0 0 1 1");
|
|
1433
|
+
symbol.setAttribute("preserveAspectRatio", "none");
|
|
1434
|
+
const node = svgElement("image");
|
|
1435
|
+
node.setAttribute("width", "1");
|
|
1436
|
+
node.setAttribute("height", "1");
|
|
1437
|
+
node.setAttribute("preserveAspectRatio", "none");
|
|
1438
|
+
node.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", `data:${image.mimeType};base64,${base64(image.bytes)}`);
|
|
1439
|
+
symbol.append(node);
|
|
1440
|
+
this.definitions.append(symbol);
|
|
1441
|
+
url = `#${symbol.id}`;
|
|
1442
|
+
} else url = URL.createObjectURL(new Blob([Uint8Array.from(image.bytes)], { type: image.mimeType }));
|
|
1443
|
+
types.set(image.mimeType, url);
|
|
1444
|
+
}
|
|
1445
|
+
return url;
|
|
1446
|
+
}
|
|
1447
|
+
dispose() {
|
|
1448
|
+
if (this.#disposed) return;
|
|
1449
|
+
this.#disposed = true;
|
|
1450
|
+
if (!this.portable) for (const types of this.#images.values()) for (const url of types.values()) URL.revokeObjectURL(url);
|
|
1451
|
+
this.#images.clear();
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
function base64(bytes) {
|
|
1455
|
+
const chunks = [];
|
|
1456
|
+
for (let offset = 0; offset < bytes.length; offset += 24576) chunks.push(btoa(String.fromCharCode(...bytes.subarray(offset, offset + 24576))));
|
|
1457
|
+
return chunks.join("");
|
|
1458
|
+
}
|
|
1459
|
+
//#endregion
|
|
1460
|
+
//#region src/arrowheads.ts
|
|
1461
|
+
var markerId = 0;
|
|
1462
|
+
/** Common Visio line/triangle and concave arrow styles. Sizes remain a documented visual approximation. */
|
|
1463
|
+
function applyArrowheads(path, style, defs, warnings) {
|
|
1464
|
+
for (const side of ["start", "end"]) {
|
|
1465
|
+
const code = side === "start" ? style.startArrow : style.endArrow;
|
|
1466
|
+
if (!code) continue;
|
|
1467
|
+
if (![
|
|
1468
|
+
1,
|
|
1469
|
+
2,
|
|
1470
|
+
3,
|
|
1471
|
+
4,
|
|
1472
|
+
5
|
|
1473
|
+
].includes(code)) {
|
|
1474
|
+
warnings.add(`Arrowhead style ${code} is not rendered in this build.`);
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
const size = side === "start" ? style.startArrowSize ?? 2 : style.endArrowSize ?? 2;
|
|
1478
|
+
const id = `visio-arrow-${++markerId}`;
|
|
1479
|
+
const marker = svgElement("marker");
|
|
1480
|
+
marker.id = id;
|
|
1481
|
+
marker.setAttribute("viewBox", code === 5 ? "-1 -5 11 10" : "0 -4 10 8");
|
|
1482
|
+
marker.setAttribute("refX", "9");
|
|
1483
|
+
marker.setAttribute("refY", "0");
|
|
1484
|
+
marker.setAttribute("orient", "auto-start-reverse");
|
|
1485
|
+
marker.setAttribute("markerUnits", "userSpaceOnUse");
|
|
1486
|
+
const length = (.1 + Math.max(0, Math.min(6, size)) * .045) * (code < 3 ? .85 : 1);
|
|
1487
|
+
marker.setAttribute("markerWidth", String(length));
|
|
1488
|
+
marker.setAttribute("markerHeight", String(length * .8));
|
|
1489
|
+
const glyph = svgElement("path");
|
|
1490
|
+
const filled = code === 2 || code === 4 || code === 5;
|
|
1491
|
+
glyph.setAttribute("d", code === 5 ? "M 0 -4 L 9 0 L 0 4 Q 4 0 0 -4 Z" : filled ? "M 0 -4 L 9 0 L 0 4 Z" : "M 0 -4 L 9 0 L 0 4");
|
|
1492
|
+
glyph.setAttribute("fill", filled ? safeColor(style.lineColor) : "none");
|
|
1493
|
+
glyph.setAttribute("stroke", safeColor(style.lineColor));
|
|
1494
|
+
glyph.setAttribute("stroke-width", "1");
|
|
1495
|
+
if (code === 5) glyph.setAttribute("stroke-linejoin", "round");
|
|
1496
|
+
glyph.setAttribute("opacity", String(style.lineOpacity));
|
|
1497
|
+
marker.append(glyph);
|
|
1498
|
+
defs.append(marker);
|
|
1499
|
+
path.setAttribute(`marker-${side}`, `url(#${id})`);
|
|
1500
|
+
warnings.add("Common arrowhead shapes are rendered; arrowhead sizing is approximate.");
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
//#endregion
|
|
1504
|
+
//#region src/text-layout.ts
|
|
1505
|
+
var DEFAULT_LIMITS = {
|
|
1506
|
+
maxCharacters: 25e4,
|
|
1507
|
+
maxMeasurements: 5e4,
|
|
1508
|
+
maxLines: 2e4,
|
|
1509
|
+
maxFragments: 3e4,
|
|
1510
|
+
maxShapeCharacters: 5e4,
|
|
1511
|
+
maxShapeMeasurements: 1e4,
|
|
1512
|
+
maxShapeLines: 5e3,
|
|
1513
|
+
maxShapeFragments: 1e4,
|
|
1514
|
+
maxCacheEntries: 4096
|
|
1515
|
+
};
|
|
1516
|
+
var TextLayoutBudgetError = class extends Error {
|
|
1517
|
+
constructor(message) {
|
|
1518
|
+
super(message);
|
|
1519
|
+
this.name = "TextLayoutBudgetError";
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
/** One bounded measurement cache and work budget per render; shape scopes share both. */
|
|
1523
|
+
var TextLayoutBudget = class TextLayoutBudget {
|
|
1524
|
+
limits;
|
|
1525
|
+
cache;
|
|
1526
|
+
parent;
|
|
1527
|
+
#characters = 0;
|
|
1528
|
+
#measurements = 0;
|
|
1529
|
+
#lines = 0;
|
|
1530
|
+
#fragments = 0;
|
|
1531
|
+
constructor(limits, cache, parent) {
|
|
1532
|
+
this.limits = limits;
|
|
1533
|
+
this.cache = cache;
|
|
1534
|
+
this.parent = parent;
|
|
1535
|
+
}
|
|
1536
|
+
static create(options = {}) {
|
|
1537
|
+
const limits = {
|
|
1538
|
+
...DEFAULT_LIMITS,
|
|
1539
|
+
...options
|
|
1540
|
+
};
|
|
1541
|
+
if (Object.values(limits).some((value) => !Number.isSafeInteger(value) || value <= 0)) throw new TextLayoutBudgetError("Text layout limits must be positive safe integers.");
|
|
1542
|
+
return new TextLayoutBudget(Object.freeze(limits), {
|
|
1543
|
+
byMeasure: /* @__PURE__ */ new WeakMap(),
|
|
1544
|
+
entries: 0
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
get characters() {
|
|
1548
|
+
return this.#characters;
|
|
1549
|
+
}
|
|
1550
|
+
get measurements() {
|
|
1551
|
+
return this.#measurements;
|
|
1552
|
+
}
|
|
1553
|
+
get lines() {
|
|
1554
|
+
return this.#lines;
|
|
1555
|
+
}
|
|
1556
|
+
get fragments() {
|
|
1557
|
+
return this.#fragments;
|
|
1558
|
+
}
|
|
1559
|
+
/** A shape may be omitted without consuming the rest of the page's budget. */
|
|
1560
|
+
forShape() {
|
|
1561
|
+
return new TextLayoutBudget(Object.freeze({
|
|
1562
|
+
...this.limits,
|
|
1563
|
+
maxCharacters: Math.min(this.limits.maxCharacters, this.limits.maxShapeCharacters),
|
|
1564
|
+
maxMeasurements: Math.min(this.limits.maxMeasurements, this.limits.maxShapeMeasurements),
|
|
1565
|
+
maxLines: Math.min(this.limits.maxLines, this.limits.maxShapeLines),
|
|
1566
|
+
maxFragments: Math.min(this.limits.maxFragments, this.limits.maxShapeFragments)
|
|
1567
|
+
}), this.cache, this);
|
|
1568
|
+
}
|
|
1569
|
+
reserveCharacters(count) {
|
|
1570
|
+
if (!Number.isSafeInteger(count) || count < 0 || this.#characters + count > this.limits.maxCharacters) throw new TextLayoutBudgetError("Text layout exceeds its character budget.");
|
|
1571
|
+
this.parent?.reserveCharacters(count);
|
|
1572
|
+
this.#characters += count;
|
|
1573
|
+
}
|
|
1574
|
+
reserveLine() {
|
|
1575
|
+
if (this.#lines >= this.limits.maxLines) throw new TextLayoutBudgetError("Text layout exceeds its line budget.");
|
|
1576
|
+
this.parent?.reserveLine();
|
|
1577
|
+
++this.#lines;
|
|
1578
|
+
}
|
|
1579
|
+
reserveFragment() {
|
|
1580
|
+
if (this.#fragments >= this.limits.maxFragments) throw new TextLayoutBudgetError("Text layout exceeds its styled-fragment budget.");
|
|
1581
|
+
this.parent?.reserveFragment();
|
|
1582
|
+
++this.#fragments;
|
|
1583
|
+
}
|
|
1584
|
+
reserveMeasurement() {
|
|
1585
|
+
if (this.#measurements >= this.limits.maxMeasurements) throw new TextLayoutBudgetError("Text layout exceeds its measurement budget.");
|
|
1586
|
+
this.parent?.reserveMeasurement();
|
|
1587
|
+
++this.#measurements;
|
|
1588
|
+
}
|
|
1589
|
+
measure(text, run, measure) {
|
|
1590
|
+
if (run.fontFamily.length > 1024 || !Number.isFinite(run.fontSize) || run.fontSize < 0 || run.fontSize > 100) throw new TextLayoutBudgetError("Text layout has an invalid font specification.");
|
|
1591
|
+
const key = text.length <= 256 ? JSON.stringify([
|
|
1592
|
+
run.fontFamily,
|
|
1593
|
+
run.fontSize,
|
|
1594
|
+
run.bold,
|
|
1595
|
+
run.italic,
|
|
1596
|
+
text
|
|
1597
|
+
]) : void 0;
|
|
1598
|
+
const cache = this.cache.byMeasure.get(measure);
|
|
1599
|
+
const cached = key === void 0 ? void 0 : cache?.get(key);
|
|
1600
|
+
if (cached !== void 0) return cached;
|
|
1601
|
+
this.reserveMeasurement();
|
|
1602
|
+
const width = measure(text, run);
|
|
1603
|
+
if (!Number.isFinite(width) || width < 0) throw new TextLayoutBudgetError("Text measurement returned an invalid width.");
|
|
1604
|
+
if (key !== void 0 && this.cache.entries < this.limits.maxCacheEntries) {
|
|
1605
|
+
const entries = cache ?? /* @__PURE__ */ new Map();
|
|
1606
|
+
if (!cache) this.cache.byMeasure.set(measure, entries);
|
|
1607
|
+
entries.set(key, width);
|
|
1608
|
+
++this.cache.entries;
|
|
1609
|
+
}
|
|
1610
|
+
return width;
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
var createTextLayoutBudget = (options = {}) => TextLayoutBudget.create(options);
|
|
1614
|
+
var canvas;
|
|
1615
|
+
var measureBrowserText = (text, run) => {
|
|
1616
|
+
if (canvas === void 0) canvas = typeof CanvasRenderingContext2D === "undefined" ? null : document.createElement("canvas").getContext("2d");
|
|
1617
|
+
if (!canvas) return Array.from(text).length * run.fontSize * .55;
|
|
1618
|
+
canvas.font = `${run.italic ? "italic " : ""}${run.bold ? "bold " : ""}${run.fontSize * 96}px ${JSON.stringify(run.fontFamily)}`;
|
|
1619
|
+
return canvas.measureText(text).width / 96;
|
|
1620
|
+
};
|
|
1621
|
+
/** Rendering-only greedy wrapping. Inject one budget across shapes to bound total UI-thread work. */
|
|
1622
|
+
function wrapText(runs, width, measure = measureBrowserText, firstLineIndent = 0, budget = createTextLayoutBudget()) {
|
|
1623
|
+
if (!Number.isFinite(width) || !Number.isFinite(firstLineIndent) || runs.length > 1e4) throw new TextLayoutBudgetError("Text layout has invalid dimensions or too many runs.");
|
|
1624
|
+
const scope = budget.forShape();
|
|
1625
|
+
scope.reserveCharacters(runs.reduce((count, run) => count + run.text.length, 0));
|
|
1626
|
+
const boundedMeasure = (value, run) => scope.measure(value, run, measure);
|
|
1627
|
+
const lines = [];
|
|
1628
|
+
const available = () => Math.max(.001, width - (lines.length === 0 ? firstLineIndent : 0));
|
|
1629
|
+
let line = {
|
|
1630
|
+
runs: [],
|
|
1631
|
+
width: 0,
|
|
1632
|
+
height: 0
|
|
1633
|
+
};
|
|
1634
|
+
const finish = () => {
|
|
1635
|
+
if (lines.length >= 5e3) throw new TextLayoutBudgetError("Text layout exceeds the safe line limit.");
|
|
1636
|
+
scope.reserveLine();
|
|
1637
|
+
lines.push(line);
|
|
1638
|
+
line = {
|
|
1639
|
+
runs: [],
|
|
1640
|
+
width: 0,
|
|
1641
|
+
height: 0
|
|
1642
|
+
};
|
|
1643
|
+
};
|
|
1644
|
+
const append = (text, run) => {
|
|
1645
|
+
if (!text) return;
|
|
1646
|
+
const size = boundedMeasure(text, run);
|
|
1647
|
+
const last = line.runs.at(-1);
|
|
1648
|
+
if (last && last.fontFamily === run.fontFamily && last.fontSize === run.fontSize && last.color === run.color && last.bold === run.bold && last.italic === run.italic && last.underline === run.underline) last.text += text;
|
|
1649
|
+
else {
|
|
1650
|
+
scope.reserveFragment();
|
|
1651
|
+
line.runs.push({
|
|
1652
|
+
...run,
|
|
1653
|
+
text
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
line.width += size;
|
|
1657
|
+
line.height = Math.max(line.height, run.fontSize * 1.2);
|
|
1658
|
+
};
|
|
1659
|
+
for (const run of runs) {
|
|
1660
|
+
const tokens = run.text.replace(/\r\n?/g, "\n").match(/\n|[^\S\n]+|[^\s]+/gu) ?? [];
|
|
1661
|
+
for (const token of tokens) {
|
|
1662
|
+
if (token === "\n") {
|
|
1663
|
+
line.height = Math.max(line.height, run.fontSize * 1.2);
|
|
1664
|
+
finish();
|
|
1665
|
+
continue;
|
|
1666
|
+
}
|
|
1667
|
+
const whitespace = /^\s+$/u.test(token);
|
|
1668
|
+
const size = boundedMeasure(token, run);
|
|
1669
|
+
if (line.runs.length && line.width + size > available()) {
|
|
1670
|
+
finish();
|
|
1671
|
+
if (whitespace) continue;
|
|
1672
|
+
}
|
|
1673
|
+
if (size <= available() || width <= 0) {
|
|
1674
|
+
append(token, run);
|
|
1675
|
+
continue;
|
|
1676
|
+
}
|
|
1677
|
+
let fragment = "", fragmentWidth = 0;
|
|
1678
|
+
for (const point of token) {
|
|
1679
|
+
const pointWidth = boundedMeasure(point, run);
|
|
1680
|
+
if (fragment && fragmentWidth + pointWidth > available()) {
|
|
1681
|
+
append(fragment, run);
|
|
1682
|
+
finish();
|
|
1683
|
+
fragment = "";
|
|
1684
|
+
fragmentWidth = 0;
|
|
1685
|
+
}
|
|
1686
|
+
fragment += point;
|
|
1687
|
+
fragmentWidth += pointWidth;
|
|
1688
|
+
}
|
|
1689
|
+
append(fragment, run);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
if (line.runs.length || !lines.length) finish();
|
|
1693
|
+
return lines;
|
|
1694
|
+
}
|
|
1695
|
+
//#endregion
|
|
1696
|
+
//#region src/paragraph-layout.ts
|
|
1697
|
+
function layoutParagraphs(text, measure = measureBrowserText, budget = createTextLayoutBudget()) {
|
|
1698
|
+
const scope = budget.forShape();
|
|
1699
|
+
const runs = text.runs.length ? text.runs : [{
|
|
1700
|
+
text: text.plainText,
|
|
1701
|
+
fontFamily: text.fontFamily,
|
|
1702
|
+
fontSize: text.fontSize,
|
|
1703
|
+
color: text.color,
|
|
1704
|
+
bold: false,
|
|
1705
|
+
italic: false,
|
|
1706
|
+
underline: false
|
|
1707
|
+
}];
|
|
1708
|
+
if (runs.length > 1e4 || text.plainText.length > scope.limits.maxCharacters) throw new TextLayoutBudgetError("Text layout exceeds its per-shape run/character limit.");
|
|
1709
|
+
const offsets = [0];
|
|
1710
|
+
for (const run of runs) {
|
|
1711
|
+
const next = offsets[offsets.length - 1] + run.text.length;
|
|
1712
|
+
if (next > scope.limits.maxCharacters) throw new TextLayoutBudgetError("Text layout exceeds its per-shape character limit.");
|
|
1713
|
+
offsets.push(next);
|
|
1714
|
+
}
|
|
1715
|
+
const paragraphs = text.paragraphs?.length ? text.paragraphs : [{
|
|
1716
|
+
start: 0,
|
|
1717
|
+
end: text.plainText.length,
|
|
1718
|
+
horizontalAlign: text.horizontalAlign,
|
|
1719
|
+
indentLeft: 0,
|
|
1720
|
+
indentRight: 0,
|
|
1721
|
+
indentFirst: 0,
|
|
1722
|
+
spaceBefore: 0,
|
|
1723
|
+
spaceAfter: 0,
|
|
1724
|
+
lineSpacing: {
|
|
1725
|
+
kind: "multiple",
|
|
1726
|
+
value: 1.2
|
|
1727
|
+
},
|
|
1728
|
+
direction: "ltr"
|
|
1729
|
+
}];
|
|
1730
|
+
if (paragraphs.length > 5e3) throw new TextLayoutBudgetError("Text layout exceeds the safe paragraph limit.");
|
|
1731
|
+
let previousEnd = 0, coveredCharacters = 0;
|
|
1732
|
+
for (const paragraph of paragraphs) {
|
|
1733
|
+
if (!Number.isSafeInteger(paragraph.start) || !Number.isSafeInteger(paragraph.end) || paragraph.start < previousEnd || paragraph.end < paragraph.start || paragraph.end > text.plainText.length) throw new TextLayoutBudgetError("Paragraph ranges must be ordered, non-overlapping and within the text.");
|
|
1734
|
+
previousEnd = paragraph.end;
|
|
1735
|
+
coveredCharacters += paragraph.end - paragraph.start;
|
|
1736
|
+
}
|
|
1737
|
+
scope.reserveCharacters(text.plainText.length - coveredCharacters);
|
|
1738
|
+
const lines = [];
|
|
1739
|
+
let y = 0;
|
|
1740
|
+
for (const paragraph of paragraphs) {
|
|
1741
|
+
const source = sliceRuns(runs, offsets, paragraph.start, paragraph.end);
|
|
1742
|
+
const left = text.margins.left + paragraph.indentLeft, right = text.width - text.margins.right - paragraph.indentRight;
|
|
1743
|
+
const wrapped = wrapText(source, Math.max(.001, right - left), measure, paragraph.indentFirst, scope);
|
|
1744
|
+
y += paragraph.spaceBefore;
|
|
1745
|
+
for (const [index, line] of wrapped.entries()) {
|
|
1746
|
+
if (lines.length >= 5e3) throw new TextLayoutBudgetError("Text layout exceeds the safe line limit.");
|
|
1747
|
+
const minX = left + (index === 0 ? paragraph.indentFirst : 0);
|
|
1748
|
+
const x = paragraph.horizontalAlign === "center" ? (minX + right) / 2 : paragraph.horizontalAlign === "right" ? right : minX;
|
|
1749
|
+
const largestFont = line.runs.reduce((size, run) => Math.max(size, run.fontSize), 0) || text.fontSize;
|
|
1750
|
+
const height = Math.max(.001, paragraph.lineSpacing.kind === "exact" ? paragraph.lineSpacing.value : largestFont * paragraph.lineSpacing.value);
|
|
1751
|
+
lines.push({
|
|
1752
|
+
...line,
|
|
1753
|
+
height,
|
|
1754
|
+
x,
|
|
1755
|
+
y: y + largestFont * .8,
|
|
1756
|
+
availableWidth: Math.max(.001, right - minX),
|
|
1757
|
+
align: paragraph.horizontalAlign,
|
|
1758
|
+
direction: paragraph.direction,
|
|
1759
|
+
last: index === wrapped.length - 1,
|
|
1760
|
+
...index === 0 && paragraph.bullet ? { bullet: {
|
|
1761
|
+
...paragraph.bullet,
|
|
1762
|
+
x: left - paragraph.bullet.offset
|
|
1763
|
+
} } : {}
|
|
1764
|
+
});
|
|
1765
|
+
y += height;
|
|
1766
|
+
}
|
|
1767
|
+
y += paragraph.spaceAfter;
|
|
1768
|
+
}
|
|
1769
|
+
return {
|
|
1770
|
+
lines,
|
|
1771
|
+
height: y
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
function sliceRuns(runs, offsets, start, end) {
|
|
1775
|
+
if (start === end) return [];
|
|
1776
|
+
let lo = 0, hi = runs.length;
|
|
1777
|
+
while (lo < hi) {
|
|
1778
|
+
const mid = lo + hi >>> 1;
|
|
1779
|
+
if (offsets[mid + 1] <= start) lo = mid + 1;
|
|
1780
|
+
else hi = mid;
|
|
1781
|
+
}
|
|
1782
|
+
const result = [];
|
|
1783
|
+
for (let index = lo; index < runs.length && offsets[index] < end; index++) {
|
|
1784
|
+
const run = runs[index], offset = offsets[index];
|
|
1785
|
+
const text = run.text.slice(Math.max(0, start - offset), Math.max(0, end - offset));
|
|
1786
|
+
if (text) result.push({
|
|
1787
|
+
...run,
|
|
1788
|
+
text
|
|
1789
|
+
});
|
|
1790
|
+
}
|
|
1791
|
+
return result;
|
|
1792
|
+
}
|
|
1793
|
+
//#endregion
|
|
1794
|
+
//#region src/render-text.ts
|
|
1795
|
+
/** Browser-font layout is approximate until compared with the licensed Visio reference corpus. */
|
|
1796
|
+
function renderText(text, warnings, budget) {
|
|
1797
|
+
const group = svgElement("g");
|
|
1798
|
+
group.setAttribute("transform", `${matrix$1(text.transform)} translate(0 ${text.height}) scale(1 -1)`);
|
|
1799
|
+
const backgrounds = svgElement("g"), bullets = svgElement("g");
|
|
1800
|
+
group.append(backgrounds);
|
|
1801
|
+
const node = svgElement("text");
|
|
1802
|
+
node.style.whiteSpace = "pre";
|
|
1803
|
+
node.setAttribute("font-family", text.fontFamily);
|
|
1804
|
+
node.setAttribute("font-size", String(text.fontSize));
|
|
1805
|
+
node.setAttribute("fill", safeColor(text.color));
|
|
1806
|
+
let layout;
|
|
1807
|
+
try {
|
|
1808
|
+
layout = layoutParagraphs(text, void 0, budget);
|
|
1809
|
+
} catch {
|
|
1810
|
+
warnings.add("Text was omitted because it exceeded the safe layout complexity limit.");
|
|
1811
|
+
return group;
|
|
1812
|
+
}
|
|
1813
|
+
const y = text.verticalAlign === "top" ? text.margins.top : text.verticalAlign === "bottom" ? text.height - text.margins.bottom - layout.height : (text.height - layout.height) / 2;
|
|
1814
|
+
for (const line of layout.lines) {
|
|
1815
|
+
if (text.backgroundColor) {
|
|
1816
|
+
const rectangle = svgElement("rect");
|
|
1817
|
+
const width = line.align === "distributed" || line.align === "justify" && !line.last ? line.availableWidth : line.width;
|
|
1818
|
+
const x = line.align === "center" ? line.x - width / 2 : line.align === "right" ? line.x - width : line.x;
|
|
1819
|
+
const font = line.runs.reduce((size, run) => Math.max(size, run.fontSize), 0) || text.fontSize;
|
|
1820
|
+
rectangle.setAttribute("x", String(x));
|
|
1821
|
+
rectangle.setAttribute("y", String(y + line.y - font * .8));
|
|
1822
|
+
rectangle.setAttribute("width", String(width));
|
|
1823
|
+
rectangle.setAttribute("height", String(font * 1.2));
|
|
1824
|
+
rectangle.setAttribute("fill", safeColor(text.backgroundColor));
|
|
1825
|
+
rectangle.setAttribute("fill-opacity", String(text.backgroundOpacity ?? 1));
|
|
1826
|
+
backgrounds.append(rectangle);
|
|
1827
|
+
}
|
|
1828
|
+
const row = svgElement("tspan");
|
|
1829
|
+
row.setAttribute("x", String(line.x));
|
|
1830
|
+
row.setAttribute("y", String(y + line.y));
|
|
1831
|
+
row.setAttribute("direction", line.direction);
|
|
1832
|
+
row.setAttribute("unicode-bidi", "plaintext");
|
|
1833
|
+
const right = line.align === "right";
|
|
1834
|
+
row.setAttribute("text-anchor", line.align === "center" ? "middle" : right === (line.direction === "rtl") ? "start" : "end");
|
|
1835
|
+
if (line.align === "justify" && !line.last) {
|
|
1836
|
+
const spaces = line.runs.reduce((count, run) => count + (run.text.match(/\s/gu)?.length ?? 0), 0);
|
|
1837
|
+
if (spaces) row.setAttribute("word-spacing", String(Math.max(0, (line.availableWidth - line.width) / spaces)));
|
|
1838
|
+
}
|
|
1839
|
+
if (line.align === "distributed") {
|
|
1840
|
+
row.setAttribute("textLength", String(line.availableWidth));
|
|
1841
|
+
row.setAttribute("lengthAdjust", "spacing");
|
|
1842
|
+
}
|
|
1843
|
+
for (const run of line.runs) {
|
|
1844
|
+
const span = svgElement("tspan");
|
|
1845
|
+
span.textContent = run.text;
|
|
1846
|
+
span.setAttribute("font-family", run.fontFamily);
|
|
1847
|
+
span.setAttribute("font-size", String(run.fontSize));
|
|
1848
|
+
span.setAttribute("fill", safeColor(run.color));
|
|
1849
|
+
if (run.bold) span.setAttribute("font-weight", "bold");
|
|
1850
|
+
if (run.italic) span.setAttribute("font-style", "italic");
|
|
1851
|
+
if (run.underline) span.setAttribute("text-decoration", "underline");
|
|
1852
|
+
row.append(span);
|
|
1853
|
+
}
|
|
1854
|
+
node.append(row);
|
|
1855
|
+
if (line.bullet) {
|
|
1856
|
+
const bullet = svgElement("text");
|
|
1857
|
+
bullet.textContent = line.bullet.text;
|
|
1858
|
+
bullet.setAttribute("x", String(line.bullet.x));
|
|
1859
|
+
bullet.setAttribute("y", String(y + line.y));
|
|
1860
|
+
bullet.setAttribute("font-family", line.bullet.fontFamily);
|
|
1861
|
+
bullet.setAttribute("font-size", String(line.bullet.fontSize));
|
|
1862
|
+
bullet.setAttribute("fill", safeColor(text.color));
|
|
1863
|
+
bullets.append(bullet);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
warnings.add("Text metrics and automatic wrapping use browser fonts; Visio line breaking and paragraph layout remain approximate.");
|
|
1867
|
+
if (layout.height > text.height - text.margins.top - text.margins.bottom) warnings.add("Text exceeds its saved text-box height.");
|
|
1868
|
+
group.append(bullets, node);
|
|
1869
|
+
return group;
|
|
1870
|
+
}
|
|
1871
|
+
//#endregion
|
|
1872
|
+
//#region src/render-svg.ts
|
|
1873
|
+
var NS = "http://www.w3.org/2000/svg";
|
|
1874
|
+
function svgElement(name) {
|
|
1875
|
+
return document.createElementNS(NS, name);
|
|
1876
|
+
}
|
|
1877
|
+
function safeColor(value, fallback = "#273c46") {
|
|
1878
|
+
return /^(#[\da-f]{3,8}|[a-z]+|(?:rgb|hsl)a?\([\d.,%\s+-]+\))$/i.test(value) ? value : fallback;
|
|
1879
|
+
}
|
|
1880
|
+
function matrix$1(value) {
|
|
1881
|
+
return `matrix(${value.map((number) => Number.isFinite(number) ? number : 0).join(" ")})`;
|
|
1882
|
+
}
|
|
1883
|
+
function renderPage(model, page, options = {}) {
|
|
1884
|
+
assertViewableDocument(model);
|
|
1885
|
+
if (!model.pages.includes(page)) throw new Error("The selected page does not belong to this document.");
|
|
1886
|
+
const overrides = layerOverrideMaps(model, options.layerVisibilityOverrides ?? EMPTY_LAYER_OVERRIDES);
|
|
1887
|
+
const visible = overrides.size ? /* @__PURE__ */ new WeakMap() : void 0;
|
|
1888
|
+
const pages = getVisioPageLayers(model, page.id);
|
|
1889
|
+
for (const source of visible ? pages : []) {
|
|
1890
|
+
const layerVisibilityOverrides = overrides.get(source.id);
|
|
1891
|
+
for (const entry of resolveVisioPageVisibility(source, layerVisibilityOverrides ? { layerVisibilityOverrides } : {})) visible.set(entry.shape, !entry.hidden);
|
|
1892
|
+
}
|
|
1893
|
+
const svg = svgElement("svg");
|
|
1894
|
+
if (!options.static) svg.classList.add("paper");
|
|
1895
|
+
svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns", NS);
|
|
1896
|
+
if (options.static) svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink");
|
|
1897
|
+
svg.setAttribute("viewBox", `0 0 ${page.width} ${page.height}`);
|
|
1898
|
+
svg.setAttribute("role", options.static ? "img" : "group");
|
|
1899
|
+
svg.setAttribute("aria-label", page.name);
|
|
1900
|
+
const title = svgElement("title");
|
|
1901
|
+
title.textContent = page.name;
|
|
1902
|
+
svg.append(title);
|
|
1903
|
+
const defs = svgElement("defs");
|
|
1904
|
+
svg.append(defs);
|
|
1905
|
+
const root = svgElement("g");
|
|
1906
|
+
root.setAttribute("transform", `translate(0 ${page.height}) scale(1 -1)`);
|
|
1907
|
+
svg.append(root);
|
|
1908
|
+
const context = {
|
|
1909
|
+
defs,
|
|
1910
|
+
warnings: /* @__PURE__ */ new Set(),
|
|
1911
|
+
resources: new RenderResources(options.static ? defs : void 0),
|
|
1912
|
+
nodes: 0,
|
|
1913
|
+
textBudget: createTextLayoutBudget(),
|
|
1914
|
+
vectorBudget: new ForeignVectorBudget(),
|
|
1915
|
+
renderable: /* @__PURE__ */ new WeakMap(),
|
|
1916
|
+
visible,
|
|
1917
|
+
interactive: !options.static
|
|
1918
|
+
};
|
|
1919
|
+
try {
|
|
1920
|
+
for (const layer of pages) for (const shape of layer.shapes) drawShape(shape, root, context, layer.id);
|
|
1921
|
+
} catch (error) {
|
|
1922
|
+
context.resources.dispose();
|
|
1923
|
+
throw error;
|
|
1924
|
+
}
|
|
1925
|
+
if (!options.static) svg.querySelector("[data-shape-id]")?.setAttribute("tabindex", "0");
|
|
1926
|
+
return {
|
|
1927
|
+
svg,
|
|
1928
|
+
warnings: [...context.warnings],
|
|
1929
|
+
dispose: () => context.resources.dispose()
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
function drawShape(shape, parent, context, pageId) {
|
|
1933
|
+
if (!hasVisibleShapeContent(shape, context.visible, context.renderable)) return;
|
|
1934
|
+
if (++context.nodes > 5e4) {
|
|
1935
|
+
context.warnings.add("Some drawing content was omitted because it exceeds the safe rendering node limit.");
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
const group = svgElement("g");
|
|
1939
|
+
group.setAttribute("transform", matrix$1(shape.transform));
|
|
1940
|
+
if (context.interactive) {
|
|
1941
|
+
group.dataset.shapeId = shape.id;
|
|
1942
|
+
group.dataset.shapeName = shape.name;
|
|
1943
|
+
group.dataset.pageId = pageId;
|
|
1944
|
+
group.setAttribute("role", shape.kind === "group" ? "group" : "button");
|
|
1945
|
+
group.setAttribute("tabindex", "-1");
|
|
1946
|
+
group.setAttribute("aria-label", shape.text.plainText || shape.name || `Shape ${shape.id}`);
|
|
1947
|
+
}
|
|
1948
|
+
const title = svgElement("title");
|
|
1949
|
+
title.textContent = shape.text.plainText || shape.name;
|
|
1950
|
+
group.append(title);
|
|
1951
|
+
parent.append(group);
|
|
1952
|
+
const own = () => drawOwn(shape, group, context);
|
|
1953
|
+
const children = () => {
|
|
1954
|
+
for (const child of shape.children) drawShape(child, group, context, pageId);
|
|
1955
|
+
};
|
|
1956
|
+
if (shape.kind === "group" && shape.groupDisplayMode === 0) {
|
|
1957
|
+
children();
|
|
1958
|
+
return;
|
|
1959
|
+
}
|
|
1960
|
+
if (shape.kind === "group" && shape.groupDisplayMode === 2) {
|
|
1961
|
+
children();
|
|
1962
|
+
own();
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1965
|
+
own();
|
|
1966
|
+
children();
|
|
1967
|
+
}
|
|
1968
|
+
function drawOwn(shape, group, context) {
|
|
1969
|
+
const { warnings, defs, resources } = context;
|
|
1970
|
+
const fill = shape.geometry.some((geometry) => geometry.fill) ? fillPaint(shape.style, defs) : "none";
|
|
1971
|
+
for (const geometry of shape.geometry) {
|
|
1972
|
+
if (++context.nodes > 5e4) {
|
|
1973
|
+
warnings.add("Some drawing content was omitted because it exceeds the safe rendering node limit.");
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
const path = svgElement("path");
|
|
1977
|
+
if (context.interactive) path.dataset.geometry = "";
|
|
1978
|
+
path.setAttribute("d", geometry.path);
|
|
1979
|
+
path.setAttribute("fill", geometry.fill ? fill : "none");
|
|
1980
|
+
path.setAttribute("fill-opacity", String(shape.style.fillOpacity));
|
|
1981
|
+
const stroked = geometry.stroke && shape.style.linePattern !== 0;
|
|
1982
|
+
path.setAttribute("stroke", stroked ? safeColor(shape.style.lineColor) : "none");
|
|
1983
|
+
path.setAttribute("stroke-width", String(shape.style.lineWidth));
|
|
1984
|
+
path.setAttribute("stroke-opacity", String(shape.style.lineOpacity));
|
|
1985
|
+
path.setAttribute("stroke-linejoin", "round");
|
|
1986
|
+
path.setAttribute("stroke-linecap", shape.style.lineCap ?? "round");
|
|
1987
|
+
if (stroked && shape.style.linePattern > 1) {
|
|
1988
|
+
const dash = shape.style.lineDash;
|
|
1989
|
+
if (dash) {
|
|
1990
|
+
path.setAttribute("stroke-dasharray", dash.map((n) => n * shape.style.lineWidth).join(" "));
|
|
1991
|
+
if (shape.style.linePattern <= 23) warnings.add("Some dashed strokes use inferred Visio pattern spacing.");
|
|
1992
|
+
} else warnings.add("An unresolved line pattern is shown as a solid stroke.");
|
|
1993
|
+
}
|
|
1994
|
+
if (stroked) applyArrowheads(path, shape.style, defs, warnings);
|
|
1995
|
+
group.append(path);
|
|
1996
|
+
}
|
|
1997
|
+
const raster = renderImage(shape, resources);
|
|
1998
|
+
if (raster) group.append(raster);
|
|
1999
|
+
const vector = renderForeignVectorShape(shape, context.vectorBudget);
|
|
2000
|
+
if (vector) group.append(vector);
|
|
2001
|
+
if (shape.text.plainText) group.append(renderText(shape.text, warnings, context.textBudget));
|
|
2002
|
+
if (shape.kind === "foreign" && !shape.image && !shape.foreignVector) warnings.add("Embedded or foreign objects are not rendered in this build.");
|
|
2003
|
+
}
|
|
2004
|
+
//#endregion
|
|
2005
|
+
//#region src/shape-inspector.ts
|
|
2006
|
+
function selectedShape(document, selection, pageIndex) {
|
|
2007
|
+
if (!document || !selection) return void 0;
|
|
2008
|
+
const stack = [...(selection.pageId ? document.pages.find((page) => page.id === selection.pageId) : document.pages[pageIndex])?.shapes ?? []];
|
|
2009
|
+
while (stack.length) {
|
|
2010
|
+
const shape = stack.pop();
|
|
2011
|
+
if (shape.id === selection.id) return shape;
|
|
2012
|
+
for (const child of shape.children) stack.push(child);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
function safeExternalHref(value) {
|
|
2016
|
+
if (value.length > 4096 || /[\u0000-\u0020\u007f-\u009f\\]/u.test(value) || /%(?![0-9a-f]{2})/iu.test(value) || /%(?:0[0-9a-f]|1[0-9a-f]|7f|8[0-9a-f]|9[0-9a-f])/iu.test(value)) return void 0;
|
|
2017
|
+
try {
|
|
2018
|
+
const url = new URL(value);
|
|
2019
|
+
if (![
|
|
2020
|
+
"http:",
|
|
2021
|
+
"https:",
|
|
2022
|
+
"mailto:"
|
|
2023
|
+
].includes(url.protocol) || url.username || url.password) return void 0;
|
|
2024
|
+
if (url.protocol !== "mailto:" && (!/^https?:\/\/[^/]/iu.test(value) || /^https?:\/\/[^/?#]*@/iu.test(value))) return void 0;
|
|
2025
|
+
return url.href;
|
|
2026
|
+
} catch {
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
function shapeDetails(shape) {
|
|
2031
|
+
const fragment = document.createDocumentFragment();
|
|
2032
|
+
let characters = 1e5, rows = 200, truncated = false;
|
|
2033
|
+
const preview = (value, limit = 2048) => {
|
|
2034
|
+
const length = Math.max(0, Math.min(limit, characters));
|
|
2035
|
+
characters -= Math.min(length, value.length);
|
|
2036
|
+
if (value.length <= length) return value;
|
|
2037
|
+
truncated = true;
|
|
2038
|
+
return value.slice(0, length) + "…";
|
|
2039
|
+
};
|
|
2040
|
+
const title = document.createElement("p");
|
|
2041
|
+
title.textContent = preview(`${shape.name || "Shape"} · ID ${shape.id}`);
|
|
2042
|
+
fragment.append(title);
|
|
2043
|
+
const fields = (shape.shapeData ?? []).filter((field) => !field.invisible);
|
|
2044
|
+
if (fields.length) {
|
|
2045
|
+
const heading = document.createElement("h3");
|
|
2046
|
+
heading.textContent = "Shape data";
|
|
2047
|
+
fragment.append(heading);
|
|
2048
|
+
const list = document.createElement("dl");
|
|
2049
|
+
for (const field of fields) {
|
|
2050
|
+
if (--rows < 0 || characters <= 0) {
|
|
2051
|
+
truncated = true;
|
|
2052
|
+
break;
|
|
2053
|
+
}
|
|
2054
|
+
const label = document.createElement("dt"), value = document.createElement("dd");
|
|
2055
|
+
label.textContent = preview(field.label || field.name || field.id);
|
|
2056
|
+
value.textContent = preview(field.value === void 0 ? field.rawValue ?? "No cached value" : String(field.value)) + (field.valueKind === "date" ? " (serial days)" : field.valueKind === "duration" ? " (days)" : "");
|
|
2057
|
+
if (field.prompt) label.title = preview(field.prompt, 512);
|
|
2058
|
+
if (field.error) value.append(document.createTextNode(preview(` (saved error: ${field.error})`)));
|
|
2059
|
+
list.append(label, value);
|
|
2060
|
+
}
|
|
2061
|
+
fragment.append(list);
|
|
2062
|
+
} else {
|
|
2063
|
+
const empty = document.createElement("p");
|
|
2064
|
+
empty.textContent = "No visible shape data.";
|
|
2065
|
+
fragment.append(empty);
|
|
2066
|
+
}
|
|
2067
|
+
const links = (shape.hyperlinks ?? []).filter((link) => !link.invisible);
|
|
2068
|
+
if (links.length) {
|
|
2069
|
+
const heading = document.createElement("h3");
|
|
2070
|
+
heading.textContent = "Links";
|
|
2071
|
+
fragment.append(heading);
|
|
2072
|
+
const list = document.createElement("ul");
|
|
2073
|
+
for (const link of links) {
|
|
2074
|
+
if (--rows < 0 || characters <= 0) {
|
|
2075
|
+
truncated = true;
|
|
2076
|
+
break;
|
|
2077
|
+
}
|
|
2078
|
+
const item = document.createElement("li"), label = preview(link.description || link.name || link.address || link.subAddress || "Link");
|
|
2079
|
+
const href = link.target.kind === "external" ? safeExternalHref(link.target.href) : void 0;
|
|
2080
|
+
if (href) {
|
|
2081
|
+
const anchor = document.createElement("a");
|
|
2082
|
+
anchor.textContent = label;
|
|
2083
|
+
anchor.href = href;
|
|
2084
|
+
anchor.target = "_blank";
|
|
2085
|
+
anchor.rel = "noopener noreferrer";
|
|
2086
|
+
item.append(anchor);
|
|
2087
|
+
} else {
|
|
2088
|
+
item.textContent = link.target.kind === "internal" ? `${label} (internal target: ${preview(link.target.subAddress)})` : `${label} (unavailable)`;
|
|
2089
|
+
if (link.target.kind === "unresolved") item.title = preview(link.target.reason, 512);
|
|
2090
|
+
}
|
|
2091
|
+
list.append(item);
|
|
2092
|
+
}
|
|
2093
|
+
fragment.append(list);
|
|
2094
|
+
}
|
|
2095
|
+
if (truncated) {
|
|
2096
|
+
const note = document.createElement("p");
|
|
2097
|
+
note.textContent = "Some fields or values are shortened in this preview. Full cached metadata remains in the document model.";
|
|
2098
|
+
fragment.append(note);
|
|
2099
|
+
}
|
|
2100
|
+
return fragment;
|
|
2101
|
+
}
|
|
2102
|
+
//#endregion
|
|
2103
|
+
//#region src/diagnostics.ts
|
|
2104
|
+
/** Presentation-only grouping keeps repeated per-shape diagnostics readable without losing counts. */
|
|
2105
|
+
function compatibilityNotes(diagnostics, renderWarnings = []) {
|
|
2106
|
+
const notes = /* @__PURE__ */ new Map();
|
|
2107
|
+
for (const diagnostic of diagnostics) {
|
|
2108
|
+
const existing = notes.get(diagnostic.message);
|
|
2109
|
+
if (existing) {
|
|
2110
|
+
existing.count++;
|
|
2111
|
+
if (diagnostic.severity === "warning") existing.severity = "warning";
|
|
2112
|
+
} else notes.set(diagnostic.message, {
|
|
2113
|
+
message: diagnostic.message,
|
|
2114
|
+
count: 1,
|
|
2115
|
+
severity: diagnostic.severity
|
|
2116
|
+
});
|
|
2117
|
+
}
|
|
2118
|
+
for (const message of renderWarnings) {
|
|
2119
|
+
const existing = notes.get(message);
|
|
2120
|
+
if (existing) {
|
|
2121
|
+
existing.count++;
|
|
2122
|
+
existing.severity = "warning";
|
|
2123
|
+
} else notes.set(message, {
|
|
2124
|
+
message,
|
|
2125
|
+
count: 1,
|
|
2126
|
+
severity: "warning"
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
return [...notes.values()].sort((a, b) => Number(a.severity === "info") - Number(b.severity === "info"));
|
|
2130
|
+
}
|
|
2131
|
+
function compatibilityText(note) {
|
|
2132
|
+
return note.message + (note.count > 1 ? ` (${note.count} occurrences)` : "");
|
|
2133
|
+
}
|
|
2134
|
+
//#endregion
|
|
2135
|
+
//#region src/viewer-input.ts
|
|
2136
|
+
var targetShape = (event) => event.target?.closest?.("[data-shape-id]");
|
|
2137
|
+
function selection(target) {
|
|
2138
|
+
return target ? {
|
|
2139
|
+
id: target.dataset.shapeId,
|
|
2140
|
+
name: target.dataset.shapeName ?? "",
|
|
2141
|
+
...target.dataset.pageId ? { pageId: target.dataset.pageId } : {}
|
|
2142
|
+
} : null;
|
|
2143
|
+
}
|
|
2144
|
+
/** Own every DOM listener and abort them together when the surface is disposed. */
|
|
2145
|
+
function wireViewerInputs(controls, controller, fit) {
|
|
2146
|
+
const { viewport, commandRoot, pageSelect, searchInput } = controls;
|
|
2147
|
+
const events = new ((viewport.ownerDocument.defaultView?.AbortController) ?? AbortController)();
|
|
2148
|
+
const options = { signal: events.signal };
|
|
2149
|
+
searchInput.addEventListener("input", () => controller.setSearchQuery(searchInput.value), options);
|
|
2150
|
+
searchInput.addEventListener("keydown", (event) => {
|
|
2151
|
+
if (event.isComposing) return;
|
|
2152
|
+
if (event.key === "Enter") {
|
|
2153
|
+
event.preventDefault();
|
|
2154
|
+
if (event.shiftKey) controller.previousSearchResult();
|
|
2155
|
+
else controller.nextSearchResult();
|
|
2156
|
+
}
|
|
2157
|
+
if (event.key === "Escape") {
|
|
2158
|
+
event.preventDefault();
|
|
2159
|
+
controller.setSearchQuery("");
|
|
2160
|
+
}
|
|
2161
|
+
}, options);
|
|
2162
|
+
pageSelect.addEventListener("change", () => controller.setPage(Number(pageSelect.value)), options);
|
|
2163
|
+
commandRoot.addEventListener("click", (event) => {
|
|
2164
|
+
const action = event.target?.closest?.("button")?.dataset.action;
|
|
2165
|
+
if (action === "in") controller.setZoom(controller.state.zoom * 1.25);
|
|
2166
|
+
if (action === "out") controller.setZoom(controller.state.zoom / 1.25);
|
|
2167
|
+
if (action === "fit") fit();
|
|
2168
|
+
if (action === "actual") controller.setZoom(1);
|
|
2169
|
+
if (action === "search-next") controller.nextSearchResult();
|
|
2170
|
+
if (action === "search-previous") controller.previousSearchResult();
|
|
2171
|
+
}, options);
|
|
2172
|
+
viewport.addEventListener("click", (event) => controller.selectShape(selection(targetShape(event))), options);
|
|
2173
|
+
viewport.addEventListener("keydown", (event) => {
|
|
2174
|
+
const target = targetShape(event);
|
|
2175
|
+
if (target && [
|
|
2176
|
+
"ArrowDown",
|
|
2177
|
+
"ArrowRight",
|
|
2178
|
+
"ArrowUp",
|
|
2179
|
+
"ArrowLeft",
|
|
2180
|
+
"Home",
|
|
2181
|
+
"End"
|
|
2182
|
+
].includes(event.key)) {
|
|
2183
|
+
event.preventDefault();
|
|
2184
|
+
const shapes = Array.from(viewport.querySelectorAll("[data-shape-id]")), index = shapes.indexOf(target);
|
|
2185
|
+
const next = event.key === "Home" ? 0 : event.key === "End" ? shapes.length - 1 : (index + (["ArrowRight", "ArrowDown"].includes(event.key) ? 1 : -1) + shapes.length) % shapes.length;
|
|
2186
|
+
for (const shape of shapes) shape.setAttribute("tabindex", "-1");
|
|
2187
|
+
shapes[next]?.setAttribute("tabindex", "0");
|
|
2188
|
+
shapes[next]?.focus();
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
2191
|
+
if (target && (event.key === "Enter" || event.key === " ")) {
|
|
2192
|
+
event.preventDefault();
|
|
2193
|
+
controller.selectShape(selection(target));
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
if (event.key === "Escape") controller.selectShape(null);
|
|
2197
|
+
if (event.key === "+" || event.key === "=") {
|
|
2198
|
+
event.preventDefault();
|
|
2199
|
+
controller.setZoom(controller.state.zoom * 1.25);
|
|
2200
|
+
}
|
|
2201
|
+
if (event.key === "-") {
|
|
2202
|
+
event.preventDefault();
|
|
2203
|
+
controller.setZoom(controller.state.zoom / 1.25);
|
|
2204
|
+
}
|
|
2205
|
+
if (event.key === "0") {
|
|
2206
|
+
event.preventDefault();
|
|
2207
|
+
fit();
|
|
2208
|
+
}
|
|
2209
|
+
}, options);
|
|
2210
|
+
return () => events.abort();
|
|
2211
|
+
}
|
|
2212
|
+
//#endregion
|
|
2213
|
+
//#region src/viewer-layer-controls.ts
|
|
2214
|
+
var layerControlsTemplate = `<details class="layer-controls" hidden><summary>Layers</summary><p>Display changes only. SVG and print snapshots use saved visibility. Guides, NoShow and unknown hidden reasons remain hidden.</p><div data-layer-list></div><p data-layer-status role="status"></p><button type="button" data-layer-reset="all">Reset all layers</button></details>`;
|
|
2215
|
+
/** Static controls and text-only labels. Document strings never become selectors or markup. */
|
|
2216
|
+
function renderLayerControls(panel, state) {
|
|
2217
|
+
const model = state.document, page = model?.pages[state.pageIndex];
|
|
2218
|
+
const list = panel.querySelector("[data-layer-list]");
|
|
2219
|
+
const focused = panel.getRootNode().activeElement;
|
|
2220
|
+
const focus = focused && panel.contains(focused) ? { ...focused.dataset } : void 0;
|
|
2221
|
+
const fragment = panel.ownerDocument.createDocumentFragment();
|
|
2222
|
+
const overrides = layerOverrideMaps(model, state.layerVisibilityOverrides);
|
|
2223
|
+
let total = 0, shown = 0;
|
|
2224
|
+
for (const source of model && page ? getVisioPageLayers(model, page.id) : []) {
|
|
2225
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2226
|
+
const fieldset = panel.ownerDocument.createElement("fieldset");
|
|
2227
|
+
const legend = panel.ownerDocument.createElement("legend");
|
|
2228
|
+
const pageLabel = `${source.name.slice(0, 256)} (page ${source.id})`;
|
|
2229
|
+
legend.textContent = `${pageLabel}${source.id === page.id ? "" : " (background)"}`;
|
|
2230
|
+
fieldset.append(legend);
|
|
2231
|
+
let rows = 0;
|
|
2232
|
+
for (const layer of source.layers ?? []) {
|
|
2233
|
+
if (ids.has(layer.id)) continue;
|
|
2234
|
+
ids.add(layer.id);
|
|
2235
|
+
++total;
|
|
2236
|
+
if (shown >= VIEWER_LAYER_LIMITS.controls) continue;
|
|
2237
|
+
++shown;
|
|
2238
|
+
++rows;
|
|
2239
|
+
const row = panel.ownerDocument.createElement("div");
|
|
2240
|
+
const label = panel.ownerDocument.createElement("label");
|
|
2241
|
+
const input = panel.ownerDocument.createElement("input");
|
|
2242
|
+
const override = overrides.get(source.id)?.get(layer.id);
|
|
2243
|
+
input.type = "checkbox";
|
|
2244
|
+
input.dataset.layerId = layer.id;
|
|
2245
|
+
input.dataset.pageId = source.id;
|
|
2246
|
+
input.checked = override ?? layer.visible;
|
|
2247
|
+
const name = `${layer.name.slice(0, 256) || "Layer"} (ID ${layer.id})`;
|
|
2248
|
+
input.setAttribute("aria-label", `${pageLabel}: ${name}`);
|
|
2249
|
+
const description = `${name} · saved ${layer.visible ? "visible" : "hidden"}${override === void 0 ? "" : ` · override ${override ? "visible" : "hidden"}`}`;
|
|
2250
|
+
label.append(input, description);
|
|
2251
|
+
row.append(label);
|
|
2252
|
+
if (override !== void 0) {
|
|
2253
|
+
const reset = panel.ownerDocument.createElement("button");
|
|
2254
|
+
reset.type = "button";
|
|
2255
|
+
reset.textContent = "Reset";
|
|
2256
|
+
reset.dataset.layerReset = "one";
|
|
2257
|
+
reset.dataset.layerId = layer.id;
|
|
2258
|
+
reset.dataset.pageId = source.id;
|
|
2259
|
+
reset.setAttribute("aria-label", `Reset ${pageLabel}: ${name}`);
|
|
2260
|
+
row.append(reset);
|
|
2261
|
+
}
|
|
2262
|
+
fieldset.append(row);
|
|
2263
|
+
}
|
|
2264
|
+
if (rows) fragment.append(fieldset);
|
|
2265
|
+
}
|
|
2266
|
+
list.replaceChildren(fragment);
|
|
2267
|
+
panel.hidden = total === 0 && state.layerVisibilityOverrides.length === 0;
|
|
2268
|
+
panel.querySelector("[data-layer-status]").textContent = total > shown ? `Showing ${shown} of ${total} layers. Other layers are available through the viewer API.` : `${total} layers in this drawing · ${state.layerVisibilityOverrides.length} document display overrides`;
|
|
2269
|
+
panel.querySelector("[data-layer-reset=\"all\"]").disabled = state.layerVisibilityOverrides.length === 0;
|
|
2270
|
+
if (focus) {
|
|
2271
|
+
const controls = [...panel.querySelectorAll("input,button")];
|
|
2272
|
+
const match = (control) => control.dataset.layerId === focus.layerId && control.dataset.pageId === focus.pageId;
|
|
2273
|
+
(controls.find((control) => match(control) && control.dataset.layerReset === focus.layerReset) ?? controls.find(match))?.focus();
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
function wireLayerControls(panel, controller) {
|
|
2277
|
+
const events = new ((panel.ownerDocument.defaultView?.AbortController) ?? AbortController)(), options = { signal: events.signal };
|
|
2278
|
+
panel.addEventListener("change", (event) => {
|
|
2279
|
+
const input = event.target;
|
|
2280
|
+
if (input.matches("input[type=\"checkbox\"]") && input.dataset.pageId !== void 0 && input.dataset.layerId !== void 0) controller.setLayerVisibility(input.dataset.pageId, input.dataset.layerId, input.checked);
|
|
2281
|
+
}, options);
|
|
2282
|
+
panel.addEventListener("click", (event) => {
|
|
2283
|
+
const button = event.target.closest("button[data-layer-reset]");
|
|
2284
|
+
if (!button || !panel.contains(button)) return;
|
|
2285
|
+
if (button.dataset.layerReset === "all") controller.resetLayerVisibility();
|
|
2286
|
+
else if (button.dataset.pageId !== void 0 && button.dataset.layerId !== void 0) controller.setLayerVisibility(button.dataset.pageId, button.dataset.layerId, null);
|
|
2287
|
+
}, options);
|
|
2288
|
+
return () => events.abort();
|
|
2289
|
+
}
|
|
2290
|
+
//#endregion
|
|
2291
|
+
//#region src/styles.ts
|
|
2292
|
+
/** Inherited public tokens use local fallback aliases so host themes are never shadowed. */
|
|
2293
|
+
var viewerStyles = `
|
|
2294
|
+
:host {
|
|
2295
|
+
--_vv-bg:var(--vv-background,#eceae8); --_vv-surface:var(--vv-surface,#fff);
|
|
2296
|
+
--_vv-secondary:var(--vv-secondary,#f3f2f1); --_vv-ink:var(--vv-ink,#1f1f1f);
|
|
2297
|
+
--_vv-muted:var(--vv-muted,#605e5c); --_vv-border:var(--vv-border,#e1dfdd);
|
|
2298
|
+
--_vv-accent:var(--vv-accent,#3955a3); --_vv-accent-soft:var(--vv-accent-soft,rgba(57,85,163,.10));
|
|
2299
|
+
display:flex; flex-direction:column; min-width:0; min-height:320px; height:100%; overflow:hidden;
|
|
2300
|
+
color:var(--_vv-ink); background:var(--_vv-bg); font:12px/1.5 "Segoe UI",Arial,sans-serif; contain:layout;
|
|
2301
|
+
}
|
|
2302
|
+
* { box-sizing:border-box; }
|
|
2303
|
+
[hidden] { display:none !important; }
|
|
2304
|
+
button,select,input,textarea { font:inherit; color:inherit; border:1px solid var(--_vv-border); background:var(--_vv-surface); border-radius:4px; min-height:28px; padding:3px 8px; }
|
|
2305
|
+
button { cursor:pointer; }
|
|
2306
|
+
button:hover:enabled { background:var(--_vv-accent-soft); border-color:var(--_vv-accent); }
|
|
2307
|
+
button:disabled { opacity:.4; cursor:default; }
|
|
2308
|
+
button:focus-visible,select:focus-visible,input:focus-visible,textarea:focus-visible,summary:focus-visible,[tabindex]:focus-visible { outline:2px solid var(--vv-focus,var(--_vv-accent)); outline-offset:2px; }
|
|
2309
|
+
button[aria-pressed="true"] { color:var(--_vv-accent); background:var(--_vv-accent-soft); }
|
|
2310
|
+
.toolbar { flex:none; min-width:0; border-bottom:1px solid var(--_vv-border); background:var(--_vv-surface); }
|
|
2311
|
+
.ribbon-primary { height:32px; display:flex; align-items:center; gap:6px; padding:2px 8px; }
|
|
2312
|
+
.page-picker { display:flex; min-width:0; align-items:center; gap:8px; color:var(--_vv-muted); font-size:11px; }
|
|
2313
|
+
.page-picker select { width:190px; max-width:42vw; height:26px; min-height:26px; padding:2px 6px; font-size:11px; color:var(--_vv-ink); }
|
|
2314
|
+
.page-stepper { display:flex; align-items:center; gap:2px; }
|
|
2315
|
+
.page-stepper button { width:26px; height:26px; min-height:26px; padding:0; font-size:18px; line-height:22px; background:transparent; border-color:transparent; }
|
|
2316
|
+
.spacer { flex:1; }
|
|
2317
|
+
.local-label { color:var(--_vv-muted); font-size:10px; white-space:nowrap; }
|
|
2318
|
+
.ribbon-tabs { height:35px; display:flex; align-items:stretch; padding:0 4px; border-bottom:1px solid var(--_vv-border); }
|
|
2319
|
+
.ribbon-tabs button { position:relative; min-height:34px; padding:7px 14px; border:0; border-radius:0; background:transparent; color:var(--_vv-muted); font-size:12px; font-weight:500; }
|
|
2320
|
+
.ribbon-tabs button[aria-selected="true"] { color:var(--_vv-accent); }
|
|
2321
|
+
.ribbon-tabs button[aria-selected="true"]::after { content:""; position:absolute; inset:auto 0 -1px; height:2.5px; background:var(--_vv-accent); }
|
|
2322
|
+
.ribbon-content { min-height:82px; display:flex; align-items:stretch; gap:0; padding:2px 4px; overflow-x:auto; }
|
|
2323
|
+
.ribbon-group { display:flex; flex-direction:column; justify-content:space-between; flex:none; gap:2px; padding:3px 10px 4px; border-right:1px solid var(--_vv-border); }
|
|
2324
|
+
.group-label { font-size:10px; line-height:12px; color:var(--_vv-muted); text-align:center; }
|
|
2325
|
+
.ribbon-actions { display:flex; align-items:stretch; gap:2px; flex:1; }
|
|
2326
|
+
.ribbon-command { display:flex; flex-direction:column; justify-content:center; align-items:center; gap:2px; min-width:70px; padding:2px 6px; border-color:transparent; background:transparent; font-size:11px; white-space:nowrap; }
|
|
2327
|
+
.command-icon { display:grid; place-items:center; width:26px; height:26px; color:var(--_vv-accent); }
|
|
2328
|
+
.command-icon::before { content:""; width:24px; height:24px; background:currentColor; mask:var(--command-symbol) center / contain no-repeat; }
|
|
2329
|
+
[data-icon="selection"] { --command-symbol:url("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22m5%203%2013%209-6%201-3%206z%22%2F%3E%3C%2Fsvg%3E"); }
|
|
2330
|
+
[data-icon="edit"] { --command-symbol:url("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M4%205h16M12%205v14M8%2019h8%22%2F%3E%3C%2Fsvg%3E"); }
|
|
2331
|
+
[data-icon="layers"] { --command-symbol:url("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22m3%208%209-5%209%205-9%205zM3%2012l9%205%209-5M3%2016l9%205%209-5%22%2F%3E%3C%2Fsvg%3E"); }
|
|
2332
|
+
[data-icon="pages"] { --command-symbol:url("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Crect%20x%3D%225%22%20y%3D%223%22%20width%3D%2214%22%20height%3D%2218%22%20rx%3D%221%22%2F%3E%3Cpath%20d%3D%22M8%208h8M8%2012h8M8%2016h5%22%2F%3E%3C%2Fsvg%3E"); }
|
|
2333
|
+
[data-icon="inspector"] { --command-symbol:url("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Crect%20x%3D%223%22%20y%3D%224%22%20width%3D%2218%22%20height%3D%2216%22%20rx%3D%221%22%2F%3E%3Cpath%20d%3D%22M14%204v16M17%208h1M17%2012h1M17%2016h1%22%2F%3E%3C%2Fsvg%3E"); }
|
|
2334
|
+
[data-icon="notes"] { --command-symbol:url("data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%221.6%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M4%204h16v12H9l-5%204z%22%2F%3E%3Cpath%20d%3D%22M8%208h8M8%2012h5%22%2F%3E%3C%2Fsvg%3E"); }
|
|
2335
|
+
.ribbon-command[data-chrome="edit"] .command-icon { font-family:ui-serif,Georgia,serif; text-decoration:underline; text-decoration-thickness:1px; text-underline-offset:3px; }
|
|
2336
|
+
.ribbon-hint { align-self:center; flex:none; margin:0; padding:0 16px; font-size:11px; line-height:18px; color:var(--_vv-muted); }
|
|
2337
|
+
.search-group { width:330px; }
|
|
2338
|
+
.search-controls { display:flex; align-items:center; gap:4px; flex-wrap:wrap; }
|
|
2339
|
+
.search-controls label { display:flex; align-items:center; min-width:0; gap:6px; flex:1; font-size:11px; }
|
|
2340
|
+
.search-controls input { width:100%; min-width:0; padding:3px 6px; font-size:11px; }
|
|
2341
|
+
.search-controls button { padding:3px 6px; font-size:10px; background:var(--_vv-secondary); }
|
|
2342
|
+
.search-controls [role="status"] { display:block; width:100%; min-height:15px; font-size:10px; line-height:15px; color:var(--_vv-muted); overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
|
|
2343
|
+
.workspace { position:relative; display:flex; flex:1; min-width:0; min-height:0; overflow:hidden; }
|
|
2344
|
+
.page-rail { flex:0 0 180px; width:180px; min-width:0; overflow:auto; border-right:1px solid var(--_vv-border); background:var(--_vv-secondary); }
|
|
2345
|
+
.pane-heading { height:37px; display:flex; align-items:center; justify-content:space-between; gap:8px; padding:8px 10px; border-bottom:1px solid var(--_vv-border); font-size:11px; font-weight:600; }
|
|
2346
|
+
.pane-heading [data-page-count],.inspector-kind { color:var(--_vv-muted); font-size:10px; font-weight:400; }
|
|
2347
|
+
.page-list { list-style:none; padding:8px 5px; margin:0; display:flex; flex-direction:column; gap:4px; }
|
|
2348
|
+
.page-link { display:flex; align-items:center; position:relative; width:100%; min-width:0; gap:4px; padding:4px 5px 4px 2px; border:0; border-radius:0; background:transparent; color:var(--_vv-muted); text-align:left; }
|
|
2349
|
+
.page-link[aria-current="page"] { background:var(--_vv-accent-soft); }
|
|
2350
|
+
.page-link[aria-current="page"]::before { content:""; position:absolute; left:0; top:4px; bottom:4px; width:3px; border-radius:0 2px 2px 0; background:var(--_vv-accent); }
|
|
2351
|
+
.page-number { flex:0 0 20px; width:20px; padding-right:3px; font-size:10px; text-align:right; }
|
|
2352
|
+
.page-card { display:flex; flex-direction:column; align-items:flex-start; min-width:0; width:100%; min-height:98px; gap:3px; padding:10px; border:1px solid var(--_vv-border); border-radius:2px; background:var(--_vv-surface); }
|
|
2353
|
+
.page-link[aria-current="page"] .page-card { border-color:var(--_vv-accent); }
|
|
2354
|
+
.page-link[aria-current="page"] .page-number { color:var(--_vv-accent); font-weight:600; }
|
|
2355
|
+
.page-mark { color:var(--_vv-muted); font-size:20px; line-height:23px; }
|
|
2356
|
+
.page-name { color:var(--_vv-ink); font-size:11px; font-weight:500; line-height:16px; overflow-wrap:anywhere; }
|
|
2357
|
+
.page-meta { color:var(--_vv-muted); font-size:9px; line-height:14px; }
|
|
2358
|
+
.page-empty { margin:12px; color:var(--_vv-muted); font-size:11px; }
|
|
2359
|
+
.viewport { position:relative; display:flex; flex-direction:column; flex:1; overflow:auto; min-width:0; min-height:0; padding:16px 4px; background:var(--_vv-bg); }
|
|
2360
|
+
.paper { display:block; flex:none; background:white; box-shadow:var(--vv-shadow,0 2px 8px rgb(0 0 0 / 14%)); margin:auto; }
|
|
2361
|
+
.paper [data-shape-id] { cursor:pointer; }
|
|
2362
|
+
.paper [data-selected="true"] > [data-geometry] { stroke:var(--_vv-accent) !important; stroke-width:.025 !important; }
|
|
2363
|
+
.inspector-pane { flex:0 0 288px; width:288px; min-width:0; overflow:auto; border-left:1px solid var(--_vv-border); background:var(--_vv-secondary); }
|
|
2364
|
+
.inspector-body { display:flex; flex-direction:column; gap:12px; padding:12px 10px; }
|
|
2365
|
+
.inspector-card,.edit-controls,.layer-controls { min-width:0; padding:8px; margin:0; border:1px solid var(--_vv-border); border-radius:4px; background:var(--_vv-surface); font-size:11px; line-height:17px; }
|
|
2366
|
+
.document-card h2 { margin:0 0 8px; font-size:10px; font-weight:500; letter-spacing:.5px; text-transform:uppercase; color:var(--_vv-muted); }
|
|
2367
|
+
.current-page-name { margin:0 0 8px; font-size:12px; font-weight:500; overflow-wrap:anywhere; }
|
|
2368
|
+
.document-card dl { display:grid; grid-template-columns:1fr auto; gap:4px 10px; margin:0; font-size:11px; }
|
|
2369
|
+
.document-card dt { color:var(--_vv-muted); }
|
|
2370
|
+
.document-card dd { margin:0; text-align:right; overflow-wrap:anywhere; }
|
|
2371
|
+
.selection-hint { margin:0; padding:0 2px; color:var(--_vv-muted); font-size:11px; }
|
|
2372
|
+
summary { cursor:pointer; min-height:22px; line-height:22px; font-weight:500; }
|
|
2373
|
+
summary::marker { color:var(--_vv-muted); font-size:10px; }
|
|
2374
|
+
.shape-inspector div { max-height:260px; overflow:auto; }
|
|
2375
|
+
.shape-inspector p { margin:6px 0; overflow-wrap:anywhere; }
|
|
2376
|
+
.shape-inspector dl { display:grid; grid-template-columns:minmax(60px,1fr) 1.5fr; gap:6px 10px; }
|
|
2377
|
+
.shape-inspector dt { font-weight:500; }
|
|
2378
|
+
.shape-inspector dd { margin:0; overflow-wrap:anywhere; white-space:pre-wrap; }
|
|
2379
|
+
.shape-inspector h3 { margin:12px 0 5px; font-size:11px; }
|
|
2380
|
+
.shape-inspector a { color:var(--_vv-accent); overflow-wrap:anywhere; }
|
|
2381
|
+
.edit-controls p,.layer-controls p { margin:6px 0; color:var(--_vv-muted); overflow-wrap:anywhere; }
|
|
2382
|
+
.edit-controls textarea { display:block; width:100%; resize:vertical; max-height:180px; margin-top:4px; background:var(--_vv-secondary); }
|
|
2383
|
+
.edit-controls label { display:block; margin-top:8px; }
|
|
2384
|
+
.edit-actions { display:flex; flex-wrap:wrap; gap:4px; margin-top:8px; }
|
|
2385
|
+
.edit-actions button { font-size:11px; padding:3px 7px; }
|
|
2386
|
+
.edit-actions [data-edit="apply"]:enabled { background:var(--_vv-accent); border-color:var(--_vv-accent); color:var(--vv-accent-ink,#fff); }
|
|
2387
|
+
.edit-controls [data-geometry] { min-width:0; margin:8px 0 0; padding:6px; border:1px solid var(--_vv-border); }
|
|
2388
|
+
.edit-controls [data-geometry-field] { display:block; width:100%; min-width:0; margin-top:4px; }
|
|
2389
|
+
.edit-controls [data-geometry-error] { color:var(--vv-danger,#b42318); }
|
|
2390
|
+
.edit-controls [data-edit-error] { color:var(--vv-danger,#b42318); }
|
|
2391
|
+
.edit-controls [data-edit-diagnostics] { max-height:100px; overflow:auto; padding-left:16px; }
|
|
2392
|
+
.layer-controls [data-layer-list] { max-height:240px; overflow:auto; }
|
|
2393
|
+
.layer-controls fieldset { border:1px solid var(--_vv-border); margin:6px 0; padding:6px; min-width:0; }
|
|
2394
|
+
.layer-controls legend { max-width:100%; overflow-wrap:anywhere; }
|
|
2395
|
+
.layer-controls fieldset div { display:flex; align-items:center; gap:6px; margin:3px 0; }
|
|
2396
|
+
.layer-controls label { display:flex; align-items:center; gap:6px; overflow-wrap:anywhere; }
|
|
2397
|
+
.layer-controls input { flex-shrink:0; width:16px; height:16px; min-height:16px; padding:0; accent-color:var(--_vv-accent); }
|
|
2398
|
+
.notes ul { max-height:220px; overflow:auto; padding-left:17px; margin:6px 0 0; color:var(--_vv-muted); }
|
|
2399
|
+
.notes li { margin:5px 0; overflow-wrap:anywhere; }
|
|
2400
|
+
.notes-strip { flex:none; display:flex; align-items:center; justify-content:space-between; min-height:25.5px; padding:0 12px; border-top:1px solid var(--_vv-border); background:var(--_vv-bg); color:var(--_vv-muted); font-size:10px; }
|
|
2401
|
+
.notes-strip button { min-height:24.5px; padding:2px 0; border:0; background:transparent; color:var(--_vv-muted); font-size:11px; }
|
|
2402
|
+
.notes-strip button::before { content:"▸"; margin-right:6px; }
|
|
2403
|
+
.notes-strip button[aria-expanded="true"]::before { content:"▾"; }
|
|
2404
|
+
.status { flex:none; display:flex; align-items:center; justify-content:space-between; gap:12px; min-width:0; min-height:29px; padding:0 8px 0 12px; border-top:1px solid var(--_vv-border); color:var(--_vv-muted); background:var(--_vv-surface); font-size:10px; }
|
|
2405
|
+
.status-message { display:flex; gap:16px; min-width:0; }
|
|
2406
|
+
.status-message span { overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
|
|
2407
|
+
.zoom-controls { display:flex; flex:none; align-items:center; gap:3px; }
|
|
2408
|
+
.zoom-controls button { min-height:24px; height:24px; padding:1px 6px; border-color:transparent; background:transparent; font-size:10px; }
|
|
2409
|
+
.zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:24px; padding:0; font-size:17px; line-height:20px; }
|
|
2410
|
+
.zoom-divider { width:1px; height:14px; background:var(--_vv-border); margin:0 3px; }
|
|
2411
|
+
.zoom { min-width:40px; font-size:10px; text-align:center; font-variant-numeric:tabular-nums; }
|
|
2412
|
+
.empty { max-width:360px; margin:60px auto; padding:16px; text-align:center; color:var(--_vv-muted); font-size:12px; }
|
|
2413
|
+
.empty strong { display:block; color:var(--_vv-ink); font-size:18px; font-weight:500; margin-bottom:8px; }
|
|
2414
|
+
@media(max-width:1100px) { .ribbon-hint { display:none; } [data-diagnostics] { display:none; } }
|
|
2415
|
+
@media(max-width:760px) {
|
|
2416
|
+
button,select,input,textarea { min-height:44px; }
|
|
2417
|
+
button { min-width:44px; }
|
|
2418
|
+
summary { min-height:44px; line-height:44px; }
|
|
2419
|
+
.ribbon-primary { height:52px; padding:4px 8px; }
|
|
2420
|
+
.page-picker { flex:1; }
|
|
2421
|
+
.page-picker select { width:100%; max-width:none; min-height:44px; height:44px; font-size:12px; }
|
|
2422
|
+
.page-stepper button { width:44px; min-height:44px; height:44px; }
|
|
2423
|
+
.local-label,.ribbon-primary>.spacer { display:none; }
|
|
2424
|
+
.ribbon-tabs { height:44px; }
|
|
2425
|
+
.ribbon-tabs button { min-height:44px; padding:8px 16px; }
|
|
2426
|
+
.ribbon-content { min-height:86px; }
|
|
2427
|
+
.ribbon-group { padding:4px 8px; }
|
|
2428
|
+
.search-group { width:100%; border-right:0; }
|
|
2429
|
+
.search-controls { gap:4px; }
|
|
2430
|
+
.search-controls label { font-size:12px; }
|
|
2431
|
+
.search-controls input { font-size:12px; }
|
|
2432
|
+
.search-controls button { min-width:44px; font-size:0; padding:3px; }
|
|
2433
|
+
.search-controls [data-action="search-previous"]::before { content:"‹"; font-size:22px; }
|
|
2434
|
+
.search-controls [data-action="search-next"]::before { content:"›"; font-size:22px; }
|
|
2435
|
+
#home-panel>.ribbon-group:not(.search-group) { display:none; }
|
|
2436
|
+
.ribbon-command { min-width:90px; }
|
|
2437
|
+
.workspace { display:block; overflow:auto; }
|
|
2438
|
+
.page-rail { width:100%; max-height:190px; border-right:0; border-bottom:1px solid var(--_vv-border); }
|
|
2439
|
+
.page-list { flex-direction:row; overflow-x:auto; }
|
|
2440
|
+
.page-list li { flex:0 0 155px; }
|
|
2441
|
+
.page-card { min-height:86px; }
|
|
2442
|
+
.viewport { height:340px; min-height:260px; padding:16px 4px; }
|
|
2443
|
+
.inspector-pane { width:100%; overflow:visible; border-left:0; border-top:1px solid var(--_vv-border); }
|
|
2444
|
+
.inspector-body { gap:8px; padding:10px; }
|
|
2445
|
+
.inspector-card,.edit-controls,.layer-controls { font-size:12px; }
|
|
2446
|
+
.layer-controls label { min-height:44px; }
|
|
2447
|
+
.layer-controls input { min-width:20px; width:20px; height:20px; min-height:20px; }
|
|
2448
|
+
.notes-strip { min-height:44px; padding:0 10px; }
|
|
2449
|
+
.notes-strip button { min-height:44px; }
|
|
2450
|
+
.status { flex-wrap:wrap; gap:0; padding:4px 8px; }
|
|
2451
|
+
.status-message { width:100%; min-height:18px; }
|
|
2452
|
+
.zoom-controls { justify-content:flex-end; width:100%; }
|
|
2453
|
+
.zoom-controls button { min-height:44px; height:44px; min-width:44px; font-size:11px; }
|
|
2454
|
+
.zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:44px; font-size:20px; }
|
|
2455
|
+
.zoom { font-size:11px; }
|
|
2456
|
+
}
|
|
2457
|
+
@media(pointer:coarse) {
|
|
2458
|
+
button,select,input,textarea { min-height:44px; }
|
|
2459
|
+
button { min-width:44px; }
|
|
2460
|
+
summary,.layer-controls label { min-height:44px; line-height:44px; }
|
|
2461
|
+
.ribbon-primary { min-height:52px; height:52px; }
|
|
2462
|
+
.page-picker select,.page-stepper button { min-height:44px; height:44px; }
|
|
2463
|
+
.page-stepper button { min-width:44px; }
|
|
2464
|
+
.ribbon-tabs { min-height:44px; height:44px; }
|
|
2465
|
+
.ribbon-tabs button { min-height:44px; }
|
|
2466
|
+
.ribbon-content { min-height:96px; }
|
|
2467
|
+
.search-group { width:380px; max-width:100%; }
|
|
2468
|
+
.zoom-controls button,.notes-strip button { min-height:44px; height:44px; }
|
|
2469
|
+
.zoom-controls [data-action="in"],.zoom-controls [data-action="out"] { min-width:44px; }
|
|
2470
|
+
.layer-controls input { min-height:20px; }
|
|
2471
|
+
}
|
|
2472
|
+
@media(prefers-reduced-motion:reduce) { * { scroll-behavior:auto !important; } }
|
|
2473
|
+
`;
|
|
2474
|
+
//#endregion
|
|
2475
|
+
//#region src/edit-error.ts
|
|
2476
|
+
/** Core error codes remain visible for a refused transaction; document content is plain text. */
|
|
2477
|
+
function editErrorMessage(error) {
|
|
2478
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2479
|
+
const code = error && typeof error === "object" && "code" in error && typeof error.code === "string" ? error.code.slice(0, 256) : void 0;
|
|
2480
|
+
return code ? `${code}: ${message}` : message;
|
|
2481
|
+
}
|
|
2482
|
+
/** Superseded worker/load operations are cancellation, not a refused document edit. */
|
|
2483
|
+
function isEditCancellation(error) {
|
|
2484
|
+
return !!error && typeof error === "object" && "name" in error && error.name === "AbortError";
|
|
2485
|
+
}
|
|
2486
|
+
//#endregion
|
|
2487
|
+
//#region src/viewer-geometry-controls.ts
|
|
2488
|
+
var geometryControlsTemplate = `<fieldset data-geometry><legend>Geometry (experimental)</legend><p>Pin X/Y use drawing inches, bottom-left origin, Y up. Resize holds the rotation pin fixed. Core rejects unsupported formulas, protected cells, unsafe group/master dependencies and referenced deletion. Native Visio reopen fidelity is unverified.</p><label>New rectangle ID <input data-geometry-field="id" type="text" maxlength="256"></label><label>Rectangle text (optional) <input data-geometry-field="text" type="text" maxlength="1000000"></label><label>Pin X (inches) <input data-geometry-field="x" type="number" step="any"></label><label>Pin Y (inches) <input data-geometry-field="y" type="number" step="any"></label><label>Width (inches) <input data-geometry-field="width" type="number" min="0" step="any"></label><label>Height (inches) <input data-geometry-field="height" type="number" min="0" step="any"></label><div class="edit-actions"><button type="button" data-geometry-action="create-rectangle">Create rectangle</button><button type="button" data-geometry-action="move-shape">Move selected</button><button type="button" data-geometry-action="resize-shape">Resize selected</button><button type="button" data-geometry-action="delete-shape">Delete selected</button></div><p data-geometry-error role="alert" hidden></p></fieldset>`;
|
|
2489
|
+
/** Collect explicit coordinates; never infer a Visio pin from rendered SVG transforms. */
|
|
2490
|
+
var ViewerGeometryControls = class {
|
|
2491
|
+
panel;
|
|
2492
|
+
controller;
|
|
2493
|
+
draftChanged;
|
|
2494
|
+
#fields;
|
|
2495
|
+
#buttons;
|
|
2496
|
+
#error;
|
|
2497
|
+
#identity = "";
|
|
2498
|
+
#request = 0;
|
|
2499
|
+
constructor(panel, controller, draftChanged = () => {}) {
|
|
2500
|
+
this.panel = panel;
|
|
2501
|
+
this.controller = controller;
|
|
2502
|
+
this.draftChanged = draftChanged;
|
|
2503
|
+
const field = (name) => panel.querySelector(`[data-geometry-field="${name}"]`);
|
|
2504
|
+
this.#fields = {
|
|
2505
|
+
id: field("id"),
|
|
2506
|
+
text: field("text"),
|
|
2507
|
+
x: field("x"),
|
|
2508
|
+
y: field("y"),
|
|
2509
|
+
width: field("width"),
|
|
2510
|
+
height: field("height")
|
|
2511
|
+
};
|
|
2512
|
+
this.#buttons = [...panel.querySelectorAll("[data-geometry-action]")];
|
|
2513
|
+
this.#error = panel.querySelector("[data-geometry-error]");
|
|
2514
|
+
}
|
|
2515
|
+
wire() {
|
|
2516
|
+
const events = new ((this.panel.ownerDocument.defaultView?.AbortController) ?? AbortController)();
|
|
2517
|
+
for (const field of Object.values(this.#fields)) field.addEventListener("input", () => {
|
|
2518
|
+
this.render(this.controller.state);
|
|
2519
|
+
this.draftChanged();
|
|
2520
|
+
}, { signal: events.signal });
|
|
2521
|
+
for (const button of this.#buttons) button.addEventListener("click", () => {
|
|
2522
|
+
this.#run(button);
|
|
2523
|
+
}, { signal: events.signal });
|
|
2524
|
+
return () => {
|
|
2525
|
+
++this.#request;
|
|
2526
|
+
events.abort();
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
async #run(button) {
|
|
2530
|
+
if (button.disabled) return;
|
|
2531
|
+
const state = this.controller.state;
|
|
2532
|
+
const pageId = state.selectedShape?.pageId ?? state.document?.pages[state.pageIndex]?.id;
|
|
2533
|
+
if (pageId === void 0) return;
|
|
2534
|
+
const type = button.dataset.geometryAction;
|
|
2535
|
+
const shapeId = type === "create-rectangle" ? this.#fields.id.value : state.selectedShape?.id;
|
|
2536
|
+
if (!shapeId) return;
|
|
2537
|
+
const x = this.#fields.x.valueAsNumber, y = this.#fields.y.valueAsNumber;
|
|
2538
|
+
const width = this.#fields.width.valueAsNumber, height = this.#fields.height.valueAsNumber;
|
|
2539
|
+
const target = {
|
|
2540
|
+
pageId,
|
|
2541
|
+
shapeId
|
|
2542
|
+
};
|
|
2543
|
+
const command = type === "create-rectangle" ? {
|
|
2544
|
+
type,
|
|
2545
|
+
...target,
|
|
2546
|
+
x,
|
|
2547
|
+
y,
|
|
2548
|
+
width,
|
|
2549
|
+
height,
|
|
2550
|
+
...this.#fields.text.value ? { text: this.#fields.text.value } : {}
|
|
2551
|
+
} : type === "move-shape" ? {
|
|
2552
|
+
type,
|
|
2553
|
+
...target,
|
|
2554
|
+
x,
|
|
2555
|
+
y
|
|
2556
|
+
} : type === "resize-shape" ? {
|
|
2557
|
+
type,
|
|
2558
|
+
...target,
|
|
2559
|
+
width,
|
|
2560
|
+
height
|
|
2561
|
+
} : {
|
|
2562
|
+
type: "delete-shape",
|
|
2563
|
+
...target
|
|
2564
|
+
};
|
|
2565
|
+
const request = ++this.#request;
|
|
2566
|
+
this.#error.hidden = true;
|
|
2567
|
+
try {
|
|
2568
|
+
await this.controller.applyEdits([command]);
|
|
2569
|
+
if (request === this.#request) {
|
|
2570
|
+
this.reset();
|
|
2571
|
+
this.render(this.controller.state);
|
|
2572
|
+
this.draftChanged();
|
|
2573
|
+
}
|
|
2574
|
+
} catch (error) {
|
|
2575
|
+
if (request === this.#request && !isEditCancellation(error)) {
|
|
2576
|
+
this.#error.textContent = editErrorMessage(error);
|
|
2577
|
+
this.#error.hidden = false;
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
get hasDraft() {
|
|
2582
|
+
return Object.values(this.#fields).some((field) => field.value !== "");
|
|
2583
|
+
}
|
|
2584
|
+
reset() {
|
|
2585
|
+
++this.#request;
|
|
2586
|
+
this.#error.hidden = true;
|
|
2587
|
+
for (const field of Object.values(this.#fields)) field.value = "";
|
|
2588
|
+
}
|
|
2589
|
+
render(state) {
|
|
2590
|
+
const pageId = state.selectedShape?.pageId ?? state.document?.pages[state.pageIndex]?.id;
|
|
2591
|
+
const identity = JSON.stringify([
|
|
2592
|
+
this.controller.sourceGeneration,
|
|
2593
|
+
state.pageIndex,
|
|
2594
|
+
pageId,
|
|
2595
|
+
state.selectedShape?.id
|
|
2596
|
+
]);
|
|
2597
|
+
if (identity !== this.#identity) {
|
|
2598
|
+
this.#identity = identity;
|
|
2599
|
+
++this.#request;
|
|
2600
|
+
this.#error.hidden = true;
|
|
2601
|
+
for (const field of Object.values(this.#fields)) field.value = "";
|
|
2602
|
+
}
|
|
2603
|
+
const available = state.edit.sourceAvailable && pageId !== void 0 && !state.loading && !state.edit.busy;
|
|
2604
|
+
for (const field of Object.values(this.#fields)) field.disabled = !available;
|
|
2605
|
+
const position = [this.#fields.x, this.#fields.y].every((field) => Number.isFinite(field.valueAsNumber));
|
|
2606
|
+
const size = [this.#fields.width, this.#fields.height].every((field) => Number.isFinite(field.valueAsNumber) && field.valueAsNumber > 0);
|
|
2607
|
+
for (const button of this.#buttons) {
|
|
2608
|
+
const action = button.dataset.geometryAction;
|
|
2609
|
+
button.disabled = !available || (action === "create-rectangle" ? !this.#fields.id.value || !position || !size : !state.selectedShape || action === "move-shape" && !position || action === "resize-shape" && !size);
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
};
|
|
2613
|
+
//#endregion
|
|
2614
|
+
//#region src/viewer-edit-controls.ts
|
|
2615
|
+
/** Static markup only. Document text is assigned exclusively through value/textContent. */
|
|
2616
|
+
var editControlsTemplate = `<details class="edit-controls"><summary>Edit diagram (experimental)</summary><p id="edit-warning">Experimental source-backed editing. Unsupported targets are rejected. Native Visio compatibility is not verified. Keep your original file.</p><p data-edit-target></p><label for="edit-text">Selected shape text</label><textarea id="edit-text" rows="3" aria-describedby="edit-warning edit-target edit-status"></textarea><div class="edit-actions"><button type="button" data-edit="apply">Apply text</button><button type="button" data-edit="cancel">Cancel</button><button type="button" data-edit="undo">Undo</button><button type="button" data-edit="redo">Redo</button></div><p id="edit-status" role="status" aria-live="polite"></p><p data-edit-error role="alert" hidden></p><ul data-edit-diagnostics></ul>${geometryControlsTemplate}</details>`;
|
|
2617
|
+
var ViewerEditControls = class {
|
|
2618
|
+
panel;
|
|
2619
|
+
input;
|
|
2620
|
+
#target;
|
|
2621
|
+
#status;
|
|
2622
|
+
#error;
|
|
2623
|
+
#diagnostics;
|
|
2624
|
+
#buttons;
|
|
2625
|
+
#identity = "";
|
|
2626
|
+
#initial = "";
|
|
2627
|
+
#pageId;
|
|
2628
|
+
#shapeId;
|
|
2629
|
+
#request = 0;
|
|
2630
|
+
#pending = false;
|
|
2631
|
+
#localError;
|
|
2632
|
+
#controller;
|
|
2633
|
+
#geometry;
|
|
2634
|
+
constructor(root, controller) {
|
|
2635
|
+
this.#controller = controller;
|
|
2636
|
+
this.panel = root.querySelector(".edit-controls");
|
|
2637
|
+
this.#geometry = new ViewerGeometryControls(this.panel, controller, () => this.render(controller.state));
|
|
2638
|
+
this.input = this.panel.querySelector("textarea");
|
|
2639
|
+
this.#target = this.panel.querySelector("[data-edit-target]");
|
|
2640
|
+
this.#target.id = "edit-target";
|
|
2641
|
+
this.#status = this.panel.querySelector("#edit-status");
|
|
2642
|
+
this.#error = this.panel.querySelector("[data-edit-error]");
|
|
2643
|
+
this.#diagnostics = this.panel.querySelector("[data-edit-diagnostics]");
|
|
2644
|
+
const button = (name) => this.panel.querySelector(`[data-edit="${name}"]`);
|
|
2645
|
+
this.#buttons = {
|
|
2646
|
+
apply: button("apply"),
|
|
2647
|
+
cancel: button("cancel"),
|
|
2648
|
+
undo: button("undo"),
|
|
2649
|
+
redo: button("redo")
|
|
2650
|
+
};
|
|
2651
|
+
}
|
|
2652
|
+
wire() {
|
|
2653
|
+
const disposeGeometry = this.#geometry.wire();
|
|
2654
|
+
const events = new ((this.panel.ownerDocument.defaultView?.AbortController) ?? AbortController)();
|
|
2655
|
+
const options = { signal: events.signal };
|
|
2656
|
+
this.input.addEventListener("input", () => this.render(this.#controller.state), options);
|
|
2657
|
+
this.input.addEventListener("keydown", (event) => {
|
|
2658
|
+
if (event.isComposing) return;
|
|
2659
|
+
if (event.key === "Escape") {
|
|
2660
|
+
event.preventDefault();
|
|
2661
|
+
this.reset();
|
|
2662
|
+
}
|
|
2663
|
+
if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
|
|
2664
|
+
event.preventDefault();
|
|
2665
|
+
this.#buttons.apply.click();
|
|
2666
|
+
}
|
|
2667
|
+
}, options);
|
|
2668
|
+
this.#buttons.cancel.addEventListener("click", () => this.reset(), options);
|
|
2669
|
+
for (const action of [
|
|
2670
|
+
"apply",
|
|
2671
|
+
"undo",
|
|
2672
|
+
"redo"
|
|
2673
|
+
]) this.#buttons[action].addEventListener("click", () => {
|
|
2674
|
+
this.#run(action);
|
|
2675
|
+
}, options);
|
|
2676
|
+
return () => {
|
|
2677
|
+
disposeGeometry();
|
|
2678
|
+
events.abort();
|
|
2679
|
+
++this.#request;
|
|
2680
|
+
this.#pending = false;
|
|
2681
|
+
this.#localError = void 0;
|
|
2682
|
+
this.#identity = "";
|
|
2683
|
+
this.input.value = this.#initial;
|
|
2684
|
+
};
|
|
2685
|
+
}
|
|
2686
|
+
reset(focus = true) {
|
|
2687
|
+
this.#geometry.reset();
|
|
2688
|
+
++this.#request;
|
|
2689
|
+
this.#pending = false;
|
|
2690
|
+
this.#localError = void 0;
|
|
2691
|
+
this.#controller.cancelEdit();
|
|
2692
|
+
this.input.value = this.#initial;
|
|
2693
|
+
this.render(this.#controller.state);
|
|
2694
|
+
if (focus && !this.input.disabled) this.input.focus();
|
|
2695
|
+
}
|
|
2696
|
+
async #run(action) {
|
|
2697
|
+
if (this.#buttons[action].disabled) return;
|
|
2698
|
+
const request = ++this.#request;
|
|
2699
|
+
this.#pending = true;
|
|
2700
|
+
this.#localError = void 0;
|
|
2701
|
+
this.render(this.#controller.state);
|
|
2702
|
+
try {
|
|
2703
|
+
if (action === "apply") {
|
|
2704
|
+
if (this.#pageId === void 0 || this.#shapeId === void 0) return;
|
|
2705
|
+
await this.#controller.replacePlainText(this.#pageId, this.#shapeId, this.input.value);
|
|
2706
|
+
} else await this.#controller[action]();
|
|
2707
|
+
} catch (error) {
|
|
2708
|
+
if (request === this.#request && !isEditCancellation(error)) this.#localError = editErrorMessage(error);
|
|
2709
|
+
} finally {
|
|
2710
|
+
if (request === this.#request) {
|
|
2711
|
+
this.#pending = false;
|
|
2712
|
+
this.render(this.#controller.state);
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
render(state) {
|
|
2717
|
+
this.#geometry.render(state);
|
|
2718
|
+
const selection = state.selectedShape;
|
|
2719
|
+
const pageId = selection?.pageId ?? state.document?.pages[state.pageIndex]?.id;
|
|
2720
|
+
const shape = selectedShape(state.document, selection, state.pageIndex);
|
|
2721
|
+
const identity = JSON.stringify([
|
|
2722
|
+
this.#controller.sourceGeneration,
|
|
2723
|
+
state.pageIndex,
|
|
2724
|
+
pageId,
|
|
2725
|
+
selection?.id
|
|
2726
|
+
]);
|
|
2727
|
+
if (identity !== this.#identity) {
|
|
2728
|
+
this.#identity = identity;
|
|
2729
|
+
++this.#request;
|
|
2730
|
+
this.#pending = false;
|
|
2731
|
+
this.#localError = void 0;
|
|
2732
|
+
this.#pageId = shape ? pageId : void 0;
|
|
2733
|
+
this.#shapeId = shape?.id;
|
|
2734
|
+
this.#initial = shape?.text?.plainText ?? "";
|
|
2735
|
+
this.input.value = this.#initial;
|
|
2736
|
+
} else if (shape) {
|
|
2737
|
+
const dirty = this.input.value !== this.#initial;
|
|
2738
|
+
this.#initial = shape.text?.plainText ?? "";
|
|
2739
|
+
if (!dirty) this.input.value = this.#initial;
|
|
2740
|
+
}
|
|
2741
|
+
const busy = state.loading || state.edit.busy || this.#pending;
|
|
2742
|
+
const available = state.edit.sourceAvailable && !!shape && pageId !== void 0;
|
|
2743
|
+
this.panel.setAttribute("aria-busy", String(busy));
|
|
2744
|
+
this.#target.textContent = shape ? `Page ID: ${pageId} · Shape ID: ${shape.id} · ${shape.name || "Unnamed shape"}` : "Select a shape to replace its plain text.";
|
|
2745
|
+
this.input.disabled = !available || busy;
|
|
2746
|
+
this.#buttons.apply.disabled = !available || busy || this.input.value === this.#initial;
|
|
2747
|
+
this.#buttons.cancel.disabled = !state.edit.busy && !this.#pending && this.input.value === this.#initial && !this.#geometry.hasDraft;
|
|
2748
|
+
this.#buttons.undo.disabled = busy || !state.edit.canUndo;
|
|
2749
|
+
this.#buttons.redo.disabled = busy || !state.edit.canRedo;
|
|
2750
|
+
this.#status.textContent = state.loading ? "Opening diagram…" : busy ? "Updating diagram…" : !state.edit.sourceAvailable ? "Open a .vsdx file to edit or download a VSDX copy. Model-only documents cannot be saved." : `${state.edit.dirty ? "Edited copy" : "Original bytes"}${state.edit.historyTruncated ? " · Earlier undo history was discarded." : ""}`;
|
|
2751
|
+
const error = this.#localError ?? (state.edit.error ? editErrorMessage(state.edit.error) : void 0);
|
|
2752
|
+
this.#error.hidden = !error;
|
|
2753
|
+
this.#error.textContent = error ?? "";
|
|
2754
|
+
this.#diagnostics.replaceChildren(...state.edit.diagnostics.map((diagnostic) => {
|
|
2755
|
+
const item = this.panel.ownerDocument.createElement("li");
|
|
2756
|
+
item.textContent = `${diagnostic.code}: ${diagnostic.message}`;
|
|
2757
|
+
return item;
|
|
2758
|
+
}));
|
|
2759
|
+
}
|
|
2760
|
+
};
|
|
2761
|
+
//#endregion
|
|
2762
|
+
//#region src/viewer-search.ts
|
|
2763
|
+
var searchTemplate = `<div class="search-controls" role="search" aria-label="Diagram text search"><label>Find <input type="search" aria-label="Search diagram text" placeholder="Text across pages" maxlength="${TEXT_SEARCH_LIMITS.queryCharacters}" autocomplete="off" spellcheck="false" aria-describedby="search-status"></label><button type="button" data-action="search-previous" aria-label="Previous matching shape">Previous</button><button type="button" data-action="search-next" aria-label="Next matching shape">Next</button><span id="search-status" role="status" aria-live="polite" aria-atomic="true"></span></div>`;
|
|
2764
|
+
function searchControls(root) {
|
|
2765
|
+
return {
|
|
2766
|
+
input: root.querySelector("input[type=\"search\"]"),
|
|
2767
|
+
status: root.querySelector("#search-status"),
|
|
2768
|
+
previous: root.querySelector("[data-action=\"search-previous\"]"),
|
|
2769
|
+
next: root.querySelector("[data-action=\"search-next\"]")
|
|
2770
|
+
};
|
|
2771
|
+
}
|
|
2772
|
+
function renderSearchControls(controls, state) {
|
|
2773
|
+
if (controls.input.value !== state.search.query) controls.input.value = state.search.query;
|
|
2774
|
+
controls.input.disabled = !state.document?.pages.length;
|
|
2775
|
+
controls.previous.disabled = controls.next.disabled = !state.search.results.length;
|
|
2776
|
+
controls.status.textContent = state.document ? textSearchStatus(state.search) : "Open a diagram to search";
|
|
2777
|
+
const result = state.search.results[state.search.activeIndex];
|
|
2778
|
+
controls.status.title = result ? `${result.pageName} · ${result.shapeName}: ${result.preview}` : "";
|
|
2779
|
+
}
|
|
2780
|
+
//#endregion
|
|
2781
|
+
//#region src/viewer-chrome.ts
|
|
2782
|
+
/** Only static application markup belongs here. All document labels are inserted as text. */
|
|
2783
|
+
var viewerChromeTemplate = `
|
|
2784
|
+
<div class="toolbar" role="group" aria-label="Diagram controls">
|
|
2785
|
+
<div class="ribbon-primary">
|
|
2786
|
+
<label class="page-picker">Page <select aria-label="Page"></select></label>
|
|
2787
|
+
<div class="page-stepper"><button type="button" data-chrome="previous-page" aria-label="Previous page">‹</button><button type="button" data-chrome="next-page" aria-label="Next page">›</button></div>
|
|
2788
|
+
<span class="spacer"></span><span class="local-label">Local workspace</span>
|
|
2789
|
+
</div>
|
|
2790
|
+
<div class="ribbon-tabs" role="tablist" aria-label="Ribbon">
|
|
2791
|
+
<button type="button" id="home-tab" role="tab" aria-selected="true" aria-controls="home-panel" data-tab="home">Home</button>
|
|
2792
|
+
<button type="button" id="view-tab" role="tab" aria-selected="false" aria-controls="view-panel" tabindex="-1" data-tab="view">View</button>
|
|
2793
|
+
</div>
|
|
2794
|
+
<div class="ribbon-content" id="home-panel" role="tabpanel" aria-labelledby="home-tab">
|
|
2795
|
+
<div class="ribbon-group search-group">${searchTemplate}<span class="group-label">Find in drawing</span></div>
|
|
2796
|
+
<div class="ribbon-group"><div class="ribbon-actions">
|
|
2797
|
+
<button class="ribbon-command" type="button" data-chrome="selection"><span class="command-icon" data-icon="selection" aria-hidden="true"></span><span>Shape details</span></button>
|
|
2798
|
+
<button class="ribbon-command" type="button" data-chrome="edit"><span class="command-icon" data-icon="edit" aria-hidden="true"></span><span>Edit text</span></button>
|
|
2799
|
+
<button class="ribbon-command" type="button" data-chrome="layers"><span class="command-icon" data-icon="layers" aria-hidden="true"></span><span>Layers</span></button>
|
|
2800
|
+
</div><span class="group-label">Inspect and edit</span></div>
|
|
2801
|
+
<p class="ribbon-hint">Select a shape to inspect its details.<br>Text editing is experimental.</p>
|
|
2802
|
+
</div>
|
|
2803
|
+
<div class="ribbon-content" id="view-panel" role="tabpanel" aria-labelledby="view-tab" hidden>
|
|
2804
|
+
<div class="ribbon-group"><div class="ribbon-actions">
|
|
2805
|
+
<button class="ribbon-command" type="button" data-chrome="pages" aria-pressed="true" aria-controls="page-rail"><span class="command-icon" data-icon="pages" aria-hidden="true"></span><span>Pages pane</span></button>
|
|
2806
|
+
<button class="ribbon-command" type="button" data-chrome="inspector" aria-pressed="true" aria-controls="inspector-pane"><span class="command-icon" data-icon="inspector" aria-hidden="true"></span><span>Inspector pane</span></button>
|
|
2807
|
+
<button class="ribbon-command" type="button" data-chrome="notes"><span class="command-icon" data-icon="notes" aria-hidden="true"></span><span>Review notes</span></button>
|
|
2808
|
+
</div><span class="group-label">Workspace panes</span></div>
|
|
2809
|
+
<p class="ribbon-hint">Canvas shortcuts: + / − to zoom, 0 to fit.<br>Arrow keys move between focused shapes.</p>
|
|
2810
|
+
</div>
|
|
2811
|
+
</div>
|
|
2812
|
+
<div class="workspace">
|
|
2813
|
+
<nav id="page-rail" class="page-rail" aria-label="Diagram pages"><div class="pane-heading"><span>Pages</span><span data-page-count>0</span></div><ol class="page-list"></ol><p class="page-empty">Open a drawing to see its pages.</p></nav>
|
|
2814
|
+
<div class="viewport" tabindex="0" role="region" aria-label="Diagram canvas"></div>
|
|
2815
|
+
<aside id="inspector-pane" class="inspector-pane" aria-label="Drawing inspector">
|
|
2816
|
+
<div class="pane-heading"><span>Inspector</span><span class="inspector-kind">Drawing</span></div>
|
|
2817
|
+
<div class="inspector-body">
|
|
2818
|
+
<section class="inspector-card document-card" aria-label="Current page"><h2>Current page</h2><p class="current-page-name" data-page-name>No diagram open</p><dl><dt>Size</dt><dd data-page-size>No page</dd><dt>Shapes</dt><dd data-page-shapes>No shapes</dd></dl></section>
|
|
2819
|
+
<p class="selection-hint">Select a shape on the canvas to see its data and links.</p>
|
|
2820
|
+
<details class="shape-inspector inspector-card" hidden><summary>Selected shape</summary><div></div></details>
|
|
2821
|
+
${editControlsTemplate}${layerControlsTemplate}
|
|
2822
|
+
<details class="notes inspector-card"><summary>Compatibility notes</summary><ul></ul></details>
|
|
2823
|
+
</div>
|
|
2824
|
+
</aside>
|
|
2825
|
+
</div>
|
|
2826
|
+
<div class="notes-strip"><button type="button" data-chrome="notes" aria-controls="inspector-pane" aria-expanded="false">Notes<span class="notes-count" data-note-count></span></button><span>Files stay in your browser</span></div>
|
|
2827
|
+
<div class="status"><div class="status-message" role="status"><span data-status></span><span data-diagnostics></span></div><div class="zoom-controls" role="group" aria-label="Canvas zoom"><button type="button" data-action="fit">Fit page</button><button type="button" data-action="actual">100%</button><span class="zoom-divider"></span><button type="button" data-action="out" aria-label="Zoom out">−</button><output class="zoom" aria-label="Zoom level">100%</output><button type="button" data-action="in" aria-label="Zoom in">+</button></div></div>`;
|
|
2828
|
+
/** Presentation state stays local; page navigation uses the same controller as every binding. */
|
|
2829
|
+
var ViewerChrome = class {
|
|
2830
|
+
#root;
|
|
2831
|
+
#controller;
|
|
2832
|
+
#document;
|
|
2833
|
+
#pageList;
|
|
2834
|
+
#rail;
|
|
2835
|
+
#inspector;
|
|
2836
|
+
#notes;
|
|
2837
|
+
#responsive;
|
|
2838
|
+
#pagesManuallyToggled = false;
|
|
2839
|
+
constructor(root, controller) {
|
|
2840
|
+
this.#root = root;
|
|
2841
|
+
this.#controller = controller;
|
|
2842
|
+
this.#pageList = root.querySelector(".page-list");
|
|
2843
|
+
this.#rail = root.querySelector(".page-rail");
|
|
2844
|
+
this.#inspector = root.querySelector(".inspector-pane");
|
|
2845
|
+
this.#notes = root.querySelector(".notes");
|
|
2846
|
+
}
|
|
2847
|
+
wire() {
|
|
2848
|
+
const view = this.#root.ownerDocument.defaultView;
|
|
2849
|
+
const events = new ((view?.AbortController) ?? AbortController)(), options = { signal: events.signal };
|
|
2850
|
+
this.#responsive = view?.matchMedia?.("(max-width: 980px)");
|
|
2851
|
+
const responsive = () => {
|
|
2852
|
+
if (!this.#pagesManuallyToggled) this.#setPane("pages", !this.#responsive?.matches);
|
|
2853
|
+
};
|
|
2854
|
+
responsive();
|
|
2855
|
+
this.#responsive?.addEventListener("change", responsive, options);
|
|
2856
|
+
this.#root.addEventListener("click", (event) => {
|
|
2857
|
+
const button = event.target?.closest?.("button");
|
|
2858
|
+
if (!button || button.disabled) return;
|
|
2859
|
+
const tab = button.dataset.tab;
|
|
2860
|
+
if (tab === "home" || tab === "view") this.#showTab(tab);
|
|
2861
|
+
if (button.dataset.pageIndex !== void 0) this.#controller.setPage(Number(button.dataset.pageIndex));
|
|
2862
|
+
const action = button.dataset.chrome;
|
|
2863
|
+
if (action === "previous-page") this.#controller.setPage(this.#controller.state.pageIndex - 1);
|
|
2864
|
+
if (action === "next-page") this.#controller.setPage(this.#controller.state.pageIndex + 1);
|
|
2865
|
+
if (action === "pages") {
|
|
2866
|
+
this.#pagesManuallyToggled = true;
|
|
2867
|
+
this.#setPane("pages", this.#rail.hidden);
|
|
2868
|
+
}
|
|
2869
|
+
if (action === "inspector") this.#setPane("inspector", this.#inspector.hidden);
|
|
2870
|
+
if (action === "notes" || action === "selection" || action === "edit" || action === "layers") this.#reveal(action);
|
|
2871
|
+
}, options);
|
|
2872
|
+
this.#root.addEventListener("keydown", (event) => {
|
|
2873
|
+
const key = event;
|
|
2874
|
+
const button = event.target?.closest?.("button");
|
|
2875
|
+
if (!button || ![
|
|
2876
|
+
"ArrowLeft",
|
|
2877
|
+
"ArrowRight",
|
|
2878
|
+
"ArrowUp",
|
|
2879
|
+
"ArrowDown",
|
|
2880
|
+
"Home",
|
|
2881
|
+
"End"
|
|
2882
|
+
].includes(key.key)) return;
|
|
2883
|
+
if (button.dataset.tab) {
|
|
2884
|
+
if (key.key === "ArrowUp" || key.key === "ArrowDown") return;
|
|
2885
|
+
key.preventDefault();
|
|
2886
|
+
const tab = key.key === "Home" ? "home" : key.key === "End" ? "view" : button.dataset.tab === "home" ? "view" : "home";
|
|
2887
|
+
this.#showTab(tab);
|
|
2888
|
+
this.#root.querySelector(`[data-tab="${tab}"]`).focus();
|
|
2889
|
+
} else if (button.dataset.pageIndex !== void 0) {
|
|
2890
|
+
key.preventDefault();
|
|
2891
|
+
const pages = [...this.#pageList.querySelectorAll("button")];
|
|
2892
|
+
const current = Number(button.dataset.pageIndex);
|
|
2893
|
+
const next = key.key === "Home" ? 0 : key.key === "End" ? pages.length - 1 : Math.max(0, Math.min(pages.length - 1, current + (["ArrowRight", "ArrowDown"].includes(key.key) ? 1 : -1)));
|
|
2894
|
+
this.#controller.setPage(next);
|
|
2895
|
+
pages[next]?.focus();
|
|
2896
|
+
}
|
|
2897
|
+
}, options);
|
|
2898
|
+
this.#notes.addEventListener("toggle", () => this.#syncNotes(), options);
|
|
2899
|
+
return () => events.abort();
|
|
2900
|
+
}
|
|
2901
|
+
#showTab(tab) {
|
|
2902
|
+
for (const button of this.#root.querySelectorAll("[data-tab]")) {
|
|
2903
|
+
const selected = button.dataset.tab === tab;
|
|
2904
|
+
button.setAttribute("aria-selected", String(selected));
|
|
2905
|
+
button.tabIndex = selected ? 0 : -1;
|
|
2906
|
+
}
|
|
2907
|
+
for (const panel of this.#root.querySelectorAll("[role=\"tabpanel\"]")) panel.hidden = panel.id !== `${tab}-panel`;
|
|
2908
|
+
}
|
|
2909
|
+
#setPane(pane, visible) {
|
|
2910
|
+
(pane === "pages" ? this.#rail : this.#inspector).hidden = !visible;
|
|
2911
|
+
this.#root.querySelector(`[data-chrome="${pane}"]`).setAttribute("aria-pressed", String(visible));
|
|
2912
|
+
this.#syncNotes();
|
|
2913
|
+
}
|
|
2914
|
+
#syncNotes() {
|
|
2915
|
+
this.#root.querySelector(".notes-strip button").setAttribute("aria-expanded", String(!this.#inspector.hidden && this.#notes.open));
|
|
2916
|
+
}
|
|
2917
|
+
#reveal(kind) {
|
|
2918
|
+
this.#setPane("inspector", true);
|
|
2919
|
+
const selector = kind === "notes" ? ".notes" : kind === "edit" ? ".edit-controls" : kind === "layers" ? ".layer-controls" : ".shape-inspector";
|
|
2920
|
+
const panel = this.#root.querySelector(selector);
|
|
2921
|
+
if (panel.hidden) return;
|
|
2922
|
+
panel.open = true;
|
|
2923
|
+
panel.querySelector("summary").focus();
|
|
2924
|
+
panel.scrollIntoView?.({
|
|
2925
|
+
block: "nearest",
|
|
2926
|
+
inline: "nearest"
|
|
2927
|
+
});
|
|
2928
|
+
this.#syncNotes();
|
|
2929
|
+
}
|
|
2930
|
+
render(state, noteCount) {
|
|
2931
|
+
const page = state.document?.pages[state.pageIndex];
|
|
2932
|
+
if (state.document !== this.#document) {
|
|
2933
|
+
this.#document = state.document;
|
|
2934
|
+
const restoreFocus = this.#root.activeElement?.dataset.pageIndex;
|
|
2935
|
+
const fragment = this.#root.ownerDocument.createDocumentFragment();
|
|
2936
|
+
for (const [index, candidate] of (state.document?.pages ?? []).entries()) {
|
|
2937
|
+
const item = this.#root.ownerDocument.createElement("li");
|
|
2938
|
+
const button = this.#root.ownerDocument.createElement("button");
|
|
2939
|
+
button.type = "button";
|
|
2940
|
+
button.className = "page-link";
|
|
2941
|
+
button.dataset.pageIndex = String(index);
|
|
2942
|
+
button.setAttribute("aria-label", `Go to page ${index + 1}: ${candidate.name}`);
|
|
2943
|
+
const number = this.#root.ownerDocument.createElement("span");
|
|
2944
|
+
number.className = "page-number";
|
|
2945
|
+
number.textContent = String(index + 1);
|
|
2946
|
+
const card = this.#root.ownerDocument.createElement("span");
|
|
2947
|
+
card.className = "page-card";
|
|
2948
|
+
const mark = this.#root.ownerDocument.createElement("span");
|
|
2949
|
+
mark.className = "page-mark";
|
|
2950
|
+
mark.textContent = "▤";
|
|
2951
|
+
mark.setAttribute("aria-hidden", "true");
|
|
2952
|
+
const name = this.#root.ownerDocument.createElement("span");
|
|
2953
|
+
name.className = "page-name";
|
|
2954
|
+
name.textContent = candidate.name;
|
|
2955
|
+
const meta = this.#root.ownerDocument.createElement("span");
|
|
2956
|
+
meta.className = "page-meta";
|
|
2957
|
+
meta.textContent = candidate.isBackground ? "Background page" : `${candidate.width} × ${candidate.height} in`;
|
|
2958
|
+
card.append(mark, name, meta);
|
|
2959
|
+
button.append(number, card);
|
|
2960
|
+
item.append(button);
|
|
2961
|
+
fragment.append(item);
|
|
2962
|
+
}
|
|
2963
|
+
this.#pageList.replaceChildren(fragment);
|
|
2964
|
+
if (restoreFocus !== void 0) [...this.#pageList.querySelectorAll("button")].find((button) => button.dataset.pageIndex === restoreFocus)?.focus();
|
|
2965
|
+
}
|
|
2966
|
+
for (const button of this.#pageList.querySelectorAll("button")) {
|
|
2967
|
+
const selected = Number(button.dataset.pageIndex) === state.pageIndex;
|
|
2968
|
+
if (selected) button.setAttribute("aria-current", "page");
|
|
2969
|
+
else button.removeAttribute("aria-current");
|
|
2970
|
+
button.tabIndex = selected ? 0 : -1;
|
|
2971
|
+
}
|
|
2972
|
+
const count = state.document?.pages.length ?? 0;
|
|
2973
|
+
this.#root.querySelector("[data-page-count]").textContent = String(count);
|
|
2974
|
+
this.#root.querySelector(".page-empty").hidden = count > 0;
|
|
2975
|
+
this.#root.querySelector("[data-page-name]").textContent = page?.name ?? "No diagram open";
|
|
2976
|
+
this.#root.querySelector("[data-page-size]").textContent = page ? `${page.width} × ${page.height} in` : "No page";
|
|
2977
|
+
this.#root.querySelector("[data-page-shapes]").textContent = page ? `${page.shapes.length} top-level` : "No shapes";
|
|
2978
|
+
this.#root.querySelector(".selection-hint").hidden = !!state.selectedShape;
|
|
2979
|
+
this.#root.querySelector("[data-chrome=\"previous-page\"]").disabled = !page || state.pageIndex === 0;
|
|
2980
|
+
this.#root.querySelector("[data-chrome=\"next-page\"]").disabled = !page || state.pageIndex >= count - 1;
|
|
2981
|
+
this.#root.querySelector("[data-chrome=\"selection\"]").disabled = !state.selectedShape;
|
|
2982
|
+
this.#root.querySelector("[data-chrome=\"edit\"]").disabled = !page;
|
|
2983
|
+
this.#root.querySelector("[data-chrome=\"layers\"]").disabled = this.#root.querySelector(".layer-controls").hidden;
|
|
2984
|
+
for (const button of this.#root.querySelectorAll("[data-chrome=\"notes\"]")) button.disabled = noteCount === 0;
|
|
2985
|
+
this.#root.querySelector("[data-note-count]").textContent = noteCount ? ` · ${noteCount}` : "";
|
|
2986
|
+
this.#syncNotes();
|
|
2987
|
+
}
|
|
2988
|
+
};
|
|
2989
|
+
//#endregion
|
|
2990
|
+
//#region src/export-svg.ts
|
|
2991
|
+
var MAX_SVG_EXPORT_BYTES = 16777216;
|
|
2992
|
+
var APPROXIMATION = "Static SVG export is a rendered approximation, not an editable VSDX round-trip or a guarantee of Microsoft Visio fidelity. Fonts are not embedded.";
|
|
2993
|
+
var INVALID_XML = "Invalid XML characters were replaced with the Unicode replacement character in the SVG export.";
|
|
2994
|
+
var limitError = () => /* @__PURE__ */ new Error("This page exceeds the safe SVG export byte or amplification limit.");
|
|
2995
|
+
/**
|
|
2996
|
+
* Export one page through the shared renderer, including its resolved backgrounds.
|
|
2997
|
+
* Requires a browser DOM at call time, never uploads, downloads or mutates viewer state.
|
|
2998
|
+
* Conservative preflight limits can reject a page whose eventual serialization would be smaller.
|
|
2999
|
+
*/
|
|
3000
|
+
function exportPageSvg(model, pageIndex = 0, options = {}) {
|
|
3001
|
+
const maxBytes = options.maxBytes ?? 16777216;
|
|
3002
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > 16777216) throw new Error("SVG maxBytes must be a positive integer no greater than 16 MiB.");
|
|
3003
|
+
assertViewableDocument(model);
|
|
3004
|
+
if (!Number.isSafeInteger(pageIndex) || pageIndex < 0 || pageIndex >= model.pages.length) throw new Error("The SVG export page index is out of range.");
|
|
3005
|
+
const page = model.pages[pageIndex];
|
|
3006
|
+
const { width, height } = page;
|
|
3007
|
+
estimatePageSvgBytes(model, page, maxBytes);
|
|
3008
|
+
if (typeof document === "undefined" || typeof XMLSerializer === "undefined") throw new Error("SVG export requires a browser DOM and XMLSerializer.");
|
|
3009
|
+
const rendered = renderPage(model, page, { static: true });
|
|
3010
|
+
try {
|
|
3011
|
+
const { svg } = rendered;
|
|
3012
|
+
svg.setAttribute("width", `${width}in`);
|
|
3013
|
+
svg.setAttribute("height", `${height}in`);
|
|
3014
|
+
let replaced = normalizeXmlTree(svg);
|
|
3015
|
+
const notes = compatibilityNotes(model.diagnostics, [APPROXIMATION, ...rendered.warnings]);
|
|
3016
|
+
const pageName = xmlText(page.name);
|
|
3017
|
+
for (const note of notes) {
|
|
3018
|
+
const message = xmlText(note.message);
|
|
3019
|
+
if (message !== note.message) replaced = true;
|
|
3020
|
+
note.message = message;
|
|
3021
|
+
}
|
|
3022
|
+
if (replaced) notes.push({
|
|
3023
|
+
message: INVALID_XML,
|
|
3024
|
+
severity: "warning",
|
|
3025
|
+
count: 1
|
|
3026
|
+
});
|
|
3027
|
+
const description = svgElement("desc");
|
|
3028
|
+
description.textContent = notes.map(compatibilityText).join("\n");
|
|
3029
|
+
svg.insertBefore(description, svg.children[1] ?? null);
|
|
3030
|
+
const metadata = svgElement("metadata");
|
|
3031
|
+
metadata.textContent = JSON.stringify({
|
|
3032
|
+
format: "visio-viewer-static-svg",
|
|
3033
|
+
page: {
|
|
3034
|
+
index: pageIndex,
|
|
3035
|
+
name: pageName,
|
|
3036
|
+
width,
|
|
3037
|
+
height,
|
|
3038
|
+
unit: "in"
|
|
3039
|
+
},
|
|
3040
|
+
diagnostics: notes
|
|
3041
|
+
});
|
|
3042
|
+
svg.append(metadata);
|
|
3043
|
+
assertSerializedBudget(svg, maxBytes);
|
|
3044
|
+
const output = new XMLSerializer().serializeToString(svg);
|
|
3045
|
+
const byteLength = new TextEncoder().encode(output).byteLength;
|
|
3046
|
+
if (byteLength > maxBytes) throw limitError();
|
|
3047
|
+
return {
|
|
3048
|
+
svg: output,
|
|
3049
|
+
byteLength,
|
|
3050
|
+
pageIndex,
|
|
3051
|
+
pageName,
|
|
3052
|
+
width,
|
|
3053
|
+
height,
|
|
3054
|
+
diagnostics: notes
|
|
3055
|
+
};
|
|
3056
|
+
} finally {
|
|
3057
|
+
rendered.dispose();
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
/** @internal Call only after canonical scene validation. Shared conservative allocation preflight. */
|
|
3061
|
+
function estimatePageSvgBytes(model, page, maxBytes) {
|
|
3062
|
+
let remaining = maxBytes;
|
|
3063
|
+
const reserve = (bytes) => {
|
|
3064
|
+
remaining -= bytes;
|
|
3065
|
+
if (remaining < 0) throw limitError();
|
|
3066
|
+
};
|
|
3067
|
+
reserve(8192 + page.name.length * 36);
|
|
3068
|
+
for (const note of model.diagnostics) reserve(384 + note.message.length * 36);
|
|
3069
|
+
const images = /* @__PURE__ */ new Map();
|
|
3070
|
+
const stack = [];
|
|
3071
|
+
for (const layer of getVisioPageLayers(model, page.id)) stack.push(...layer.shapes);
|
|
3072
|
+
while (stack.length) {
|
|
3073
|
+
const shape = stack.pop();
|
|
3074
|
+
if (shape.hidden) continue;
|
|
3075
|
+
stack.push(...shape.children);
|
|
3076
|
+
reserve(1024 + (shape.text.plainText || shape.name).length * 6);
|
|
3077
|
+
if (shape.kind === "group" && shape.groupDisplayMode === 0) continue;
|
|
3078
|
+
for (const geometry of shape.geometry) reserve(4096 + geometry.path.length * 6 + shape.style.lineColor.length * 24 + shape.style.fill.length * 6);
|
|
3079
|
+
for (const stop of shape.style.fillGradient?.stops ?? []) reserve(256 + stop.color.length * 6);
|
|
3080
|
+
if (shape.image) {
|
|
3081
|
+
reserve(1536);
|
|
3082
|
+
const image = shape.image;
|
|
3083
|
+
const types = images.get(image.bytes) ?? /* @__PURE__ */ new Set();
|
|
3084
|
+
if (!types.has(image.mimeType)) {
|
|
3085
|
+
reserve(512 + 4 * Math.ceil(image.bytes.byteLength / 3));
|
|
3086
|
+
types.add(image.mimeType);
|
|
3087
|
+
images.set(image.bytes, types);
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
if (shape.foreignVector) reserve(inspectForeignVectorResource(shape.foreignVector.vector).svgBytes);
|
|
3091
|
+
if (!shape.text.plainText) continue;
|
|
3092
|
+
const text = shape.text;
|
|
3093
|
+
const runs = text.runs.length ? text.runs : [{
|
|
3094
|
+
text: text.plainText,
|
|
3095
|
+
fontFamily: text.fontFamily,
|
|
3096
|
+
color: text.color
|
|
3097
|
+
}];
|
|
3098
|
+
let characters = 0;
|
|
3099
|
+
for (const run of runs) {
|
|
3100
|
+
characters += run.text.length;
|
|
3101
|
+
reserve(run.text.length * (512 + run.fontFamily.length * 6 + run.color.length * 6));
|
|
3102
|
+
}
|
|
3103
|
+
reserve(Math.min(5e3, characters + (text.paragraphs?.length ?? 1)) * (640 + (text.backgroundColor ? 512 + text.backgroundColor.length * 6 : 0)));
|
|
3104
|
+
for (const paragraph of text.paragraphs ?? []) if (paragraph.bullet) reserve(512 + (paragraph.bullet.text.length + paragraph.bullet.fontFamily.length + text.color.length) * 6);
|
|
3105
|
+
}
|
|
3106
|
+
return maxBytes - remaining;
|
|
3107
|
+
}
|
|
3108
|
+
function xmlText(value) {
|
|
3109
|
+
return value.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\uFFFE\uFFFF\uD800-\uDFFF]/gu, "�");
|
|
3110
|
+
}
|
|
3111
|
+
function normalizeXmlTree(root) {
|
|
3112
|
+
let replaced = false;
|
|
3113
|
+
const stack = [root];
|
|
3114
|
+
while (stack.length) {
|
|
3115
|
+
const node = stack.pop();
|
|
3116
|
+
if (node.nodeType === 1) for (const attribute of Array.from(node.attributes)) {
|
|
3117
|
+
const normalized = xmlText(attribute.value);
|
|
3118
|
+
if (normalized !== attribute.value) {
|
|
3119
|
+
replaced = true;
|
|
3120
|
+
attribute.value = normalized;
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
else if (node.nodeType === 3) {
|
|
3124
|
+
const normalized = xmlText(node.nodeValue ?? "");
|
|
3125
|
+
if (normalized !== node.nodeValue) {
|
|
3126
|
+
replaced = true;
|
|
3127
|
+
node.nodeValue = normalized;
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
stack.push(...node.childNodes);
|
|
3131
|
+
}
|
|
3132
|
+
return replaced;
|
|
3133
|
+
}
|
|
3134
|
+
/** An upper bound, without allocating a second escaped copy of large paths or data URLs. */
|
|
3135
|
+
function assertSerializedBudget(root, maxBytes) {
|
|
3136
|
+
let bytes = 0;
|
|
3137
|
+
const add = (value) => {
|
|
3138
|
+
for (const point of value) {
|
|
3139
|
+
const code = point.codePointAt(0);
|
|
3140
|
+
bytes += /[&<>"'\t\n\r]/u.test(point) ? 6 : code < 128 ? 1 : code < 2048 ? 2 : code < 65536 ? 3 : 4;
|
|
3141
|
+
if (bytes > maxBytes) throw limitError();
|
|
3142
|
+
}
|
|
3143
|
+
};
|
|
3144
|
+
const stack = [root];
|
|
3145
|
+
while (stack.length) {
|
|
3146
|
+
const node = stack.pop();
|
|
3147
|
+
if (node.nodeType === 1) {
|
|
3148
|
+
const element = node;
|
|
3149
|
+
bytes += element.tagName.length * 2 + 5;
|
|
3150
|
+
for (const attribute of element.attributes) {
|
|
3151
|
+
bytes += attribute.name.length + 4;
|
|
3152
|
+
add(attribute.value);
|
|
3153
|
+
}
|
|
3154
|
+
} else if (node.nodeType === 3) add(node.nodeValue ?? "");
|
|
3155
|
+
if (bytes > maxBytes) throw limitError();
|
|
3156
|
+
stack.push(...node.childNodes);
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
//#endregion
|
|
3160
|
+
//#region src/snapshot-scene.ts
|
|
3161
|
+
/** Copy only named scalar fields, never arbitrary host properties or their getters. */
|
|
3162
|
+
function fields(source, keys) {
|
|
3163
|
+
const result = {};
|
|
3164
|
+
for (const key of keys) {
|
|
3165
|
+
const value = source[key];
|
|
3166
|
+
if (value !== null && (typeof value === "object" || typeof value === "function")) throw invalid();
|
|
3167
|
+
if (value !== void 0) result[key] = value;
|
|
3168
|
+
}
|
|
3169
|
+
return result;
|
|
3170
|
+
}
|
|
3171
|
+
var matrix = (value) => [
|
|
3172
|
+
value[0],
|
|
3173
|
+
value[1],
|
|
3174
|
+
value[2],
|
|
3175
|
+
value[3],
|
|
3176
|
+
value[4],
|
|
3177
|
+
value[5]
|
|
3178
|
+
];
|
|
3179
|
+
var invalid = () => /* @__PURE__ */ new Error("The scene exceeds safe snapshot copy limits.");
|
|
3180
|
+
/**
|
|
3181
|
+
* @internal Copy a canonically validated scene without retaining any renderer-used mutable input.
|
|
3182
|
+
* The caller validates the result before use. Independent allocation caps also bound a host that
|
|
3183
|
+
* changes its arrays while fields are read. Shared raster buffers are copied once per invocation.
|
|
3184
|
+
* Inspector metadata, connectors, layers and unknown host properties are not used by static SVG.
|
|
3185
|
+
*/
|
|
3186
|
+
function copySnapshotScene(model) {
|
|
3187
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3188
|
+
const take = (key, amount, maximum) => {
|
|
3189
|
+
const total = (counts.get(key) ?? 0) + amount;
|
|
3190
|
+
if (!Number.isSafeInteger(amount) || amount < 0 || total > maximum) throw invalid();
|
|
3191
|
+
counts.set(key, total);
|
|
3192
|
+
};
|
|
3193
|
+
const list = (source, key, maximum, copy) => {
|
|
3194
|
+
if (!Array.isArray(source)) throw invalid();
|
|
3195
|
+
const length = source.length;
|
|
3196
|
+
take(key, length, maximum);
|
|
3197
|
+
const result = [];
|
|
3198
|
+
for (let index = 0; index < length; index++) result.push(copy(source[index]));
|
|
3199
|
+
return result;
|
|
3200
|
+
};
|
|
3201
|
+
const resources = /* @__PURE__ */ new Map();
|
|
3202
|
+
const vectorBudget = new ForeignVectorBudget();
|
|
3203
|
+
const foreignVector = (source) => ({
|
|
3204
|
+
...fields(source, [
|
|
3205
|
+
"x",
|
|
3206
|
+
"y",
|
|
3207
|
+
"width",
|
|
3208
|
+
"height",
|
|
3209
|
+
"opacity"
|
|
3210
|
+
]),
|
|
3211
|
+
vector: vectorBudget.take(source.vector).vector
|
|
3212
|
+
});
|
|
3213
|
+
const image = (source) => {
|
|
3214
|
+
const original = source.bytes;
|
|
3215
|
+
if (!(original instanceof Uint8Array) || original.byteLength > 8388608) throw invalid();
|
|
3216
|
+
let bytes = resources.get(original);
|
|
3217
|
+
if (!bytes) {
|
|
3218
|
+
take("raster bytes", original.byteLength, 67108864);
|
|
3219
|
+
bytes = new Uint8Array(original.byteLength);
|
|
3220
|
+
bytes.set(original);
|
|
3221
|
+
resources.set(original, bytes);
|
|
3222
|
+
}
|
|
3223
|
+
return {
|
|
3224
|
+
...fields(source, [
|
|
3225
|
+
"mimeType",
|
|
3226
|
+
"pixelWidth",
|
|
3227
|
+
"pixelHeight",
|
|
3228
|
+
"opacity",
|
|
3229
|
+
"x",
|
|
3230
|
+
"y",
|
|
3231
|
+
"width",
|
|
3232
|
+
"height"
|
|
3233
|
+
]),
|
|
3234
|
+
bytes
|
|
3235
|
+
};
|
|
3236
|
+
};
|
|
3237
|
+
const style = (source) => {
|
|
3238
|
+
const result = fields(source, [
|
|
3239
|
+
"fill",
|
|
3240
|
+
"lineColor",
|
|
3241
|
+
"lineWidth",
|
|
3242
|
+
"lineCap",
|
|
3243
|
+
"linePattern",
|
|
3244
|
+
"fillOpacity",
|
|
3245
|
+
"lineOpacity",
|
|
3246
|
+
"startArrow",
|
|
3247
|
+
"endArrow",
|
|
3248
|
+
"startArrowSize",
|
|
3249
|
+
"endArrowSize"
|
|
3250
|
+
]);
|
|
3251
|
+
const dash = source.lineDash;
|
|
3252
|
+
const gradient = source.fillGradient;
|
|
3253
|
+
return {
|
|
3254
|
+
...result,
|
|
3255
|
+
...dash === void 0 ? {} : { lineDash: list(dash, "dash values", 15e4, (value) => value) },
|
|
3256
|
+
...gradient ? { fillGradient: {
|
|
3257
|
+
type: gradient.type,
|
|
3258
|
+
start: [gradient.start[0], gradient.start[1]],
|
|
3259
|
+
end: [gradient.end[0], gradient.end[1]],
|
|
3260
|
+
stops: list(gradient.stops, "gradient stops", 1e5, (stop) => fields(stop, [
|
|
3261
|
+
"offset",
|
|
3262
|
+
"color",
|
|
3263
|
+
"opacity"
|
|
3264
|
+
]))
|
|
3265
|
+
} } : {}
|
|
3266
|
+
};
|
|
3267
|
+
};
|
|
3268
|
+
const paragraph = (source) => ({
|
|
3269
|
+
...fields(source, [
|
|
3270
|
+
"start",
|
|
3271
|
+
"end",
|
|
3272
|
+
"horizontalAlign",
|
|
3273
|
+
"indentLeft",
|
|
3274
|
+
"indentRight",
|
|
3275
|
+
"indentFirst",
|
|
3276
|
+
"spaceBefore",
|
|
3277
|
+
"spaceAfter",
|
|
3278
|
+
"direction"
|
|
3279
|
+
]),
|
|
3280
|
+
lineSpacing: fields(source.lineSpacing, ["kind", "value"]),
|
|
3281
|
+
...source.bullet ? { bullet: fields(source.bullet, [
|
|
3282
|
+
"text",
|
|
3283
|
+
"fontFamily",
|
|
3284
|
+
"fontSize",
|
|
3285
|
+
"offset"
|
|
3286
|
+
]) } : {}
|
|
3287
|
+
});
|
|
3288
|
+
const text = (source) => ({
|
|
3289
|
+
...fields(source, [
|
|
3290
|
+
"plainText",
|
|
3291
|
+
"backgroundColor",
|
|
3292
|
+
"backgroundOpacity",
|
|
3293
|
+
"fontFamily",
|
|
3294
|
+
"fontSize",
|
|
3295
|
+
"color",
|
|
3296
|
+
"horizontalAlign",
|
|
3297
|
+
"verticalAlign",
|
|
3298
|
+
"width",
|
|
3299
|
+
"height"
|
|
3300
|
+
]),
|
|
3301
|
+
transform: matrix(source.transform),
|
|
3302
|
+
margins: fields(source.margins, [
|
|
3303
|
+
"left",
|
|
3304
|
+
"right",
|
|
3305
|
+
"top",
|
|
3306
|
+
"bottom"
|
|
3307
|
+
]),
|
|
3308
|
+
runs: list(source.runs, "text runs", 1e5, (run) => ({
|
|
3309
|
+
...fields(run, [
|
|
3310
|
+
"text",
|
|
3311
|
+
"fontFamily",
|
|
3312
|
+
"fontSize",
|
|
3313
|
+
"color"
|
|
3314
|
+
]),
|
|
3315
|
+
bold: !!run.bold,
|
|
3316
|
+
italic: !!run.italic,
|
|
3317
|
+
underline: !!run.underline
|
|
3318
|
+
})),
|
|
3319
|
+
...source.paragraphs === void 0 ? {} : { paragraphs: list(source.paragraphs, "paragraphs", 5e4, paragraph) }
|
|
3320
|
+
});
|
|
3321
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3322
|
+
const shapes = (source, depth = 0) => {
|
|
3323
|
+
if (depth > 64) {
|
|
3324
|
+
if (!Array.isArray(source) || source.length) throw invalid();
|
|
3325
|
+
return [];
|
|
3326
|
+
}
|
|
3327
|
+
return list(source, "shapes", 25e3, (shape) => {
|
|
3328
|
+
if (seen.has(shape)) throw invalid();
|
|
3329
|
+
seen.add(shape);
|
|
3330
|
+
return {
|
|
3331
|
+
...fields(shape, [
|
|
3332
|
+
"id",
|
|
3333
|
+
"name",
|
|
3334
|
+
"kind",
|
|
3335
|
+
"groupDisplayMode",
|
|
3336
|
+
"width",
|
|
3337
|
+
"height",
|
|
3338
|
+
"hidden"
|
|
3339
|
+
]),
|
|
3340
|
+
transform: matrix(shape.transform),
|
|
3341
|
+
geometry: list(shape.geometry, "geometry", 1e5, (geometry) => fields(geometry, [
|
|
3342
|
+
"path",
|
|
3343
|
+
"fill",
|
|
3344
|
+
"stroke"
|
|
3345
|
+
])),
|
|
3346
|
+
style: style(shape.style),
|
|
3347
|
+
text: text(shape.text),
|
|
3348
|
+
children: shapes(shape.children, depth + 1),
|
|
3349
|
+
...shape.image ? { image: image(shape.image) } : {},
|
|
3350
|
+
...shape.foreignVector ? { foreignVector: foreignVector(shape.foreignVector) } : {}
|
|
3351
|
+
};
|
|
3352
|
+
});
|
|
3353
|
+
};
|
|
3354
|
+
return {
|
|
3355
|
+
format: "vsdx",
|
|
3356
|
+
diagnostics: list(model.diagnostics, "diagnostics", 2e3, (note) => fields(note, [
|
|
3357
|
+
"code",
|
|
3358
|
+
"message",
|
|
3359
|
+
"severity"
|
|
3360
|
+
])),
|
|
3361
|
+
pages: list(model.pages, "pages", 256, (page) => ({
|
|
3362
|
+
...fields(page, [
|
|
3363
|
+
"id",
|
|
3364
|
+
"name",
|
|
3365
|
+
"width",
|
|
3366
|
+
"height",
|
|
3367
|
+
"isBackground",
|
|
3368
|
+
"backgroundPageId"
|
|
3369
|
+
]),
|
|
3370
|
+
shapes: shapes(page.shapes),
|
|
3371
|
+
connectors: []
|
|
3372
|
+
}))
|
|
3373
|
+
};
|
|
3374
|
+
}
|
|
3375
|
+
//#endregion
|
|
3376
|
+
//#region src/print-snapshot.ts
|
|
3377
|
+
/** Application guardrails, not printer capabilities or Microsoft Visio limits. */
|
|
3378
|
+
var PRINT_SNAPSHOT_LIMITS = Object.freeze({
|
|
3379
|
+
maxPages: 32,
|
|
3380
|
+
maxTotalBytes: 33554432,
|
|
3381
|
+
maxPageBytes: MAX_SVG_EXPORT_BYTES,
|
|
3382
|
+
maxShapeInstances: 1e5,
|
|
3383
|
+
maxGeometryInstances: 1e5,
|
|
3384
|
+
maxPathCharacters: 16e6,
|
|
3385
|
+
maxTextCharacters: 1e6,
|
|
3386
|
+
maxTextRuns: 1e5,
|
|
3387
|
+
maxImageBytes: 67108864,
|
|
3388
|
+
maxUniqueImagePixels: 64e6,
|
|
3389
|
+
maxImageInstancePixels: 256e6,
|
|
3390
|
+
maxVectorNodes: FOREIGN_VECTOR_SCENE_LIMITS.maxNodes,
|
|
3391
|
+
maxVectorCommands: FOREIGN_VECTOR_SCENE_LIMITS.maxCommands,
|
|
3392
|
+
maxVectorOperands: FOREIGN_VECTOR_SCENE_LIMITS.maxOperands,
|
|
3393
|
+
maxPageAxisCssPixels: 4096,
|
|
3394
|
+
maxPageAreaCssPixels: 16e6,
|
|
3395
|
+
maxTotalAreaCssPixels: 64e6,
|
|
3396
|
+
maxValidationShapeVisits: 1e6,
|
|
3397
|
+
maxValidationWork: 268435456
|
|
3398
|
+
});
|
|
3399
|
+
var LIMITATIONS = Object.freeze([
|
|
3400
|
+
"This snapshot reproduces supported saved drawing display appearance, independent of viewport zoom, pan, selection, search highlighting and temporary viewer filters. It is not a guarantee of Microsoft Visio fidelity.",
|
|
3401
|
+
"Saved print visibility, including per-layer Print and NonPrinting, is not applied. This snapshot preserves saved display visibility and does not reveal hidden content for printing.",
|
|
3402
|
+
"Saved printer settings are unsupported. Drawing dimensions are not printer paper, margins, orientation, tiling or print scale. This snapshot only prepares isolated drawing artifacts and does not print."
|
|
3403
|
+
]);
|
|
3404
|
+
var exceeded = (name) => /* @__PURE__ */ new Error(`Print snapshot exceeds the safe ${name} limit. Select fewer or smaller pages.`);
|
|
3405
|
+
function within(value, limit, name) {
|
|
3406
|
+
if (!Number.isFinite(value) || value > limit) throw exceeded(name);
|
|
3407
|
+
}
|
|
3408
|
+
function copyLimits(input) {
|
|
3409
|
+
if (input !== void 0 && (!input || typeof input !== "object" || Array.isArray(input))) throw new Error("Print snapshot limits must be an object.");
|
|
3410
|
+
const limits = { ...PRINT_SNAPSHOT_LIMITS };
|
|
3411
|
+
for (const key of Object.keys(input ?? {})) {
|
|
3412
|
+
if (!Object.hasOwn(limits, key)) throw new Error(`Unknown print snapshot limit: ${key}.`);
|
|
3413
|
+
const name = key;
|
|
3414
|
+
const value = input[name];
|
|
3415
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > PRINT_SNAPSHOT_LIMITS[name]) throw new Error(`Print snapshot ${name} must be a positive integer no greater than ${PRINT_SNAPSHOT_LIMITS[name]}.`);
|
|
3416
|
+
limits[name] = value;
|
|
3417
|
+
}
|
|
3418
|
+
return Object.freeze(limits);
|
|
3419
|
+
}
|
|
3420
|
+
/**
|
|
3421
|
+
* Synchronously prepare bounded, immutable page artifacts through the shared SVG renderer.
|
|
3422
|
+
* Requires its DOM/serializer, but never mounts nodes, downloads, fetches, opens frames or prints.
|
|
3423
|
+
* Canonical scene/raster validation is repeated on every call. No source object is frozen.
|
|
3424
|
+
* Conservative budgets include hidden content and can reject otherwise exportable individual pages.
|
|
3425
|
+
*/
|
|
3426
|
+
function createPrintSnapshot(model, options) {
|
|
3427
|
+
if (!options || typeof options !== "object" || !Array.isArray(options.pageIndices)) throw new Error("Print snapshot requires an explicit pageIndices array.");
|
|
3428
|
+
for (const key of Object.keys(options)) if (key !== "pageIndices" && key !== "limits") throw new Error(`Unknown print snapshot option: ${key}.`);
|
|
3429
|
+
const limits = copyLimits(options.limits);
|
|
3430
|
+
if (!options.pageIndices.length) throw new Error("Print snapshot page selection must not be empty.");
|
|
3431
|
+
within(options.pageIndices.length, limits.maxPages, "selected page count");
|
|
3432
|
+
const pageIndices = Object.freeze([...options.pageIndices]);
|
|
3433
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3434
|
+
for (const index of pageIndices) {
|
|
3435
|
+
if (!Number.isSafeInteger(index) || index < 0 || !Array.isArray(model.pages) || index >= model.pages.length) throw new Error("Print snapshot page index is out of range.");
|
|
3436
|
+
if (seen.has(index)) throw new Error("Print snapshot page selection contains duplicate indices.");
|
|
3437
|
+
seen.add(index);
|
|
3438
|
+
}
|
|
3439
|
+
assertViewableDocument(model);
|
|
3440
|
+
const validation = validationCost(model);
|
|
3441
|
+
const passes = 3 + 2 * pageIndices.length;
|
|
3442
|
+
const vectorPasses = 5 + 6 * pageIndices.length;
|
|
3443
|
+
const usage = {
|
|
3444
|
+
estimatedSvgBytes: 0,
|
|
3445
|
+
svgBytes: 0,
|
|
3446
|
+
shapeInstances: 0,
|
|
3447
|
+
geometryInstances: 0,
|
|
3448
|
+
pathCharacters: 0,
|
|
3449
|
+
textCharacters: 0,
|
|
3450
|
+
textRuns: 0,
|
|
3451
|
+
imageBytes: 0,
|
|
3452
|
+
uniqueImagePixels: 0,
|
|
3453
|
+
imageInstancePixels: 0,
|
|
3454
|
+
vectorNodes: 0,
|
|
3455
|
+
vectorCommands: 0,
|
|
3456
|
+
vectorOperands: 0,
|
|
3457
|
+
totalAreaCssPixels: 0,
|
|
3458
|
+
validationShapeVisits: validation.shapes * passes,
|
|
3459
|
+
validationWork: validation.work * passes + validation.vectorWork * vectorPasses
|
|
3460
|
+
};
|
|
3461
|
+
within(usage.validationShapeVisits, limits.maxValidationShapeVisits, "whole-document validation shape visits");
|
|
3462
|
+
within(usage.validationWork, limits.maxValidationWork, "whole-document validation work");
|
|
3463
|
+
model = copySnapshotScene(model);
|
|
3464
|
+
assertViewableDocument(model);
|
|
3465
|
+
const detached = validationCost(model);
|
|
3466
|
+
usage.validationShapeVisits = Math.max(validation.shapes, detached.shapes) * passes;
|
|
3467
|
+
usage.validationWork = Math.max(validation.work, detached.work) * passes + Math.max(validation.vectorWork, detached.vectorWork) * vectorPasses;
|
|
3468
|
+
within(usage.validationShapeVisits, limits.maxValidationShapeVisits, "whole-document validation shape visits");
|
|
3469
|
+
within(usage.validationWork, limits.maxValidationWork, "whole-document validation work");
|
|
3470
|
+
const add = (key, value, limit) => {
|
|
3471
|
+
usage[key] += value;
|
|
3472
|
+
within(usage[key], limit, key);
|
|
3473
|
+
};
|
|
3474
|
+
for (const index of pageIndices) {
|
|
3475
|
+
const page = model.pages[index];
|
|
3476
|
+
const width = page.width * 96, height = page.height * 96;
|
|
3477
|
+
within(width, limits.maxPageAxisCssPixels, "drawing width in CSS pixels");
|
|
3478
|
+
within(height, limits.maxPageAxisCssPixels, "drawing height in CSS pixels");
|
|
3479
|
+
within(width * height, limits.maxPageAreaCssPixels, "drawing area in CSS pixels");
|
|
3480
|
+
add("totalAreaCssPixels", width * height, limits.maxTotalAreaCssPixels);
|
|
3481
|
+
const resources = /* @__PURE__ */ new Set();
|
|
3482
|
+
for (const layer of getVisioPageLayers(model, page.id)) {
|
|
3483
|
+
const stack = [...layer.shapes];
|
|
3484
|
+
while (stack.length) {
|
|
3485
|
+
const shape = stack.pop();
|
|
3486
|
+
add("shapeInstances", 1, limits.maxShapeInstances);
|
|
3487
|
+
add("geometryInstances", shape.geometry.length, limits.maxGeometryInstances);
|
|
3488
|
+
for (const geometry of shape.geometry) add("pathCharacters", geometry.path.length, limits.maxPathCharacters);
|
|
3489
|
+
add("textCharacters", shape.text.plainText.length, limits.maxTextCharacters);
|
|
3490
|
+
add("textRuns", shape.text.runs.length, limits.maxTextRuns);
|
|
3491
|
+
for (const run of shape.text.runs) add("textCharacters", run.text.length, limits.maxTextCharacters);
|
|
3492
|
+
if (shape.foreignVector) {
|
|
3493
|
+
const vector = inspectForeignVectorResource(shape.foreignVector.vector);
|
|
3494
|
+
add("vectorNodes", vector.nodes, limits.maxVectorNodes);
|
|
3495
|
+
add("vectorCommands", vector.commands, limits.maxVectorCommands);
|
|
3496
|
+
add("vectorOperands", vector.operands, limits.maxVectorOperands);
|
|
3497
|
+
}
|
|
3498
|
+
if (shape.image) {
|
|
3499
|
+
const { bytes, pixelWidth, pixelHeight } = shape.image;
|
|
3500
|
+
add("imageInstancePixels", pixelWidth * pixelHeight, limits.maxImageInstancePixels);
|
|
3501
|
+
if (!resources.has(bytes)) {
|
|
3502
|
+
resources.add(bytes);
|
|
3503
|
+
add("imageBytes", bytes.byteLength, limits.maxImageBytes);
|
|
3504
|
+
add("uniqueImagePixels", pixelWidth * pixelHeight, limits.maxUniqueImagePixels);
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
stack.push(...shape.children);
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
add("estimatedSvgBytes", estimatePageSvgBytes(model, page, limits.maxPageBytes), limits.maxTotalBytes);
|
|
3511
|
+
}
|
|
3512
|
+
const pages = [];
|
|
3513
|
+
for (const pageIndex of pageIndices) {
|
|
3514
|
+
const pageId = model.pages[pageIndex].id;
|
|
3515
|
+
const result = exportPageSvg(model, pageIndex, { maxBytes: limits.maxPageBytes });
|
|
3516
|
+
add("svgBytes", result.byteLength, limits.maxTotalBytes);
|
|
3517
|
+
pages.push(Object.freeze({
|
|
3518
|
+
pageIndex,
|
|
3519
|
+
pageId,
|
|
3520
|
+
pageName: result.pageName,
|
|
3521
|
+
drawingSize: Object.freeze({
|
|
3522
|
+
width: result.width,
|
|
3523
|
+
height: result.height,
|
|
3524
|
+
unit: "in"
|
|
3525
|
+
}),
|
|
3526
|
+
svg: result.svg,
|
|
3527
|
+
byteLength: result.byteLength,
|
|
3528
|
+
diagnostics: Object.freeze(result.diagnostics.map((note) => Object.freeze({ ...note })))
|
|
3529
|
+
}));
|
|
3530
|
+
}
|
|
3531
|
+
return Object.freeze({
|
|
3532
|
+
appearance: "saved-display",
|
|
3533
|
+
pageIndices,
|
|
3534
|
+
pages: Object.freeze(pages),
|
|
3535
|
+
byteLength: usage.svgBytes,
|
|
3536
|
+
limits,
|
|
3537
|
+
usage: Object.freeze(usage),
|
|
3538
|
+
limitations: LIMITATIONS
|
|
3539
|
+
});
|
|
3540
|
+
}
|
|
3541
|
+
/** Cost accounting only. Format interpretation and safety checks remain canonical. */
|
|
3542
|
+
function validationCost(model) {
|
|
3543
|
+
let shapes = 0, work = model.pages.length, vectorWork = 0;
|
|
3544
|
+
const images = /* @__PURE__ */ new Set();
|
|
3545
|
+
const strings = (...values) => {
|
|
3546
|
+
for (const value of values) work += 1 + (value?.length ?? 0);
|
|
3547
|
+
};
|
|
3548
|
+
for (const note of model.diagnostics) strings(note.code, note.message);
|
|
3549
|
+
for (const page of model.pages) {
|
|
3550
|
+
strings(page.id, page.name, page.backgroundPageId);
|
|
3551
|
+
for (const layer of page.layers ?? []) strings(layer.id, layer.name);
|
|
3552
|
+
const stack = [...page.shapes];
|
|
3553
|
+
while (stack.length) {
|
|
3554
|
+
const shape = stack.pop();
|
|
3555
|
+
shapes++;
|
|
3556
|
+
work += 64;
|
|
3557
|
+
strings(shape.id, shape.name, shape.style.fill, shape.style.lineColor, shape.text.plainText, shape.text.fontFamily, shape.text.color, shape.text.backgroundColor);
|
|
3558
|
+
for (const geometry of shape.geometry) strings(geometry.path);
|
|
3559
|
+
for (const stop of shape.style.fillGradient?.stops ?? []) {
|
|
3560
|
+
work += 3;
|
|
3561
|
+
strings(stop.color);
|
|
3562
|
+
}
|
|
3563
|
+
for (const run of shape.text.runs) {
|
|
3564
|
+
work += 8;
|
|
3565
|
+
strings(run.text, run.fontFamily, run.color);
|
|
3566
|
+
}
|
|
3567
|
+
for (const paragraph of shape.text.paragraphs ?? []) {
|
|
3568
|
+
work += 16;
|
|
3569
|
+
if (paragraph.bullet) strings(paragraph.bullet.text, paragraph.bullet.fontFamily);
|
|
3570
|
+
}
|
|
3571
|
+
for (const rows of [shape.shapeData ?? [], shape.hyperlinks ?? []]) for (const row of rows) {
|
|
3572
|
+
const count = (record) => {
|
|
3573
|
+
for (const value of Object.values(record)) work += 1 + (typeof value === "string" ? value.length : 0);
|
|
3574
|
+
};
|
|
3575
|
+
count(row);
|
|
3576
|
+
if ("target" in row) count(row.target);
|
|
3577
|
+
}
|
|
3578
|
+
if (shape.image && !images.has(shape.image.bytes)) {
|
|
3579
|
+
images.add(shape.image.bytes);
|
|
3580
|
+
work += shape.image.bytes.byteLength;
|
|
3581
|
+
}
|
|
3582
|
+
if (shape.foreignVector) vectorWork += inspectForeignVectorResource(shape.foreignVector.vector).validationWork;
|
|
3583
|
+
stack.push(...shape.children);
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
return {
|
|
3587
|
+
shapes,
|
|
3588
|
+
work,
|
|
3589
|
+
vectorWork
|
|
3590
|
+
};
|
|
3591
|
+
}
|
|
3592
|
+
//#endregion
|
|
3593
|
+
//#region src/viewer-element.ts
|
|
3594
|
+
var BaseElement = typeof HTMLElement === "undefined" ? class {} : HTMLElement;
|
|
3595
|
+
var VisioViewerElement = class extends BaseElement {
|
|
3596
|
+
controller = new ViewerController(createWorkerParser());
|
|
3597
|
+
#root;
|
|
3598
|
+
#disposeInputs;
|
|
3599
|
+
#viewport;
|
|
3600
|
+
#pageSelect;
|
|
3601
|
+
#zoomLabel;
|
|
3602
|
+
#status;
|
|
3603
|
+
#diagnostics;
|
|
3604
|
+
#toolbar;
|
|
3605
|
+
#chrome;
|
|
3606
|
+
#search;
|
|
3607
|
+
#edit;
|
|
3608
|
+
#layers;
|
|
3609
|
+
#renderedLayerOverrides;
|
|
3610
|
+
#revealedSearchResult;
|
|
3611
|
+
#notes;
|
|
3612
|
+
#notesPanel;
|
|
3613
|
+
#inspector;
|
|
3614
|
+
#inspectedShape;
|
|
3615
|
+
#renderedSelection = null;
|
|
3616
|
+
#unsubscribe;
|
|
3617
|
+
#eventUnsubscribe;
|
|
3618
|
+
#document = null;
|
|
3619
|
+
#renderedPage = -1;
|
|
3620
|
+
#disposed = false;
|
|
3621
|
+
#suspended = false;
|
|
3622
|
+
#renderWarnings = [];
|
|
3623
|
+
#disposeRenderer = () => {};
|
|
3624
|
+
#fontEvents;
|
|
3625
|
+
#fontsChanged = () => {
|
|
3626
|
+
if (this.#disposed) return;
|
|
3627
|
+
this.#document = null;
|
|
3628
|
+
this.#renderedPage = -1;
|
|
3629
|
+
this.#render(this.controller.state);
|
|
3630
|
+
};
|
|
3631
|
+
constructor() {
|
|
3632
|
+
super();
|
|
3633
|
+
this.#root = this.attachShadow({ mode: "open" });
|
|
3634
|
+
this.#root.innerHTML = `<style>${viewerStyles}</style>${viewerChromeTemplate}`;
|
|
3635
|
+
this.#viewport = this.#root.querySelector(".viewport");
|
|
3636
|
+
this.#pageSelect = this.#root.querySelector("select");
|
|
3637
|
+
this.#zoomLabel = this.#root.querySelector("output");
|
|
3638
|
+
this.#status = this.#root.querySelector("[data-status]");
|
|
3639
|
+
this.#diagnostics = this.#root.querySelector("[data-diagnostics]");
|
|
3640
|
+
this.#toolbar = this.#root.querySelector(".toolbar");
|
|
3641
|
+
this.#search = searchControls(this.#root);
|
|
3642
|
+
this.#edit = new ViewerEditControls(this.#root, this.controller);
|
|
3643
|
+
this.#layers = this.#root.querySelector(".layer-controls");
|
|
3644
|
+
this.#notes = this.#root.querySelector(".notes ul");
|
|
3645
|
+
this.#notesPanel = this.#root.querySelector(".notes");
|
|
3646
|
+
this.#inspector = this.#root.querySelector(".shape-inspector");
|
|
3647
|
+
this.#chrome = new ViewerChrome(this.#root, this.controller);
|
|
3648
|
+
this.#disposeInputs = this.#wireInputs();
|
|
3649
|
+
this.#unsubscribe = this.controller.subscribe((state) => this.#render(state));
|
|
3650
|
+
this.#eventUnsubscribe = this.controller.onEvent((name, detail) => {
|
|
3651
|
+
this.dispatchEvent(new CustomEvent(name, {
|
|
3652
|
+
detail,
|
|
3653
|
+
bubbles: true,
|
|
3654
|
+
composed: true
|
|
3655
|
+
}));
|
|
3656
|
+
});
|
|
3657
|
+
}
|
|
3658
|
+
get document() {
|
|
3659
|
+
return this.controller.state.document;
|
|
3660
|
+
}
|
|
3661
|
+
set document(value) {
|
|
3662
|
+
this.controller.setDocument(value);
|
|
3663
|
+
}
|
|
3664
|
+
get pageIndex() {
|
|
3665
|
+
return this.controller.state.pageIndex;
|
|
3666
|
+
}
|
|
3667
|
+
set pageIndex(value) {
|
|
3668
|
+
this.controller.setPage(value);
|
|
3669
|
+
}
|
|
3670
|
+
get zoom() {
|
|
3671
|
+
return this.controller.state.zoom;
|
|
3672
|
+
}
|
|
3673
|
+
set zoom(value) {
|
|
3674
|
+
this.controller.setZoom(value);
|
|
3675
|
+
}
|
|
3676
|
+
get showToolbar() {
|
|
3677
|
+
return !this.#toolbar.hidden;
|
|
3678
|
+
}
|
|
3679
|
+
set showToolbar(value) {
|
|
3680
|
+
this.#assertAlive();
|
|
3681
|
+
this.#toolbar.hidden = !value;
|
|
3682
|
+
this.#root.querySelector(".zoom-controls").hidden = !value;
|
|
3683
|
+
}
|
|
3684
|
+
get renderWarnings() {
|
|
3685
|
+
return this.#renderWarnings;
|
|
3686
|
+
}
|
|
3687
|
+
async load(source) {
|
|
3688
|
+
this.#assertAlive();
|
|
3689
|
+
if (source instanceof Blob) await this.controller.loadSource(() => {
|
|
3690
|
+
if (source.size > 33554432) throw new Error("This viewer accepts files up to 32 MiB.");
|
|
3691
|
+
return source.arrayBuffer();
|
|
3692
|
+
});
|
|
3693
|
+
else await this.controller.load(source);
|
|
3694
|
+
}
|
|
3695
|
+
applyEdits(edits) {
|
|
3696
|
+
this.#assertAlive();
|
|
3697
|
+
return this.controller.applyEdits(edits);
|
|
3698
|
+
}
|
|
3699
|
+
replacePlainText(pageId, shapeId, text) {
|
|
3700
|
+
this.#assertAlive();
|
|
3701
|
+
return this.controller.replacePlainText(pageId, shapeId, text);
|
|
3702
|
+
}
|
|
3703
|
+
undo() {
|
|
3704
|
+
this.#assertAlive();
|
|
3705
|
+
return this.controller.undo();
|
|
3706
|
+
}
|
|
3707
|
+
redo() {
|
|
3708
|
+
this.#assertAlive();
|
|
3709
|
+
return this.controller.redo();
|
|
3710
|
+
}
|
|
3711
|
+
cancelEdit() {
|
|
3712
|
+
this.#assertAlive();
|
|
3713
|
+
this.#edit.reset(false);
|
|
3714
|
+
}
|
|
3715
|
+
exportVsdx() {
|
|
3716
|
+
this.#assertAlive();
|
|
3717
|
+
return this.controller.exportVsdx();
|
|
3718
|
+
}
|
|
3719
|
+
fit() {
|
|
3720
|
+
this.#assertAlive();
|
|
3721
|
+
const page = this.document?.pages[this.pageIndex];
|
|
3722
|
+
if (!page) return;
|
|
3723
|
+
const style = this.ownerDocument.defaultView?.getComputedStyle(this.#viewport);
|
|
3724
|
+
const padding = (value) => Number.parseFloat(value ?? "") || 0;
|
|
3725
|
+
const width = Math.max(1, this.#viewport.clientWidth - padding(style?.paddingLeft) - padding(style?.paddingRight));
|
|
3726
|
+
const height = Math.max(1, this.#viewport.clientHeight - padding(style?.paddingTop) - padding(style?.paddingBottom));
|
|
3727
|
+
this.zoom = Math.min(1, width / (page.width * 96), height / (page.height * 96));
|
|
3728
|
+
}
|
|
3729
|
+
setLayerVisibility(pageId, layerId, visible) {
|
|
3730
|
+
this.#assertAlive();
|
|
3731
|
+
this.controller.setLayerVisibility(pageId, layerId, visible);
|
|
3732
|
+
}
|
|
3733
|
+
resetLayerVisibility(pageId) {
|
|
3734
|
+
this.#assertAlive();
|
|
3735
|
+
this.controller.resetLayerVisibility(pageId);
|
|
3736
|
+
}
|
|
3737
|
+
/** Return a portable saved-display current-page snapshot without changing selection or downloading a file. */
|
|
3738
|
+
exportSvg(options) {
|
|
3739
|
+
this.#assertAlive();
|
|
3740
|
+
const { document, pageIndex } = this.controller.state;
|
|
3741
|
+
if (!document) throw new Error("Open a document before exporting SVG.");
|
|
3742
|
+
return exportPageSvg(document, pageIndex, options);
|
|
3743
|
+
}
|
|
3744
|
+
/** Prepare only the captured current drawing with saved display visibility. No frame, download, print dialog or state change. */
|
|
3745
|
+
createPrintSnapshot(options) {
|
|
3746
|
+
this.#assertAlive();
|
|
3747
|
+
if (options !== void 0 && (!options || typeof options !== "object" || Array.isArray(options) || "pageIndices" in options)) throw new Error("Current-page print snapshot options may only contain limits.");
|
|
3748
|
+
const { document, pageIndex } = this.controller.state;
|
|
3749
|
+
const generation = this.controller.documentGeneration;
|
|
3750
|
+
if (!document) throw new Error("Open a document before preparing a print snapshot.");
|
|
3751
|
+
const page = document.pages[pageIndex];
|
|
3752
|
+
const result = createPrintSnapshot(document, {
|
|
3753
|
+
...options,
|
|
3754
|
+
pageIndices: [pageIndex]
|
|
3755
|
+
});
|
|
3756
|
+
this.#assertAlive();
|
|
3757
|
+
if (generation !== this.controller.documentGeneration || document.pages[pageIndex] !== page) throw new Error("The document changed while preparing the print snapshot.");
|
|
3758
|
+
return result;
|
|
3759
|
+
}
|
|
3760
|
+
destroy() {
|
|
3761
|
+
if (this.#disposed) return;
|
|
3762
|
+
this.#disposed = true;
|
|
3763
|
+
this.#disposeInputs();
|
|
3764
|
+
this.#fontEvents?.removeEventListener("loadingdone", this.#fontsChanged);
|
|
3765
|
+
this.#fontEvents = void 0;
|
|
3766
|
+
this.#disposeRenderer();
|
|
3767
|
+
this.#unsubscribe();
|
|
3768
|
+
this.#eventUnsubscribe();
|
|
3769
|
+
this.controller.destroy();
|
|
3770
|
+
this.#revealedSearchResult = void 0;
|
|
3771
|
+
this.#renderedSelection = null;
|
|
3772
|
+
this.#root.replaceChildren();
|
|
3773
|
+
}
|
|
3774
|
+
connectedCallback() {
|
|
3775
|
+
if (this.#disposed) return;
|
|
3776
|
+
if (this.#suspended) this.#disposeInputs = this.#wireInputs();
|
|
3777
|
+
this.#suspended = false;
|
|
3778
|
+
this.#fontEvents = this.ownerDocument.fonts;
|
|
3779
|
+
this.#fontEvents?.addEventListener("loadingdone", this.#fontsChanged);
|
|
3780
|
+
this.#render(this.controller.state);
|
|
3781
|
+
}
|
|
3782
|
+
disconnectedCallback() {
|
|
3783
|
+
if (!this.#disposed) {
|
|
3784
|
+
this.#suspended = true;
|
|
3785
|
+
this.#disposeInputs();
|
|
3786
|
+
this.#fontEvents?.removeEventListener("loadingdone", this.#fontsChanged);
|
|
3787
|
+
this.#fontEvents = void 0;
|
|
3788
|
+
this.controller.cancelLoad();
|
|
3789
|
+
this.controller.cancelEdit();
|
|
3790
|
+
this.#disposeRenderer();
|
|
3791
|
+
this.#document = null;
|
|
3792
|
+
this.#renderedPage = -1;
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
#wireInputs() {
|
|
3796
|
+
const disposeChrome = this.#chrome.wire();
|
|
3797
|
+
const disposeEdit = this.#edit.wire();
|
|
3798
|
+
const disposeLayers = wireLayerControls(this.#layers, this.controller);
|
|
3799
|
+
const disposeInputs = wireViewerInputs({
|
|
3800
|
+
viewport: this.#viewport,
|
|
3801
|
+
commandRoot: this.#root,
|
|
3802
|
+
pageSelect: this.#pageSelect,
|
|
3803
|
+
searchInput: this.#search.input
|
|
3804
|
+
}, this.controller, () => this.fit());
|
|
3805
|
+
return () => {
|
|
3806
|
+
disposeChrome();
|
|
3807
|
+
disposeInputs();
|
|
3808
|
+
disposeLayers();
|
|
3809
|
+
disposeEdit();
|
|
3810
|
+
};
|
|
3811
|
+
}
|
|
3812
|
+
#assertAlive() {
|
|
3813
|
+
if (this.#disposed) throw new Error("The viewer has been destroyed.");
|
|
3814
|
+
}
|
|
3815
|
+
#render(state) {
|
|
3816
|
+
if (this.#suspended) return;
|
|
3817
|
+
const page = state.document?.pages[state.pageIndex];
|
|
3818
|
+
const changed = this.#document !== state.document || this.#renderedPage !== state.pageIndex || this.#renderedLayerOverrides !== state.layerVisibilityOverrides;
|
|
3819
|
+
if (changed) {
|
|
3820
|
+
this.#disposeRenderer();
|
|
3821
|
+
this.#disposeRenderer = () => {};
|
|
3822
|
+
this.#document = state.document;
|
|
3823
|
+
this.#renderedPage = state.pageIndex;
|
|
3824
|
+
this.#renderedLayerOverrides = state.layerVisibilityOverrides;
|
|
3825
|
+
this.#pageSelect.replaceChildren();
|
|
3826
|
+
for (const [index, candidate] of (state.document?.pages ?? []).entries()) {
|
|
3827
|
+
const option = document.createElement("option");
|
|
3828
|
+
option.value = String(index);
|
|
3829
|
+
option.textContent = candidate.name + (candidate.isBackground ? " (background)" : "");
|
|
3830
|
+
this.#pageSelect.append(option);
|
|
3831
|
+
}
|
|
3832
|
+
this.#pageSelect.value = String(state.pageIndex);
|
|
3833
|
+
this.#renderWarnings = [];
|
|
3834
|
+
if (state.document && page) {
|
|
3835
|
+
const result = renderPage(state.document, page, { layerVisibilityOverrides: state.layerVisibilityOverrides });
|
|
3836
|
+
this.#renderWarnings = result.warnings;
|
|
3837
|
+
this.#disposeRenderer = result.dispose;
|
|
3838
|
+
this.#viewport.replaceChildren(result.svg);
|
|
3839
|
+
} else {
|
|
3840
|
+
const empty = document.createElement("div");
|
|
3841
|
+
empty.className = "empty";
|
|
3842
|
+
const heading = document.createElement("strong");
|
|
3843
|
+
heading.textContent = "Open a Visio drawing";
|
|
3844
|
+
empty.append(heading, "Open a .vsdx file to start. Files stay in this browser.");
|
|
3845
|
+
this.#viewport.replaceChildren(empty);
|
|
3846
|
+
}
|
|
3847
|
+
const notes = compatibilityNotes(state.document?.diagnostics ?? [], this.#renderWarnings);
|
|
3848
|
+
this.#notes.replaceChildren(...notes.map((note) => {
|
|
3849
|
+
const item = document.createElement("li");
|
|
3850
|
+
item.textContent = compatibilityText(note);
|
|
3851
|
+
return item;
|
|
3852
|
+
}));
|
|
3853
|
+
this.#notesPanel.hidden = !notes.length;
|
|
3854
|
+
}
|
|
3855
|
+
const svg = this.#viewport.querySelector("svg");
|
|
3856
|
+
if (svg && page) {
|
|
3857
|
+
svg.style.width = `${page.width * 96 * state.zoom}px`;
|
|
3858
|
+
svg.style.height = `${page.height * 96 * state.zoom}px`;
|
|
3859
|
+
}
|
|
3860
|
+
const searchResult = state.search.results[state.search.activeIndex];
|
|
3861
|
+
const selection = state.selectedShape;
|
|
3862
|
+
if (changed || this.#renderedSelection?.id !== selection?.id || this.#renderedSelection?.pageId !== selection?.pageId || this.#renderedSelection?.name !== selection?.name || searchResult && searchResult !== this.#revealedSearchResult) {
|
|
3863
|
+
for (const shape of this.#viewport.querySelectorAll("[data-shape-id]")) {
|
|
3864
|
+
const selected = !!state.selectedShape && shape.dataset.shapeId === state.selectedShape.id && (!state.selectedShape.pageId || shape.dataset.pageId === state.selectedShape.pageId);
|
|
3865
|
+
shape.dataset.selected = String(selected);
|
|
3866
|
+
if (selected && searchResult && searchResult !== this.#revealedSearchResult && shape.dataset.shapeId === searchResult.shapeId && shape.dataset.pageId === searchResult.pageId) {
|
|
3867
|
+
shape.scrollIntoView?.({
|
|
3868
|
+
block: "nearest",
|
|
3869
|
+
inline: "nearest"
|
|
3870
|
+
});
|
|
3871
|
+
this.#revealedSearchResult = searchResult;
|
|
3872
|
+
}
|
|
3873
|
+
if (state.selectedShape) shape.setAttribute("tabindex", selected ? "0" : "-1");
|
|
3874
|
+
if (shape.getAttribute("role") === "button") shape.setAttribute("aria-pressed", String(selected));
|
|
3875
|
+
}
|
|
3876
|
+
const inspected = selectedShape(state.document, state.selectedShape, state.pageIndex);
|
|
3877
|
+
if (inspected !== this.#inspectedShape) {
|
|
3878
|
+
this.#inspectedShape = inspected;
|
|
3879
|
+
this.#inspector.hidden = !inspected;
|
|
3880
|
+
if (inspected) this.#inspector.open = true;
|
|
3881
|
+
this.#inspector.querySelector("div").replaceChildren(...inspected ? [shapeDetails(inspected)] : []);
|
|
3882
|
+
}
|
|
3883
|
+
this.#renderedSelection = selection ? { ...selection } : null;
|
|
3884
|
+
}
|
|
3885
|
+
if (!searchResult) this.#revealedSearchResult = void 0;
|
|
3886
|
+
this.#zoomLabel.value = `${Math.round(state.zoom * 100)}%`;
|
|
3887
|
+
this.#pageSelect.disabled = !page;
|
|
3888
|
+
for (const button of this.#root.querySelectorAll("[data-action]")) button.disabled = !page;
|
|
3889
|
+
renderSearchControls(this.#search, state);
|
|
3890
|
+
this.#edit.render(state);
|
|
3891
|
+
if (changed) renderLayerControls(this.#layers, state);
|
|
3892
|
+
else this.#layers.querySelector("[data-layer-reset=\"all\"]").disabled = state.layerVisibilityOverrides.length === 0;
|
|
3893
|
+
this.#chrome.render(state, this.#notes.children.length);
|
|
3894
|
+
this.#viewport.setAttribute("aria-busy", String(state.loading || state.edit.busy));
|
|
3895
|
+
this.#status.textContent = state.loading ? "Opening diagram…" : state.error?.message ?? (page ? `${page.name} · ${page.shapes.length} top-level shapes` : "No diagram open");
|
|
3896
|
+
const warnings = (state.document?.diagnostics.length ?? 0) + this.#renderWarnings.length;
|
|
3897
|
+
this.#diagnostics.textContent = warnings ? `${this.#notes.children.length} compatibility notes` : "Local-only viewing";
|
|
3898
|
+
}
|
|
3899
|
+
};
|
|
3900
|
+
function registerVisioViewer() {
|
|
3901
|
+
if (typeof customElements === "undefined") throw new Error("Register the Visio viewer in a browser.");
|
|
3902
|
+
if (!customElements.get("visio-viewer")) customElements.define("visio-viewer", VisioViewerElement);
|
|
3903
|
+
}
|
|
3904
|
+
//#endregion
|
|
3905
|
+
//#region src/binding.ts
|
|
3906
|
+
/** One client-only lifecycle adapter; framework wrappers forward to this implementation. */
|
|
3907
|
+
function mountViewer(container, initial = {}) {
|
|
3908
|
+
registerVisioViewer();
|
|
3909
|
+
const element = document.createElement("visio-viewer");
|
|
3910
|
+
let destroyed = false;
|
|
3911
|
+
let updateRevision = 0;
|
|
3912
|
+
let callbacks = {};
|
|
3913
|
+
const listeners = eventKeys.map((name) => {
|
|
3914
|
+
const listener = (event) => {
|
|
3915
|
+
const callback = callbacks[name];
|
|
3916
|
+
callback?.(event.detail);
|
|
3917
|
+
};
|
|
3918
|
+
element.addEventListener(name, listener);
|
|
3919
|
+
return {
|
|
3920
|
+
name,
|
|
3921
|
+
listener
|
|
3922
|
+
};
|
|
3923
|
+
});
|
|
3924
|
+
function assertAlive() {
|
|
3925
|
+
if (destroyed) throw new Error("The viewer has been destroyed.");
|
|
3926
|
+
}
|
|
3927
|
+
function update(options) {
|
|
3928
|
+
assertAlive();
|
|
3929
|
+
const revision = ++updateRevision;
|
|
3930
|
+
if ("events" in options) callbacks = options.events ?? {};
|
|
3931
|
+
for (const key of propertyKeys) {
|
|
3932
|
+
if (destroyed || revision !== updateRevision) break;
|
|
3933
|
+
if (!(key in options)) continue;
|
|
3934
|
+
if (key === "document" && options.document !== void 0) {
|
|
3935
|
+
if (element.document !== options.document) element.document = options.document;
|
|
3936
|
+
} else if (key === "pageIndex" && options.pageIndex !== void 0) element.pageIndex = options.pageIndex;
|
|
3937
|
+
else if (key === "zoom" && options.zoom !== void 0) element.zoom = options.zoom;
|
|
3938
|
+
else if (key === "showToolbar" && options.showToolbar !== void 0) element.showToolbar = options.showToolbar;
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
update(initial);
|
|
3942
|
+
container.append(element);
|
|
3943
|
+
return {
|
|
3944
|
+
element,
|
|
3945
|
+
controller: element.controller,
|
|
3946
|
+
update,
|
|
3947
|
+
async load(source) {
|
|
3948
|
+
assertAlive();
|
|
3949
|
+
await element.load(source);
|
|
3950
|
+
},
|
|
3951
|
+
async applyEdits(edits) {
|
|
3952
|
+
assertAlive();
|
|
3953
|
+
await element.applyEdits(edits);
|
|
3954
|
+
},
|
|
3955
|
+
async replacePlainText(pageId, shapeId, text) {
|
|
3956
|
+
assertAlive();
|
|
3957
|
+
await element.replacePlainText(pageId, shapeId, text);
|
|
3958
|
+
},
|
|
3959
|
+
async undo() {
|
|
3960
|
+
assertAlive();
|
|
3961
|
+
await element.undo();
|
|
3962
|
+
},
|
|
3963
|
+
async redo() {
|
|
3964
|
+
assertAlive();
|
|
3965
|
+
await element.redo();
|
|
3966
|
+
},
|
|
3967
|
+
cancelEdit() {
|
|
3968
|
+
assertAlive();
|
|
3969
|
+
element.cancelEdit();
|
|
3970
|
+
},
|
|
3971
|
+
exportVsdx() {
|
|
3972
|
+
assertAlive();
|
|
3973
|
+
return element.exportVsdx();
|
|
3974
|
+
},
|
|
3975
|
+
fit() {
|
|
3976
|
+
assertAlive();
|
|
3977
|
+
element.fit();
|
|
3978
|
+
},
|
|
3979
|
+
setLayerVisibility(pageId, layerId, visible) {
|
|
3980
|
+
assertAlive();
|
|
3981
|
+
element.setLayerVisibility(pageId, layerId, visible);
|
|
3982
|
+
},
|
|
3983
|
+
resetLayerVisibility(pageId) {
|
|
3984
|
+
assertAlive();
|
|
3985
|
+
element.resetLayerVisibility(pageId);
|
|
3986
|
+
},
|
|
3987
|
+
exportSvg(options) {
|
|
3988
|
+
assertAlive();
|
|
3989
|
+
return element.exportSvg(options);
|
|
3990
|
+
},
|
|
3991
|
+
createPrintSnapshot(options) {
|
|
3992
|
+
assertAlive();
|
|
3993
|
+
return element.createPrintSnapshot(options);
|
|
3994
|
+
},
|
|
3995
|
+
destroy() {
|
|
3996
|
+
if (destroyed) return;
|
|
3997
|
+
destroyed = true;
|
|
3998
|
+
callbacks = {};
|
|
3999
|
+
for (const { name, listener } of listeners) element.removeEventListener(name, listener);
|
|
4000
|
+
element.destroy();
|
|
4001
|
+
element.remove();
|
|
4002
|
+
}
|
|
4003
|
+
};
|
|
4004
|
+
}
|
|
4005
|
+
//#endregion
|
|
4006
|
+
//#region packages/bindings/src/common.ts
|
|
4007
|
+
/** One mapping for every property and event, shared by every framework adapter. */
|
|
4008
|
+
function viewerOptions(props) {
|
|
4009
|
+
const result = { events: props.events ?? {} };
|
|
4010
|
+
for (const key of propertyKeys) {
|
|
4011
|
+
const value = props[key];
|
|
4012
|
+
if (value !== void 0) Object.assign(result, { [key]: value });
|
|
4013
|
+
}
|
|
4014
|
+
return result;
|
|
4015
|
+
}
|
|
4016
|
+
/**
|
|
4017
|
+
* Native props are snapshots, while the browser binding accepts explicit patches. Forward only
|
|
4018
|
+
* changed properties so a callback/class update cannot undo an imperative load or user zoom.
|
|
4019
|
+
*/
|
|
4020
|
+
function mountFrameworkViewer(container, initial) {
|
|
4021
|
+
const binding = mountViewer(container, initial);
|
|
4022
|
+
let previous = { ...initial };
|
|
4023
|
+
let destroyed = false;
|
|
4024
|
+
let updateRevision = 0;
|
|
4025
|
+
const activePatches = /* @__PURE__ */ new Set();
|
|
4026
|
+
return {
|
|
4027
|
+
...binding,
|
|
4028
|
+
update(next) {
|
|
4029
|
+
if (destroyed) throw new Error("The viewer has been destroyed.");
|
|
4030
|
+
const revision = ++updateRevision;
|
|
4031
|
+
const snapshot = { ...next };
|
|
4032
|
+
const patch = { events: snapshot.events ?? {} };
|
|
4033
|
+
for (const key of propertyKeys) {
|
|
4034
|
+
const touchedByOlderCall = [...activePatches].some((active) => key in active);
|
|
4035
|
+
if (snapshot[key] !== void 0 && (snapshot[key] !== previous[key] || touchedByOlderCall)) Object.assign(patch, { [key]: snapshot[key] });
|
|
4036
|
+
}
|
|
4037
|
+
activePatches.add(patch);
|
|
4038
|
+
try {
|
|
4039
|
+
binding.update(patch);
|
|
4040
|
+
} finally {
|
|
4041
|
+
activePatches.delete(patch);
|
|
4042
|
+
}
|
|
4043
|
+
if (!destroyed && revision === updateRevision) previous = snapshot;
|
|
4044
|
+
},
|
|
4045
|
+
destroy() {
|
|
4046
|
+
if (destroyed) return;
|
|
4047
|
+
destroyed = true;
|
|
4048
|
+
previous = {};
|
|
4049
|
+
binding.destroy();
|
|
4050
|
+
}
|
|
4051
|
+
};
|
|
4052
|
+
}
|
|
4053
|
+
/** Lazy imperative methods reject both before mount and after unmount. */
|
|
4054
|
+
function viewerHandle(current) {
|
|
4055
|
+
const requireViewer = () => {
|
|
4056
|
+
const binding = current();
|
|
4057
|
+
if (!binding) throw new Error("The viewer is not mounted.");
|
|
4058
|
+
return binding;
|
|
4059
|
+
};
|
|
4060
|
+
return {
|
|
4061
|
+
get element() {
|
|
4062
|
+
return requireViewer().element;
|
|
4063
|
+
},
|
|
4064
|
+
get controller() {
|
|
4065
|
+
return requireViewer().controller;
|
|
4066
|
+
},
|
|
4067
|
+
async load(source) {
|
|
4068
|
+
await requireViewer().load(source);
|
|
4069
|
+
},
|
|
4070
|
+
async applyEdits(edits) {
|
|
4071
|
+
await requireViewer().applyEdits(edits);
|
|
4072
|
+
},
|
|
4073
|
+
async replacePlainText(pageId, shapeId, text) {
|
|
4074
|
+
await requireViewer().replacePlainText(pageId, shapeId, text);
|
|
4075
|
+
},
|
|
4076
|
+
async undo() {
|
|
4077
|
+
await requireViewer().undo();
|
|
4078
|
+
},
|
|
4079
|
+
async redo() {
|
|
4080
|
+
await requireViewer().redo();
|
|
4081
|
+
},
|
|
4082
|
+
cancelEdit() {
|
|
4083
|
+
requireViewer().cancelEdit();
|
|
4084
|
+
},
|
|
4085
|
+
exportVsdx() {
|
|
4086
|
+
return requireViewer().exportVsdx();
|
|
4087
|
+
},
|
|
4088
|
+
fit() {
|
|
4089
|
+
requireViewer().fit();
|
|
4090
|
+
},
|
|
4091
|
+
setLayerVisibility(pageId, layerId, visible) {
|
|
4092
|
+
requireViewer().setLayerVisibility(pageId, layerId, visible);
|
|
4093
|
+
},
|
|
4094
|
+
resetLayerVisibility(pageId) {
|
|
4095
|
+
requireViewer().resetLayerVisibility(pageId);
|
|
4096
|
+
},
|
|
4097
|
+
exportSvg(options) {
|
|
4098
|
+
return requireViewer().exportSvg(options);
|
|
4099
|
+
},
|
|
4100
|
+
createPrintSnapshot(options) {
|
|
4101
|
+
return requireViewer().createPrintSnapshot(options);
|
|
4102
|
+
}
|
|
4103
|
+
};
|
|
4104
|
+
}
|
|
4105
|
+
//#endregion
|
|
4106
|
+
//#region packages/bindings/src/react.ts
|
|
4107
|
+
/** React owns only the host and lifecycle; the shared element owns all viewer behavior. */
|
|
4108
|
+
var VisioViewer = forwardRef(function VisioViewer(props, ref) {
|
|
4109
|
+
const host = useRef(null);
|
|
4110
|
+
const binding = useRef(void 0);
|
|
4111
|
+
useEffect(() => {
|
|
4112
|
+
const mounted = mountFrameworkViewer(host.current, viewerOptions(props));
|
|
4113
|
+
binding.current = mounted;
|
|
4114
|
+
return () => {
|
|
4115
|
+
binding.current = void 0;
|
|
4116
|
+
mounted.destroy();
|
|
4117
|
+
};
|
|
4118
|
+
}, []);
|
|
4119
|
+
useEffect(() => {
|
|
4120
|
+
binding.current?.update(viewerOptions(props));
|
|
4121
|
+
});
|
|
4122
|
+
useImperativeHandle(ref, () => viewerHandle(() => binding.current), []);
|
|
4123
|
+
return createElement("div", {
|
|
4124
|
+
ref: host,
|
|
4125
|
+
className: props.className,
|
|
4126
|
+
style: props.style,
|
|
4127
|
+
"aria-label": props["aria-label"]
|
|
4128
|
+
});
|
|
4129
|
+
});
|
|
4130
|
+
//#endregion
|
|
4131
|
+
export { MAX_SVG_EXPORT_BYTES, PRINT_SNAPSHOT_LIMITS, TEXT_SEARCH_LIMITS, VIEWER_LAYER_LIMITS, ViewerController, VisioViewer, VisioViewerElement, compatibilityNotes, compatibilityText, createPrintSnapshot, createWorkerParser, eventKeys, exportPageSvg, mountViewer, propertyKeys, registerVisioViewer, renderPage };
|