@formicoidea/labre-core 0.50.0 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_pkgs/affine-block-attachment/asset-picker.d.ts +24 -0
- package/dist/_pkgs/affine-block-attachment/asset-picker.js +87 -0
- package/dist/_pkgs/affine-block-attachment/configs/slash-menu.js +3 -20
- package/dist/_pkgs/affine-block-attachment/index.d.ts +1 -0
- package/dist/_pkgs/affine-block-attachment/index.js +1 -0
- package/dist/_pkgs/affine-block-attachment/utils.d.ts +9 -1
- package/dist/_pkgs/affine-block-attachment/utils.js +9 -0
- package/dist/_pkgs/affine-block-bookmark/adapters/markdown/markdown.js +14 -1
- package/dist/_pkgs/affine-block-bookmark/adapters/notion-html.js +5 -1
- package/dist/_pkgs/affine-block-database/adapters/markdown.js +5 -3
- package/dist/_pkgs/affine-block-embed/common/adapters/html.js +5 -3
- package/dist/_pkgs/affine-block-embed/common/adapters/markdown.js +11 -0
- package/dist/_pkgs/affine-block-frame/frame-template.d.ts +58 -0
- package/dist/_pkgs/affine-block-frame/frame-template.js +207 -0
- package/dist/_pkgs/affine-block-frame/frame-toolbar.js +19 -3
- package/dist/_pkgs/affine-block-frame/index.d.ts +1 -0
- package/dist/_pkgs/affine-block-frame/index.js +1 -0
- package/dist/_pkgs/affine-block-frame/translations.d.ts +7 -0
- package/dist/_pkgs/affine-block-frame/translations.js +16 -1
- package/dist/_pkgs/affine-block-frame/view.js +6 -0
- package/dist/_pkgs/affine-block-image/asset-picker.d.ts +63 -0
- package/dist/_pkgs/affine-block-image/asset-picker.js +183 -0
- package/dist/_pkgs/affine-block-image/commands/insert-images.d.ts +5 -0
- package/dist/_pkgs/affine-block-image/commands/insert-images.js +8 -8
- package/dist/_pkgs/affine-block-image/components/page-image-block.d.ts +11 -0
- package/dist/_pkgs/affine-block-image/components/page-image-block.js +81 -12
- package/dist/_pkgs/affine-block-image/configs/replace-action.d.ts +9 -0
- package/dist/_pkgs/affine-block-image/configs/replace-action.js +27 -0
- package/dist/_pkgs/affine-block-image/configs/toolbar.js +6 -0
- package/dist/_pkgs/affine-block-image/crop/crop-controller.d.ts +46 -0
- package/dist/_pkgs/affine-block-image/crop/crop-controller.js +104 -0
- package/dist/_pkgs/affine-block-image/crop/crop-math.d.ts +70 -0
- package/dist/_pkgs/affine-block-image/crop/crop-math.js +141 -0
- package/dist/_pkgs/affine-block-image/crop/crop-overlay.d.ts +57 -0
- package/dist/_pkgs/affine-block-image/crop/crop-overlay.js +343 -0
- package/dist/_pkgs/affine-block-image/crop/crop-toolbar.d.ts +12 -0
- package/dist/_pkgs/affine-block-image/crop/crop-toolbar.js +78 -0
- package/dist/_pkgs/affine-block-image/crop/write-crop.d.ts +16 -0
- package/dist/_pkgs/affine-block-image/crop/write-crop.js +57 -0
- package/dist/_pkgs/affine-block-image/edgeless-clipboard-config.js +4 -1
- package/dist/_pkgs/affine-block-image/effects.js +2 -0
- package/dist/_pkgs/affine-block-image/image-block.d.ts +2 -0
- package/dist/_pkgs/affine-block-image/image-block.js +2 -0
- package/dist/_pkgs/affine-block-image/image-edgeless-block.d.ts +2 -0
- package/dist/_pkgs/affine-block-image/image-edgeless-block.js +36 -8
- package/dist/_pkgs/affine-block-image/image-resize-manager.js +15 -0
- package/dist/_pkgs/affine-block-image/index.d.ts +1 -0
- package/dist/_pkgs/affine-block-image/index.js +1 -0
- package/dist/_pkgs/affine-block-image/translations.d.ts +15 -0
- package/dist/_pkgs/affine-block-image/translations.js +43 -0
- package/dist/_pkgs/affine-block-image/utils.d.ts +13 -0
- package/dist/_pkgs/affine-block-image/utils.js +10 -1
- package/dist/_pkgs/affine-block-root/clipboard/page-clipboard.js +5 -0
- package/dist/_pkgs/affine-block-root/commands/page-export-commands.d.ts +2 -0
- package/dist/_pkgs/affine-block-root/commands/page-export-commands.js +71 -0
- package/dist/_pkgs/affine-block-root/index.d.ts +1 -0
- package/dist/_pkgs/affine-block-root/index.js +1 -0
- package/dist/_pkgs/affine-block-root/view.js +8 -0
- package/dist/_pkgs/affine-block-surface/extensions/drawio-export/capability.d.ts +30 -0
- package/dist/_pkgs/affine-block-surface/extensions/drawio-export/capability.js +44 -0
- package/dist/_pkgs/affine-block-surface/extensions/drawio-export/colors.d.ts +26 -0
- package/dist/_pkgs/affine-block-surface/extensions/drawio-export/colors.js +82 -0
- package/dist/_pkgs/affine-block-surface/extensions/drawio-export/command.d.ts +18 -0
- package/dist/_pkgs/affine-block-surface/extensions/drawio-export/command.js +91 -0
- package/dist/_pkgs/affine-block-surface/extensions/drawio-export/index.d.ts +3 -0
- package/dist/_pkgs/affine-block-surface/extensions/drawio-export/index.js +3 -0
- package/dist/_pkgs/affine-block-surface/extensions/drawio-export/writer.d.ts +50 -0
- package/dist/_pkgs/affine-block-surface/extensions/drawio-export/writer.js +732 -0
- package/dist/_pkgs/affine-block-surface/extensions/export-manager/draw-image-block.d.ts +12 -0
- package/dist/_pkgs/affine-block-surface/extensions/export-manager/draw-image-block.js +15 -0
- package/dist/_pkgs/affine-block-surface/extensions/export-manager/export-manager.js +7 -1
- package/dist/_pkgs/affine-block-surface/extensions/export-svg/command.d.ts +7 -1
- package/dist/_pkgs/affine-block-surface/extensions/export-svg/command.js +60 -1
- package/dist/_pkgs/affine-block-surface/extensions/export-svg/index.d.ts +2 -2
- package/dist/_pkgs/affine-block-surface/extensions/export-svg/index.js +2 -2
- package/dist/_pkgs/affine-block-surface/extensions/export-svg/marker-version.d.ts +6 -0
- package/dist/_pkgs/affine-block-surface/extensions/export-svg/marker-version.js +8 -0
- package/dist/_pkgs/affine-block-surface/extensions/export-svg/render.d.ts +28 -1
- package/dist/_pkgs/affine-block-surface/extensions/export-svg/render.js +66 -6
- package/dist/_pkgs/affine-block-surface/extensions/export-svg/svg-context.d.ts +2 -7
- package/dist/_pkgs/affine-block-surface/extensions/export-svg/svg-context.js +2 -6
- package/dist/_pkgs/affine-block-surface/extensions/index.d.ts +2 -0
- package/dist/_pkgs/affine-block-surface/extensions/index.js +2 -0
- package/dist/_pkgs/affine-block-surface/extensions/interchange-export.d.ts +84 -0
- package/dist/_pkgs/affine-block-surface/extensions/interchange-export.js +150 -0
- package/dist/_pkgs/affine-block-surface/extensions/interchange-import.js +24 -2
- package/dist/_pkgs/affine-block-surface/extensions/interchange.d.ts +23 -2
- package/dist/_pkgs/affine-block-surface/extensions/interchange.js +4 -1
- package/dist/_pkgs/affine-block-surface/extensions/legend-role.d.ts +18 -0
- package/dist/_pkgs/affine-block-surface/extensions/legend-role.js +20 -0
- package/dist/_pkgs/affine-block-surface/extensions/legend.d.ts +2 -19
- package/dist/_pkgs/affine-block-surface/extensions/legend.js +2 -18
- package/dist/_pkgs/affine-block-surface/extensions/svg-sanitize.d.ts +67 -0
- package/dist/_pkgs/affine-block-surface/extensions/svg-sanitize.js +235 -0
- package/dist/_pkgs/affine-block-surface/extensions/svg-sketch-translations.d.ts +1 -0
- package/dist/_pkgs/affine-block-surface/extensions/svg-sketch-translations.js +9 -0
- package/dist/_pkgs/affine-block-surface/extensions/svg-sketch.d.ts +17 -13
- package/dist/_pkgs/affine-block-surface/extensions/svg-sketch.js +52 -80
- package/dist/_pkgs/affine-block-surface/extensions/xml-element.d.ts +98 -0
- package/dist/_pkgs/affine-block-surface/extensions/xml-element.js +261 -0
- package/dist/_pkgs/affine-block-surface/extensions/xml-reader.d.ts +238 -0
- package/dist/_pkgs/affine-block-surface/extensions/xml-reader.js +570 -0
- package/dist/_pkgs/affine-block-surface/framework-background/def.js +1 -1
- package/dist/_pkgs/affine-block-surface/index.d.ts +1 -0
- package/dist/_pkgs/affine-block-surface/index.js +1 -0
- package/dist/_pkgs/affine-block-surface/interchange-headless.d.ts +29 -0
- package/dist/_pkgs/affine-block-surface/interchange-headless.js +32 -0
- package/dist/_pkgs/affine-block-surface/view.js +22 -2
- package/dist/_pkgs/affine-block-table/adapters/utils.js +24 -1
- package/dist/_pkgs/affine-gfx-note/toolbar/note-menu.js +5 -8
- package/dist/_pkgs/affine-gfx-template/contribute.d.ts +14 -4
- package/dist/_pkgs/affine-gfx-template/contribute.js +47 -6
- package/dist/_pkgs/affine-gfx-template/toolbar/builtin-templates.d.ts +7 -2
- package/dist/_pkgs/affine-gfx-template/toolbar/builtin-templates.js +14 -40
- package/dist/_pkgs/affine-gfx-template/toolbar/resolve-name.d.ts +1 -1
- package/dist/_pkgs/affine-gfx-template/toolbar/resolve-name.js +1 -1
- package/dist/_pkgs/affine-gfx-template/toolbar/template-panel.d.ts +4 -4
- package/dist/_pkgs/affine-gfx-template/toolbar/template-panel.js +9 -9
- package/dist/_pkgs/affine-gfx-template/toolbar/template-type.d.ts +2 -4
- package/dist/_pkgs/affine-gfx-template/translations.d.ts +5 -0
- package/dist/_pkgs/affine-gfx-template/translations.js +9 -0
- package/dist/_pkgs/affine-inline-footnote/adapters/markdown/inline-delta.js +6 -0
- package/dist/_pkgs/affine-inline-footnote/adapters/markdown/markdown-inline.js +7 -0
- package/dist/_pkgs/affine-inline-link/adapters/html/html-inline.js +4 -1
- package/dist/_pkgs/affine-inline-link/adapters/html/inline-delta.js +3 -1
- package/dist/_pkgs/affine-inline-link/adapters/markdown/inline-delta.js +4 -1
- package/dist/_pkgs/affine-inline-link/adapters/notion-html/html-inline.js +2 -1
- package/dist/_pkgs/affine-inline-link/link-node/affine-link.d.ts +7 -0
- package/dist/_pkgs/affine-inline-link/link-node/affine-link.js +37 -0
- package/dist/_pkgs/affine-inline-link/link-node/index.d.ts +1 -0
- package/dist/_pkgs/affine-inline-link/link-node/index.js +1 -0
- package/dist/_pkgs/affine-inline-link/link-node/propose-views.d.ts +22 -0
- package/dist/_pkgs/affine-inline-link/link-node/propose-views.js +59 -0
- package/dist/_pkgs/affine-model/blocks/image/image-crop.d.ts +48 -0
- package/dist/_pkgs/affine-model/blocks/image/image-crop.js +78 -0
- package/dist/_pkgs/affine-model/blocks/image/image-model.d.ts +6 -0
- package/dist/_pkgs/affine-model/blocks/image/image-model.js +2 -0
- package/dist/_pkgs/affine-model/blocks/image/index.d.ts +1 -0
- package/dist/_pkgs/affine-model/blocks/image/index.js +1 -0
- package/dist/_pkgs/affine-model/consts/shape.d.ts +2 -0
- package/dist/_pkgs/affine-model/consts/shape.js +5 -0
- package/dist/_pkgs/affine-model/elements/bpmn/consts.d.ts +31 -0
- package/dist/_pkgs/affine-model/elements/bpmn/consts.js +31 -0
- package/dist/_pkgs/affine-model/elements/bpmn/index.d.ts +1 -0
- package/dist/_pkgs/affine-model/elements/bpmn/index.js +1 -0
- package/dist/_pkgs/affine-model/elements/bpmn/pool.d.ts +0 -25
- package/dist/_pkgs/affine-model/elements/bpmn/pool.js +1 -25
- package/dist/_pkgs/affine-model/elements/shape/shape.d.ts +0 -2
- package/dist/_pkgs/affine-model/elements/shape/shape.js +1 -2
- package/dist/_pkgs/affine-model/elements/uml/consts.d.ts +133 -0
- package/dist/_pkgs/affine-model/elements/uml/consts.js +107 -0
- package/dist/_pkgs/affine-model/elements/uml/diagram.d.ts +1 -79
- package/dist/_pkgs/affine-model/elements/uml/diagram.js +1 -53
- package/dist/_pkgs/affine-model/elements/uml/fragment.d.ts +0 -15
- package/dist/_pkgs/affine-model/elements/uml/fragment.js +1 -15
- package/dist/_pkgs/affine-model/elements/uml/index.d.ts +1 -0
- package/dist/_pkgs/affine-model/elements/uml/index.js +1 -0
- package/dist/_pkgs/affine-model/elements/uml/partition.d.ts +0 -20
- package/dist/_pkgs/affine-model/elements/uml/partition.js +1 -20
- package/dist/_pkgs/affine-model/elements/uml/region.d.ts +0 -11
- package/dist/_pkgs/affine-model/elements/uml/region.js +1 -11
- package/dist/_pkgs/affine-model/headless.d.ts +19 -0
- package/dist/_pkgs/affine-model/headless.js +19 -0
- package/dist/_pkgs/affine-shared/headless.d.ts +10 -0
- package/dist/_pkgs/affine-shared/headless.js +10 -0
- package/dist/_pkgs/affine-shared/services/asset-picker-service.d.ts +63 -0
- package/dist/_pkgs/affine-shared/services/asset-picker-service.js +20 -0
- package/dist/_pkgs/affine-shared/services/frame-template-service.d.ts +83 -0
- package/dist/_pkgs/affine-shared/services/frame-template-service.js +17 -0
- package/dist/_pkgs/affine-shared/services/index.d.ts +3 -0
- package/dist/_pkgs/affine-shared/services/index.js +3 -0
- package/dist/_pkgs/affine-shared/services/thread-marks-service.d.ts +69 -0
- package/dist/_pkgs/affine-shared/services/thread-marks-service.js +20 -0
- package/dist/_pkgs/affine-shared/services/translation-service/chrome.d.ts +6 -0
- package/dist/_pkgs/affine-shared/services/translation-service/chrome.js +10 -0
- package/dist/_pkgs/affine-widget-drag-handle/watchers/drag-event-watcher.js +8 -3
- package/dist/_pkgs/affine-widget-keyboard-toolbar/config.js +4 -16
- package/dist/_pkgs/affine-widget-linked-doc/transformers/html.d.ts +7 -0
- package/dist/_pkgs/affine-widget-linked-doc/transformers/html.js +25 -24
- package/dist/_pkgs/affine-widget-linked-doc/transformers/index.d.ts +1 -1
- package/dist/_pkgs/affine-widget-linked-doc/transformers/markdown.d.ts +7 -0
- package/dist/_pkgs/affine-widget-linked-doc/transformers/markdown.js +26 -25
- package/dist/_pkgs/affine-widget-linked-doc/transformers/utils.d.ts +23 -0
- package/dist/_pkgs/affine-widget-linked-doc/transformers/utils.js +14 -0
- package/dist/_pkgs/affine-widget-thread-marks/effects.d.ts +1 -0
- package/dist/_pkgs/affine-widget-thread-marks/effects.js +4 -0
- package/dist/_pkgs/affine-widget-thread-marks/excerpt.d.ts +19 -0
- package/dist/_pkgs/affine-widget-thread-marks/excerpt.js +68 -0
- package/dist/_pkgs/affine-widget-thread-marks/index.d.ts +5 -0
- package/dist/_pkgs/affine-widget-thread-marks/index.js +8 -0
- package/dist/_pkgs/affine-widget-thread-marks/toolbar.d.ts +49 -0
- package/dist/_pkgs/affine-widget-thread-marks/toolbar.js +108 -0
- package/dist/_pkgs/affine-widget-thread-marks/translations.d.ts +9 -0
- package/dist/_pkgs/affine-widget-thread-marks/translations.js +11 -0
- package/dist/_pkgs/affine-widget-thread-marks/view.d.ts +12 -0
- package/dist/_pkgs/affine-widget-thread-marks/view.js +25 -0
- package/dist/_pkgs/affine-widget-thread-marks/widget.d.ts +51 -0
- package/dist/_pkgs/affine-widget-thread-marks/widget.js +214 -0
- package/dist/commands.js +5 -2
- package/dist/extensions/view.js +2 -0
- package/dist/host-panels.d.ts +24 -1
- package/dist/host-panels.js +61 -10
- package/dist/interchange.d.ts +1 -0
- package/dist/interchange.js +1 -0
- package/dist/model/headless.d.ts +1 -0
- package/dist/model/headless.js +1 -0
- package/dist/shared/headless.d.ts +1 -0
- package/dist/shared/headless.js +1 -0
- package/dist/translations.js +10 -1
- package/package.json +13 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { ShapeStyle, ShapeType, StrokeStyle, TextAlign, } from '../../affine-model/
|
|
2
|
-
import DOMPurify from 'dompurify';
|
|
1
|
+
import { ShapeStyle, ShapeType, StrokeStyle, TextAlign, } from '../../affine-model/headless.js';
|
|
3
2
|
import { InterchangeImportError, } from './interchange.js';
|
|
4
|
-
import {
|
|
3
|
+
import { MAX_SVG_DEPTH, sanitizeSvgNode } from './svg-sanitize.js';
|
|
4
|
+
import { SVG_SKETCH_CURRENT_COLOR, SVG_SKETCH_CURVE, SVG_SKETCH_EMPTY, SVG_SKETCH_EMPTY_BOX, SVG_SKETCH_EMPTY_TEXT, SVG_SKETCH_ERROR_MALFORMED_XML, SVG_SKETCH_ERROR_NOT_SVG, SVG_SKETCH_ERROR_SANITIZED_AWAY, SVG_SKETCH_FLAT_POLYGON, SVG_SKETCH_FONT_UNIT, SVG_SKETCH_HIDDEN_DISPLAY, SVG_SKETCH_HIDDEN_VISIBILITY, SVG_SKETCH_LONE_POINT, SVG_SKETCH_OPACITY, SVG_SKETCH_PAINT_SERVER, SVG_SKETCH_PERCENT_RADIUS, SVG_SKETCH_REMOVED, SVG_SKETCH_REMOVED_ATTRIBUTE, SVG_SKETCH_SKIPPED, SVG_SKETCH_SWITCH, SVG_SKETCH_TOO_DEEP, SVG_SKETCH_TRANSFORM, } from './svg-sketch-translations.js';
|
|
5
|
+
import { readXml } from './xml-reader.js';
|
|
5
6
|
/**
|
|
6
7
|
* **SVG → a canvas sketch.** The platform's VISUAL-tier importer, shared by
|
|
7
8
|
* every framework that declares it (`docs/adr/0012`, P2).
|
|
@@ -99,27 +100,14 @@ export const SVG_SKETCH_EXTENSION = '.svg';
|
|
|
99
100
|
export const SVG_SKETCH_MIME = 'image/svg+xml';
|
|
100
101
|
/* ── Small readers ────────────────────────────────────────────────────── */
|
|
101
102
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* an `HTMLCollection` whose behaviour on a foreign-namespaced document is one
|
|
107
|
-
* more thing that differs between the two environments.
|
|
103
|
+
* The walk reads the sanitizer's tree (`svg-sanitize.ts`), a DOM-shaped view
|
|
104
|
+
* over `readXml` rather than a DOM, so a server reads an `.svg` with no DOM
|
|
105
|
+
* installed (ADR 0012, P3 as amended 2026-10-14). It compares `localName`
|
|
106
|
+
* folded to lower case, as it did when that tree came out of an HTML parser.
|
|
108
107
|
*/
|
|
109
|
-
const ELEMENT_NODE = 1;
|
|
110
108
|
const TEXT_NODE = 3;
|
|
111
|
-
const CDATA_NODE = 4;
|
|
112
|
-
// `Node` and not `Element`, because only `childNodes` is ever read and one
|
|
113
|
-
// caller is DOMPurify's `RETURN_DOM` answer — which its own typed overload
|
|
114
|
-
// declares as a `Node`. Widening the parameter is more honest than casting the
|
|
115
|
-
// argument at that one call site.
|
|
116
109
|
function childElements(parent) {
|
|
117
|
-
|
|
118
|
-
for (const node of Array.from(parent.childNodes)) {
|
|
119
|
-
if (node.nodeType === ELEMENT_NODE)
|
|
120
|
-
found.push(node);
|
|
121
|
-
}
|
|
122
|
-
return found;
|
|
110
|
+
return parent.children;
|
|
123
111
|
}
|
|
124
112
|
const nameOf = (element) => element.localName?.toLowerCase() ?? '';
|
|
125
113
|
/** One attribute as a finite number, or the caller's default. */
|
|
@@ -614,10 +602,10 @@ function readPolygon(element, frame, paint, sketch) {
|
|
|
614
602
|
}
|
|
615
603
|
const xs = points.map(p => p[0]);
|
|
616
604
|
const ys = points.map(p => p[1]);
|
|
617
|
-
const minX = Math.min(
|
|
618
|
-
const minY = Math.min(
|
|
619
|
-
const w = Math.max(
|
|
620
|
-
const h = Math.max(
|
|
605
|
+
const minX = xs.reduce((a, b) => Math.min(a, b), Infinity);
|
|
606
|
+
const minY = ys.reduce((a, b) => Math.min(a, b), Infinity);
|
|
607
|
+
const w = xs.reduce((a, b) => Math.max(a, b), -Infinity) - minX;
|
|
608
|
+
const h = ys.reduce((a, b) => Math.max(a, b), -Infinity) - minY;
|
|
621
609
|
if (points.length < 3 || w <= 0 || h <= 0) {
|
|
622
610
|
if (points.length === 0 || w < 0 || h < 0) {
|
|
623
611
|
sketch.notes.once('empty-box', EMPTY_BOX_NOTE, SVG_SKETCH_EMPTY_BOX);
|
|
@@ -712,13 +700,14 @@ const LINE_HEIGHT_RATIO = 1.2;
|
|
|
712
700
|
function readText(element, frame, paint, sketch) {
|
|
713
701
|
const lines = [];
|
|
714
702
|
const collapse = (raw) => raw.replace(/\s+/g, ' ').trim();
|
|
715
|
-
for (const node of
|
|
716
|
-
if (
|
|
717
|
-
|
|
703
|
+
for (const node of element.childNodes) {
|
|
704
|
+
if (!('localName' in node)) {
|
|
705
|
+
// Text and CDATA alike; a comment is not a line.
|
|
706
|
+
const line = node.nodeType === TEXT_NODE ? collapse(node.nodeValue) : '';
|
|
718
707
|
if (line)
|
|
719
708
|
lines.push(line);
|
|
720
709
|
}
|
|
721
|
-
else
|
|
710
|
+
else {
|
|
722
711
|
const child = node;
|
|
723
712
|
if (nameOf(child) !== 'tspan')
|
|
724
713
|
continue;
|
|
@@ -734,7 +723,9 @@ function readText(element, frame, paint, sketch) {
|
|
|
734
723
|
return;
|
|
735
724
|
}
|
|
736
725
|
const fontSize = len(frame, paint.fontSize ?? DEFAULT_FONT_SIZE);
|
|
737
|
-
const widest =
|
|
726
|
+
const widest = lines
|
|
727
|
+
.map(line => line.length)
|
|
728
|
+
.reduce((a, b) => Math.max(a, b), -Infinity);
|
|
738
729
|
const w = Math.max(widest * fontSize * CHARACTER_WIDTH_RATIO, fontSize);
|
|
739
730
|
const h = lines.length * fontSize * LINE_HEIGHT_RATIO;
|
|
740
731
|
// `text-anchor` says which end of the box `x` names, and it INHERITS — a
|
|
@@ -771,8 +762,6 @@ const CONTAINERS = new Set(['g', 'a']);
|
|
|
771
762
|
* most visible thing this reader does to a mermaid diagram.
|
|
772
763
|
*/
|
|
773
764
|
const NON_RENDERING = new Set(['title', 'desc', 'metadata']);
|
|
774
|
-
/** The sanitizer's own HTML wrapper, which no `.svg` on a disk ever contained. */
|
|
775
|
-
const PARSER_SCAFFOLDING = new Set(['html', 'head', 'body']);
|
|
776
765
|
function walk(parent, frame, inherited, sketch) {
|
|
777
766
|
for (const element of childElements(parent)) {
|
|
778
767
|
visit(element, frame, inherited, sketch);
|
|
@@ -925,83 +914,66 @@ export function svgFrameOf(element) {
|
|
|
925
914
|
/**
|
|
926
915
|
* The document, sanitized, or an exception naming what is wrong with it.
|
|
927
916
|
*
|
|
928
|
-
*
|
|
929
|
-
* `
|
|
930
|
-
*
|
|
931
|
-
*
|
|
932
|
-
*
|
|
933
|
-
*
|
|
917
|
+
* Read with `readXml` in its conformant mode, then filtered by the allow-list
|
|
918
|
+
* in `svg-sanitize.ts`; the walk and every recogniser see only what came back.
|
|
919
|
+
* No DOM is involved at any step, so the same function runs in the editor, in
|
|
920
|
+
* a test and on a server (ADR 0012, P3 as amended 2026-10-14). It replaced
|
|
921
|
+
* `DOMParser` + DOMPurify, which needed a DOM installed before the library was
|
|
922
|
+
* even imported (the P0 import spike measured that trap).
|
|
923
|
+
*
|
|
924
|
+
* Well-formedness first: anything the reader had to recover from is refused,
|
|
925
|
+
* with the reader's first remark as the detail, as `DOMParser` used to refuse
|
|
926
|
+
* it. A `DOCTYPE` is not an error: it is skipped and no entity it declares is
|
|
927
|
+
* ever expanded, so a `&name;` it declares stays those characters.
|
|
934
928
|
*
|
|
935
929
|
* The sanitizer is not optional and it is not defensive programming. An `.svg`
|
|
936
930
|
* is an executable document — `<script>`, `<foreignObject>` with markup, event
|
|
937
|
-
* handler attributes — handed over by whoever sent the file.
|
|
938
|
-
* `USE_PROFILES: { svg: true }` guard is what the edgeless clipboard already
|
|
939
|
-
* puts in front of a pasted SVG.
|
|
931
|
+
* handler attributes — handed over by whoever sent the file.
|
|
940
932
|
*
|
|
941
933
|
* The first of the two passes {@link parseSvgSketch} is made of, exported so a
|
|
942
934
|
* framework that recognises part of a picture reads the SAME sanitised tree
|
|
943
935
|
* the sketch walks (ADR 0032 §2, §7) — never the source.
|
|
944
936
|
*/
|
|
945
937
|
export function sanitizeSvg(source, notes) {
|
|
946
|
-
const
|
|
947
|
-
const
|
|
948
|
-
if (
|
|
949
|
-
|
|
950
|
-
// document — a heading, the message, then the offending source line with a
|
|
951
|
-
// caret under it — and pouring that into a toast makes the one sentence
|
|
952
|
-
// that matters unreadable.
|
|
953
|
-
const [summary = ''] = (error.textContent ?? '')
|
|
954
|
-
.split('\n')
|
|
955
|
-
.map(line => line.trim())
|
|
956
|
-
.filter(line => line.length > 0);
|
|
957
|
-
const detail = summary.slice(0, 200);
|
|
938
|
+
const document = readXml(source, { conformant: true });
|
|
939
|
+
const [complaint] = document.notes;
|
|
940
|
+
if (complaint !== undefined) {
|
|
941
|
+
const detail = complaint.slice(0, 200);
|
|
958
942
|
throw new InterchangeImportError(`This file is not well-formed XML, so no drawing can be read out of it: ${detail}`, {
|
|
959
943
|
messageKey: SVG_SKETCH_ERROR_MALFORMED_XML[0],
|
|
960
944
|
messageParams: { detail },
|
|
961
945
|
});
|
|
962
946
|
}
|
|
963
|
-
if (
|
|
964
|
-
const tag =
|
|
947
|
+
if (document.root?.local.toLowerCase() !== 'svg') {
|
|
948
|
+
const tag = document.root?.local ?? 'nothing';
|
|
965
949
|
throw new InterchangeImportError(`An SVG opens on <svg>; this one opens on <${tag}>.`, { messageKey: SVG_SKETCH_ERROR_NOT_SVG[0], messageParams: { tag } });
|
|
966
950
|
}
|
|
967
|
-
const
|
|
968
|
-
USE_PROFILES: { svg: true },
|
|
969
|
-
RETURN_DOM: true,
|
|
970
|
-
});
|
|
951
|
+
const { root, report } = sanitizeSvgNode(document.root);
|
|
971
952
|
// …and then say what the sanitizer took, which is NOT the same list as what
|
|
972
953
|
// the walk below skips. `<use>` and `<foreignObject>` never reach the walk at
|
|
973
|
-
// all — they are two of the SVG format's genuine attack surfaces
|
|
974
|
-
//
|
|
975
|
-
//
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
for (const entry of DOMPurify.removed) {
|
|
979
|
-
const removed = entry;
|
|
980
|
-
if (removed.element) {
|
|
954
|
+
// all — they are two of the SVG format's genuine attack surfaces — so a
|
|
955
|
+
// drawing built out of symbol instances arrives empty. Without this the only
|
|
956
|
+
// remark would be "nothing was recognised", which is true and useless.
|
|
957
|
+
for (const removal of report.removed) {
|
|
958
|
+
if ('element' in removal) {
|
|
981
959
|
// The node's OWN spelling, because `foreignObject` is camel-cased in the
|
|
982
960
|
// format and a remark that called it `foreignobject` would send its
|
|
983
961
|
// reader looking for something the spec does not have. The dedup key is
|
|
984
962
|
// folded; the sentence is not.
|
|
985
|
-
const name =
|
|
986
|
-
// The sanitizer parses into an HTML document and reports unwinding its
|
|
987
|
-
// own wrapper as a removal. Nobody wrote a `<body>` into an `.svg`, and a
|
|
988
|
-
// remark about one would be this reader telling the user about its own
|
|
989
|
-
// plumbing.
|
|
990
|
-
if (PARSER_SCAFFOLDING.has(name.toLowerCase()))
|
|
991
|
-
continue;
|
|
963
|
+
const name = removal.element;
|
|
992
964
|
notes.once(`removed:${name.toLowerCase()}`, `\`<${name}>\` was removed while sanitizing the file — it is one of the constructs an SVG can carry code in — so nothing it drew was imported.`, SVG_SKETCH_REMOVED, { name }, name);
|
|
993
965
|
}
|
|
994
|
-
else
|
|
966
|
+
else {
|
|
995
967
|
// Worded for what this actually is, which is broader than "unsafe": the
|
|
996
|
-
//
|
|
997
|
-
//
|
|
968
|
+
// allow-list drops an event handler AND a `requiredFeatures`, and
|
|
969
|
+
// calling the second one dangerous would be this reader accusing a
|
|
998
970
|
// perfectly ordinary file of something.
|
|
999
971
|
notes.once('removed-attribute', 'Attributes outside the safe SVG drawing vocabulary — event handlers, script URLs, and anything else the sanitizer does not know — were removed before the file was read.', SVG_SKETCH_REMOVED_ATTRIBUTE);
|
|
1000
972
|
}
|
|
1001
973
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
974
|
+
if (report.tooDeep) {
|
|
975
|
+
notes.once('too-deep', `Elements nested more than ${MAX_SVG_DEPTH} levels deep were not read.`, SVG_SKETCH_TOO_DEEP, { depth: MAX_SVG_DEPTH });
|
|
976
|
+
}
|
|
1005
977
|
if (!root) {
|
|
1006
978
|
throw new InterchangeImportError(SVG_SKETCH_ERROR_SANITIZED_AWAY[1], {
|
|
1007
979
|
messageKey: SVG_SKETCH_ERROR_SANITIZED_AWAY[0],
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { type XmlDocument, type XmlNode, type XmlReadOptions } from './xml-reader.js';
|
|
2
|
+
/**
|
|
3
|
+
* A read-only, DOM-SHAPED view over {@link readXml}'s tree, with namespaces
|
|
4
|
+
* resolved — what the BPMN reader and the SVG sketch walk instead of a
|
|
5
|
+
* `DOMParser` document (ADR 0012, P3 as amended 2026-10-14).
|
|
6
|
+
*
|
|
7
|
+
* Both readers were written against the DOM and read only a small part of it:
|
|
8
|
+
* `localName`, `namespaceURI`, `attributes`, `getAttribute`, `children`,
|
|
9
|
+
* `childNodes`, `textContent`, `parentElement` and a few compound selectors.
|
|
10
|
+
* This class answers exactly that part, with the DOM's names and the DOM's
|
|
11
|
+
* answers for an XML document (case-sensitive names, `null` for a missing
|
|
12
|
+
* attribute or namespace), so the readers kept their logic and lost their
|
|
13
|
+
* global. It is not a DOM: nothing here mutates, and a member a reader does
|
|
14
|
+
* not use does not exist.
|
|
15
|
+
*
|
|
16
|
+
* Built with an explicit stack: the depth of a file is its author's choice.
|
|
17
|
+
*/
|
|
18
|
+
export declare const XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace";
|
|
19
|
+
export declare const XMLNS_NAMESPACE = "http://www.w3.org/2000/xmlns/";
|
|
20
|
+
/** One attribute, as the DOM's `Attr` names it. */
|
|
21
|
+
export interface XmlAttribute {
|
|
22
|
+
/** The qualified name as written: `xlink:href`, `fill`. */
|
|
23
|
+
name: string;
|
|
24
|
+
prefix: string | null;
|
|
25
|
+
localName: string;
|
|
26
|
+
/** Resolved; `null` for an unprefixed attribute, as XML namespaces say. */
|
|
27
|
+
namespaceURI: string | null;
|
|
28
|
+
value: string;
|
|
29
|
+
}
|
|
30
|
+
/** Character data or a comment, as the DOM's `Text` / `Comment` show it. */
|
|
31
|
+
export interface XmlCharacterNode {
|
|
32
|
+
/** 3 for text and CDATA, 8 for a comment, 7 for an instruction. */
|
|
33
|
+
nodeType: 3 | 7 | 8;
|
|
34
|
+
nodeValue: string;
|
|
35
|
+
}
|
|
36
|
+
export declare class XmlElement {
|
|
37
|
+
/** The reader's node, for a caller that wants its range or its content. */
|
|
38
|
+
readonly node: XmlNode;
|
|
39
|
+
readonly parentElement: XmlElement | null;
|
|
40
|
+
readonly namespaceURI: string | null;
|
|
41
|
+
readonly attributes: XmlAttribute[];
|
|
42
|
+
readonly nodeType = 1;
|
|
43
|
+
readonly children: XmlElement[];
|
|
44
|
+
readonly childNodes: (XmlElement | XmlCharacterNode)[];
|
|
45
|
+
constructor(
|
|
46
|
+
/** The reader's node, for a caller that wants its range or its content. */
|
|
47
|
+
node: XmlNode, parentElement: XmlElement | null, namespaceURI: string | null, attributes: XmlAttribute[]);
|
|
48
|
+
get localName(): string;
|
|
49
|
+
get prefix(): string | null;
|
|
50
|
+
/** The qualified name as written, which is what the DOM says for XML. */
|
|
51
|
+
get nodeName(): string;
|
|
52
|
+
get tagName(): string;
|
|
53
|
+
get firstElementChild(): XmlElement | null;
|
|
54
|
+
/** Every text and CDATA under this element, in order; comments excluded. */
|
|
55
|
+
get textContent(): string;
|
|
56
|
+
/** By qualified name, exactly as written — the DOM's rule for XML. */
|
|
57
|
+
getAttribute(name: string): string | null;
|
|
58
|
+
hasAttribute(name: string): boolean;
|
|
59
|
+
get classList(): {
|
|
60
|
+
contains(token: string): boolean;
|
|
61
|
+
};
|
|
62
|
+
/** Is `other` this element or one of its descendants? */
|
|
63
|
+
contains(other: XmlElement | null): boolean;
|
|
64
|
+
/** Every descendant (never this element), in document order. */
|
|
65
|
+
descendants(): XmlElement[];
|
|
66
|
+
matches(selectors: string): boolean;
|
|
67
|
+
querySelectorAll(selectors: string): XmlElement[];
|
|
68
|
+
querySelector(selectors: string): XmlElement | null;
|
|
69
|
+
private is;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The namespaces in scope during a depth-first walk, as one stack per prefix:
|
|
73
|
+
* {@link enter} and {@link exit} cost the element's own declarations, and
|
|
74
|
+
* {@link resolve} is one lookup. A copied map per declaring element was
|
|
75
|
+
* quadratic: a root declaring 2 000 prefixes over thousands of
|
|
76
|
+
* `<g xmlns:q="x"/>` took 28 s and 284 MiB (security review, 2026-10-14).
|
|
77
|
+
*/
|
|
78
|
+
export declare class XmlNamespaceScope {
|
|
79
|
+
private readonly bound;
|
|
80
|
+
enter(node: XmlNode): void;
|
|
81
|
+
exit(node: XmlNode): void;
|
|
82
|
+
/** `null` when unbound — or undeclared by `xmlns=""` (Namespaces in XML §6.2). */
|
|
83
|
+
resolve(prefix: string): string | null;
|
|
84
|
+
/** The node's attributes, resolved in this scope (entered first). */
|
|
85
|
+
attributesOf(node: XmlNode): XmlAttribute[];
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The element view of a node the reader produced, namespaces resolved from the
|
|
89
|
+
* `xmlns` declarations on the way down (`xml` is bound, as XML says it is).
|
|
90
|
+
* An unbound prefix resolves to `null`, which is what a reader then refuses or
|
|
91
|
+
* ignores: this function never guesses a namespace.
|
|
92
|
+
*/
|
|
93
|
+
export declare function xmlElementOf(root: XmlNode): XmlElement;
|
|
94
|
+
/** {@link readXml}, then {@link xmlElementOf} on its first root. */
|
|
95
|
+
export declare function readXmlElement(source: string, options?: XmlReadOptions): {
|
|
96
|
+
document: XmlDocument;
|
|
97
|
+
root: XmlElement | undefined;
|
|
98
|
+
};
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { readXml, } from './xml-reader.js';
|
|
2
|
+
/**
|
|
3
|
+
* A read-only, DOM-SHAPED view over {@link readXml}'s tree, with namespaces
|
|
4
|
+
* resolved — what the BPMN reader and the SVG sketch walk instead of a
|
|
5
|
+
* `DOMParser` document (ADR 0012, P3 as amended 2026-10-14).
|
|
6
|
+
*
|
|
7
|
+
* Both readers were written against the DOM and read only a small part of it:
|
|
8
|
+
* `localName`, `namespaceURI`, `attributes`, `getAttribute`, `children`,
|
|
9
|
+
* `childNodes`, `textContent`, `parentElement` and a few compound selectors.
|
|
10
|
+
* This class answers exactly that part, with the DOM's names and the DOM's
|
|
11
|
+
* answers for an XML document (case-sensitive names, `null` for a missing
|
|
12
|
+
* attribute or namespace), so the readers kept their logic and lost their
|
|
13
|
+
* global. It is not a DOM: nothing here mutates, and a member a reader does
|
|
14
|
+
* not use does not exist.
|
|
15
|
+
*
|
|
16
|
+
* Built with an explicit stack: the depth of a file is its author's choice.
|
|
17
|
+
*/
|
|
18
|
+
export const XML_NAMESPACE = 'http://www.w3.org/XML/1998/namespace';
|
|
19
|
+
export const XMLNS_NAMESPACE = 'http://www.w3.org/2000/xmlns/';
|
|
20
|
+
/** `Node.ELEMENT_NODE`, spelled. */
|
|
21
|
+
const ELEMENT_NODE = 1;
|
|
22
|
+
const COMPOUND = /^([A-Za-z][\w-]*)?((?:[.#][\w-]+|\[[\w:-]+(?:="[^"]*")?\])*)$/;
|
|
23
|
+
const PART = /\.([\w-]+)|#([\w-]+)|\[([\w:-]+)(?:="([^"]*)")?\]/g;
|
|
24
|
+
/**
|
|
25
|
+
* A selector list without combinators — the only shape the readers write.
|
|
26
|
+
* Anything else is a programming error in a reader, and says so.
|
|
27
|
+
*/
|
|
28
|
+
function parseSelectors(selectors) {
|
|
29
|
+
return selectors.split(',').map(raw => {
|
|
30
|
+
const text = raw.trim();
|
|
31
|
+
const match = COMPOUND.exec(text);
|
|
32
|
+
if (!match || text.length === 0) {
|
|
33
|
+
throw new Error(`XmlElement does not support the selector "${text}"`);
|
|
34
|
+
}
|
|
35
|
+
const compound = { type: match[1], classes: [], attrs: [] };
|
|
36
|
+
for (const part of (match[2] ?? '').matchAll(PART)) {
|
|
37
|
+
if (part[1] !== undefined)
|
|
38
|
+
compound.classes.push(part[1]);
|
|
39
|
+
else if (part[2] !== undefined)
|
|
40
|
+
compound.attrs.push({ name: 'id', value: part[2] });
|
|
41
|
+
else
|
|
42
|
+
compound.attrs.push({ name: part[3], value: part[4] });
|
|
43
|
+
}
|
|
44
|
+
return compound;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
export class XmlElement {
|
|
48
|
+
constructor(
|
|
49
|
+
/** The reader's node, for a caller that wants its range or its content. */
|
|
50
|
+
node, parentElement, namespaceURI, attributes) {
|
|
51
|
+
this.node = node;
|
|
52
|
+
this.parentElement = parentElement;
|
|
53
|
+
this.namespaceURI = namespaceURI;
|
|
54
|
+
this.attributes = attributes;
|
|
55
|
+
this.nodeType = ELEMENT_NODE;
|
|
56
|
+
this.children = [];
|
|
57
|
+
this.childNodes = [];
|
|
58
|
+
}
|
|
59
|
+
get localName() {
|
|
60
|
+
return this.node.local;
|
|
61
|
+
}
|
|
62
|
+
get prefix() {
|
|
63
|
+
return this.node.prefix === '' ? null : this.node.prefix;
|
|
64
|
+
}
|
|
65
|
+
/** The qualified name as written, which is what the DOM says for XML. */
|
|
66
|
+
get nodeName() {
|
|
67
|
+
return this.node.name;
|
|
68
|
+
}
|
|
69
|
+
get tagName() {
|
|
70
|
+
return this.node.name;
|
|
71
|
+
}
|
|
72
|
+
get firstElementChild() {
|
|
73
|
+
return this.children[0] ?? null;
|
|
74
|
+
}
|
|
75
|
+
/** Every text and CDATA under this element, in order; comments excluded. */
|
|
76
|
+
get textContent() {
|
|
77
|
+
const parts = [];
|
|
78
|
+
const stack = [
|
|
79
|
+
...this.childNodes,
|
|
80
|
+
].reverse();
|
|
81
|
+
while (stack.length > 0) {
|
|
82
|
+
const next = stack.pop();
|
|
83
|
+
if (next instanceof XmlElement) {
|
|
84
|
+
for (let index = next.childNodes.length - 1; index >= 0; index -= 1) {
|
|
85
|
+
stack.push(next.childNodes[index]);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else if (next.nodeType === 3) {
|
|
89
|
+
parts.push(next.nodeValue);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return parts.join('');
|
|
93
|
+
}
|
|
94
|
+
/** By qualified name, exactly as written — the DOM's rule for XML. */
|
|
95
|
+
getAttribute(name) {
|
|
96
|
+
// Own properties only: an attribute named `constructor` is a string, and
|
|
97
|
+
// a missing one is never `Object.prototype`'s.
|
|
98
|
+
return Object.hasOwn(this.node.attrs, name) ? this.node.attrs[name] : null;
|
|
99
|
+
}
|
|
100
|
+
hasAttribute(name) {
|
|
101
|
+
return Object.hasOwn(this.node.attrs, name);
|
|
102
|
+
}
|
|
103
|
+
get classList() {
|
|
104
|
+
const tokens = (this.getAttribute('class') ?? '').split(/\s+/);
|
|
105
|
+
return { contains: token => tokens.includes(token) };
|
|
106
|
+
}
|
|
107
|
+
/** Is `other` this element or one of its descendants? */
|
|
108
|
+
contains(other) {
|
|
109
|
+
for (let at = other; at; at = at.parentElement) {
|
|
110
|
+
if (at === this)
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
/** Every descendant (never this element), in document order. */
|
|
116
|
+
descendants() {
|
|
117
|
+
const found = [];
|
|
118
|
+
const stack = [...this.children].reverse();
|
|
119
|
+
while (stack.length > 0) {
|
|
120
|
+
const next = stack.pop();
|
|
121
|
+
found.push(next);
|
|
122
|
+
for (let index = next.children.length - 1; index >= 0; index -= 1) {
|
|
123
|
+
stack.push(next.children[index]);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return found;
|
|
127
|
+
}
|
|
128
|
+
matches(selectors) {
|
|
129
|
+
return parseSelectors(selectors).some(compound => this.is(compound));
|
|
130
|
+
}
|
|
131
|
+
querySelectorAll(selectors) {
|
|
132
|
+
const list = parseSelectors(selectors);
|
|
133
|
+
return this.descendants().filter(element => list.some(compound => element.is(compound)));
|
|
134
|
+
}
|
|
135
|
+
querySelector(selectors) {
|
|
136
|
+
return this.querySelectorAll(selectors)[0] ?? null;
|
|
137
|
+
}
|
|
138
|
+
is(compound) {
|
|
139
|
+
if (compound.type !== undefined && compound.type !== this.localName) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
if (compound.classes.some(token => !this.classList.contains(token))) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return compound.attrs.every(({ name, value }) => {
|
|
146
|
+
const actual = this.getAttribute(name);
|
|
147
|
+
return actual !== null && (value === undefined || actual === value);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The namespaces in scope during a depth-first walk, as one stack per prefix:
|
|
153
|
+
* {@link enter} and {@link exit} cost the element's own declarations, and
|
|
154
|
+
* {@link resolve} is one lookup. A copied map per declaring element was
|
|
155
|
+
* quadratic: a root declaring 2 000 prefixes over thousands of
|
|
156
|
+
* `<g xmlns:q="x"/>` took 28 s and 284 MiB (security review, 2026-10-14).
|
|
157
|
+
*/
|
|
158
|
+
export class XmlNamespaceScope {
|
|
159
|
+
constructor() {
|
|
160
|
+
this.bound = new Map([
|
|
161
|
+
['xml', [XML_NAMESPACE]],
|
|
162
|
+
]);
|
|
163
|
+
}
|
|
164
|
+
enter(node) {
|
|
165
|
+
for (const [prefix, value] of declarationsOf(node)) {
|
|
166
|
+
const stack = this.bound.get(prefix);
|
|
167
|
+
if (stack)
|
|
168
|
+
stack.push(value);
|
|
169
|
+
else
|
|
170
|
+
this.bound.set(prefix, [value]);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
exit(node) {
|
|
174
|
+
for (const [prefix] of declarationsOf(node))
|
|
175
|
+
this.bound.get(prefix)?.pop();
|
|
176
|
+
}
|
|
177
|
+
/** `null` when unbound — or undeclared by `xmlns=""` (Namespaces in XML §6.2). */
|
|
178
|
+
resolve(prefix) {
|
|
179
|
+
const stack = this.bound.get(prefix);
|
|
180
|
+
return stack?.at(-1) || null;
|
|
181
|
+
}
|
|
182
|
+
/** The node's attributes, resolved in this scope (entered first). */
|
|
183
|
+
attributesOf(node) {
|
|
184
|
+
return Object.entries(node.attrs).map(([name, value]) => {
|
|
185
|
+
const colon = name.indexOf(':');
|
|
186
|
+
const prefix = colon < 0 ? null : name.slice(0, colon);
|
|
187
|
+
const localName = colon < 0 ? name : name.slice(colon + 1);
|
|
188
|
+
let namespaceURI = null;
|
|
189
|
+
if (name === 'xmlns' || prefix === 'xmlns')
|
|
190
|
+
namespaceURI = XMLNS_NAMESPACE;
|
|
191
|
+
else if (prefix !== null)
|
|
192
|
+
namespaceURI = this.resolve(prefix);
|
|
193
|
+
return { name, prefix, localName, namespaceURI, value };
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function* declarationsOf(node) {
|
|
198
|
+
for (const name in node.attrs) {
|
|
199
|
+
if (name === 'xmlns')
|
|
200
|
+
yield ['', node.attrs[name]];
|
|
201
|
+
else if (name.startsWith('xmlns:'))
|
|
202
|
+
yield [name.slice(6), node.attrs[name]];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* The element view of a node the reader produced, namespaces resolved from the
|
|
207
|
+
* `xmlns` declarations on the way down (`xml` is bound, as XML says it is).
|
|
208
|
+
* An unbound prefix resolves to `null`, which is what a reader then refuses or
|
|
209
|
+
* ignores: this function never guesses a namespace.
|
|
210
|
+
*/
|
|
211
|
+
export function xmlElementOf(root) {
|
|
212
|
+
const scope = new XmlNamespaceScope();
|
|
213
|
+
const make = (node, parent) => {
|
|
214
|
+
scope.enter(node);
|
|
215
|
+
return new XmlElement(node, parent, scope.resolve(node.prefix), scope.attributesOf(node));
|
|
216
|
+
};
|
|
217
|
+
// A node assembled by hand has no `content`; its children are all it has.
|
|
218
|
+
const frameOf = (node, element) => ({
|
|
219
|
+
node,
|
|
220
|
+
element,
|
|
221
|
+
entries: node.content ?? node.children,
|
|
222
|
+
index: 0,
|
|
223
|
+
});
|
|
224
|
+
const top = make(root, null);
|
|
225
|
+
const stack = [frameOf(root, top)];
|
|
226
|
+
while (stack.length > 0) {
|
|
227
|
+
const frame = stack[stack.length - 1];
|
|
228
|
+
if (frame.index >= frame.entries.length) {
|
|
229
|
+
scope.exit(frame.node);
|
|
230
|
+
stack.pop();
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const entry = frame.entries[frame.index];
|
|
234
|
+
frame.index += 1;
|
|
235
|
+
const { element } = frame;
|
|
236
|
+
if (!('kind' in entry)) {
|
|
237
|
+
const child = make(entry, element);
|
|
238
|
+
element.children.push(child);
|
|
239
|
+
element.childNodes.push(child);
|
|
240
|
+
stack.push(frameOf(entry, child));
|
|
241
|
+
}
|
|
242
|
+
else if (entry.kind === 'text') {
|
|
243
|
+
element.childNodes.push({ nodeType: 3, nodeValue: entry.value });
|
|
244
|
+
}
|
|
245
|
+
else if (entry.kind === 'comment') {
|
|
246
|
+
element.childNodes.push({ nodeType: 8, nodeValue: entry.value });
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
element.childNodes.push({ nodeType: 7, nodeValue: entry.data });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return top;
|
|
253
|
+
}
|
|
254
|
+
/** {@link readXml}, then {@link xmlElementOf} on its first root. */
|
|
255
|
+
export function readXmlElement(source, options = {}) {
|
|
256
|
+
const document = readXml(source, options);
|
|
257
|
+
return {
|
|
258
|
+
document,
|
|
259
|
+
root: document.root ? xmlElementOf(document.root) : undefined,
|
|
260
|
+
};
|
|
261
|
+
}
|