@pptx-studio/render-dom 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright 2026 Dewiride Technologies
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
package/NOTICE ADDED
@@ -0,0 +1,43 @@
1
+ PPTX Studio
2
+ Copyright 2026 Dewiride Technologies
3
+
4
+ This product includes software developed at Dewiride Technologies
5
+ (https://github.com/Dewiride-Open-Source/Dewiride-PPTX-Studio).
6
+
7
+ --------------------------------------------------------------------------------
8
+ Third-party material redistributed in this repository
9
+ --------------------------------------------------------------------------------
10
+
11
+ Entries are added here at the moment the material lands, not at release time.
12
+
13
+ * Apache POI
14
+ Copyright The Apache Software Foundation
15
+ Licensed under the Apache License, Version 2.0.
16
+ https://poi.apache.org/
17
+
18
+ @pptx-studio/geometry contains the 187 DrawingML preset shape definitions,
19
+ transcoded at build time from POI's presetShapeDefinitions.xml by
20
+ tools/geometry-codegen. The transcoded form is a re-encoding of that file's
21
+ content and carries POI's copyright.
22
+
23
+ The file itself is not redistributed in this repository. The generated
24
+ modules in packages/geometry/src/presets record the SHA-256 of the exact
25
+ input they were produced from:
26
+
27
+ presetShapeDefinitions.xml, 538970 bytes
28
+ sha256 4a762444d8d85876881c02a5b1dedf6f73006fcd8acb7b4e393435615b37c780
29
+
30
+ --------------------------------------------------------------------------------
31
+ Attributions the plan commits us to, listed so the obligation is visible before
32
+ the code arrives
33
+ --------------------------------------------------------------------------------
34
+
35
+ * Pattern fill tiles (sub-phase 2.7) are generated from Mono libgdiplus.
36
+ libgdiplus - Copyright Novell, Inc. and contributors - MIT
37
+
38
+ * Metric-compatible substitute fonts ship in a separate package,
39
+ @pptx-studio/fonts-metric-compat, under SIL OFL 1.1 with its own
40
+ OFL.txt and reserved-font-name handling. They are deliberately kept out
41
+ of this Apache-2.0 tree so the licence boundary is a package boundary.
42
+
43
+ See LEGAL.md for the clean-room record and the reimplementation basis.
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # @pptx-studio/render-dom
2
+
3
+ The live renderer: one `<svg>` per slide, built as real elements.
4
+
5
+ Sub-phase 2.10. Every fact about DrawingML this package draws on lives in
6
+ [`@pptx-studio/render-svg`](../render-svg#readme) and was measured against Microsoft PowerPoint in
7
+ experiment C6.
8
+
9
+ ## One tree, two renderers
10
+
11
+ The plan asks for two renderers "over the same layout engine", so that a thumbnail and the editor
12
+ cannot disagree about what is on the screen. Sharing a _layout_ is not enough for that on its own:
13
+ emitting a `<linearGradient>` twice, once as text and once through `createElementNS`, is two chances
14
+ to write the same gradient wrong in two different ways.
15
+
16
+ So the shared surface goes a level deeper. `render-svg` builds a node tree and serialises it; this
17
+ builds the identical tree and instantiates it. The suite asserts that the browser's own
18
+ `XMLSerializer` over the mounted nodes returns the string emitter's output character for character.
19
+
20
+ ## The namespace
21
+
22
+ `document.createElement('path')` makes an HTML element that happens to be spelled `path`, and it
23
+ draws nothing at all — silently, with no error anywhere. Every element here goes through
24
+ `createElementNS`. Attributes are the mirror image: they belong to no namespace and go through plain
25
+ `setAttribute`.
26
+
27
+ The suite runs in real Chromium rather than jsdom, which is the repository's standing choice and
28
+ earns it here: the test calls `getTotalLength()` on the mounted path, which an HTML element cannot
29
+ answer.
30
+
31
+ ## Zoom is a transform, never a re-layout
32
+
33
+ The mounted `<svg>` carries a `viewBox` in EMU and is sized in CSS pixels, so scaling it is one
34
+ attribute and no arithmetic — `resize()` writes `width` and `height` and nothing else moves. The
35
+ plan's rule is the reason: font metrics are not linear in point size, so a renderer that re-lays out
36
+ per zoom level changes its line breaks as the user zooms.
37
+
38
+ ## Using it
39
+
40
+ ```ts
41
+ import { mountSlide } from '@pptx-studio/render-dom';
42
+
43
+ const mounted = mountSlide(host, slide, { cx: 12192000, cy: 6858000 }, { width: 960, height: 540 });
44
+ mounted.element(shape.cNvPrId); // the element drawing one shape, for hit-testing
45
+ mounted.resize(1280, 720);
46
+ mounted.unmount();
47
+ ```
48
+
49
+ `document` may be passed explicitly, because this package has to work in a Web Worker with no global
50
+ one — and because a test that has to stub a global is a test that can pass for the wrong reason.
51
+
52
+ ## What it does not do yet
53
+
54
+ Text (3.8), the overlay canvas and adjust-handle chrome (2.11), selection and gestures (Phase 5),
55
+ and slide virtualization (12.1). The host is emptied on mount; anything a caller wants beside the
56
+ slide is a sibling of the host, not a child of it.
57
+
58
+ ## The text layer
59
+
60
+ `mountTextLayer` builds an HTML layer over the slide from the identical `TextBlock`
61
+ `@pptx-studio/render-svg` emits: one div per shape, one per line, a span per piece, and the same
62
+ rule rectangles. Real text nodes, so selection, an IME and a screen reader all work on them.
63
+
64
+ There is one set of line boxes and both renderers read it, which is what stops them drifting. The
65
+ one place the HTML layer is not exact is the baseline: a browser puts an inline baseline a
66
+ half-leading plus an ascent below the line box, from font metrics it has rounded to whole pixels,
67
+ so the `line-height` that reconciles the two lands within one CSS pixel and no closer. The SVG
68
+ emitter, where a baseline is a coordinate, is exact — which is the right way round, since that is
69
+ what thumbnails and export go through.
@@ -0,0 +1,173 @@
1
+ import { OverlayOptions, Placed, RenderOptions, ShapeOverlay, SlideSize, SvgNode, TextBlock } from "@pptx-studio/render-svg";
2
+ import { Point, PresetGuide } from "@pptx-studio/geometry";
3
+ import { Sheet } from "@pptx-studio/model";
4
+ //#region src/errors.d.ts
5
+ /**
6
+ * Every failure in this package is a `RenderDomError` with a code.
7
+ *
8
+ * There are only two, and both mean the *caller* asked for something
9
+ * impossible: a host that is not an element, or an element with no document to
10
+ * build in. Everything about the slide itself is `render-svg`'s to refuse, and
11
+ * this package never re-decides it.
12
+ */
13
+ type RenderDomErrorCode =
14
+ /** `mountSlide` was handed something that is not an element. */
15
+ 'RENDER_DOM_NO_HOST' |
16
+ /** The host is detached and no `document` was passed. */
17
+ 'RENDER_DOM_NO_DOCUMENT';
18
+ declare const RENDER_DOM_ERROR_CODES: readonly RenderDomErrorCode[];
19
+ declare class RenderDomError extends Error {
20
+ readonly code: RenderDomErrorCode;
21
+ constructor(code: RenderDomErrorCode, message: string);
22
+ }
23
+ declare function isRenderDomError(value: unknown): value is RenderDomError;
24
+ //#endregion
25
+ //#region src/mount.d.ts
26
+ /**
27
+ * One node into one element. Exported because the debug overlay wants it too.
28
+ *
29
+ * A number goes through `render-svg`'s own `num`, not `String`. That is the
30
+ * whole "one tree, two renderers" claim in one line: the string emitter rounds
31
+ * to three decimals and normalises `-0`, so a DOM path using `String` would
32
+ * write `117158.34312` where a thumbnail wrote `117158.343`. Every coordinate
33
+ * in a slide render happens to be a whole number of EMU, which is why this went
34
+ * unnoticed until the overlay's text rectangle - the first fractional one -
35
+ * disagreed at the fourth decimal.
36
+ */
37
+ declare function createNode(document: Document, node: SvgNode): Node;
38
+ interface MountOptions extends RenderOptions {
39
+ /**
40
+ * The `document` to build in.
41
+ *
42
+ * Passed rather than reached for, because this package must work in a Web
43
+ * Worker with an `OffscreenCanvas` and no global `document` - and because a
44
+ * test that has to stub a global is a test that can pass for the wrong reason.
45
+ */
46
+ readonly document?: Document;
47
+ }
48
+ interface MountedSlide {
49
+ readonly root: SVGSVGElement;
50
+ readonly placed: readonly Placed[];
51
+ /** The element drawing a given `p:cNvPr/@id`, for hit-testing and selection. */
52
+ element(cNvPrId: number): SVGElement | null;
53
+ /** Resize without re-laying anything out: the viewBox does the work. */
54
+ resize(width: number, height: number): void;
55
+ /** Detach from the host. The nodes stay valid, so a caller may re-attach. */
56
+ unmount(): void;
57
+ }
58
+ /**
59
+ * Render a sheet into a host element.
60
+ *
61
+ * The host is emptied first. Anything a caller wants to keep beside the slide -
62
+ * the overlay canvas of 2.11, the text layer of 3.8 - is a sibling of the host,
63
+ * not a child of it.
64
+ */
65
+ declare function mountSlide(host: Element, sheet: Sheet, size: SlideSize, options?: MountOptions): MountedSlide;
66
+ /**
67
+ * The same slide as a string, from this package.
68
+ *
69
+ * A convenience rather than a second implementation: it calls `render-svg`'s
70
+ * serializer on the same tree `mountSlide` instantiates, which is the property
71
+ * the tests check.
72
+ */
73
+ declare function renderSlideMarkup(sheet: Sheet, size: SlideSize, options?: RenderOptions): string;
74
+ //#endregion
75
+ //#region src/overlay.d.ts
76
+ /** What a completed drag produced. */
77
+ interface HandleEdit {
78
+ /** Which `a:ahLst` entry was dragged. */
79
+ readonly handle: number;
80
+ /** The adjust values this handle controls, rounded as the file will hold them. */
81
+ readonly adjust: Readonly<Record<string, number>>;
82
+ /** The shape's whole `a:avLst` after the edit, ready to write back. */
83
+ readonly avLst: readonly PresetGuide[];
84
+ }
85
+ interface OverlayMountOptions extends OverlayOptions {
86
+ readonly document?: Document;
87
+ /** Called on every pointer move during a drag, and once more on release. */
88
+ onChange?: (edit: HandleEdit) => void;
89
+ /** Called once on release. */
90
+ onCommit?: (edit: HandleEdit) => void;
91
+ }
92
+ interface MountedOverlay {
93
+ readonly root: SVGGElement;
94
+ readonly overlay: ShapeOverlay;
95
+ /** Redraw against a new placement - after a drag, or a resize. */
96
+ update(placed: Placed): void;
97
+ /** True while a handle is being dragged. */
98
+ dragging(): boolean;
99
+ unmount(): void;
100
+ }
101
+ /**
102
+ * A point in the host `<svg>`'s user space - EMU - from a pointer event.
103
+ *
104
+ * Through the SVG CTM rather than by arithmetic on `getBoundingClientRect`,
105
+ * because the slide may be inside any number of CSS transforms and the browser
106
+ * already knows the composed matrix. `getScreenCTM` returns null for an element
107
+ * that is not being rendered, which a drag cannot reach but a stray event can.
108
+ */
109
+ declare function slidePoint(root: SVGSVGElement, clientX: number, clientY: number): Point | null;
110
+ /**
111
+ * The `a:avLst` a drag leaves behind.
112
+ *
113
+ * The dragged guides replace their namesakes and everything else is kept in
114
+ * place, because an `a:avLst` is an ordered list and a shape with three adjusts
115
+ * whose second one moves must still write three, in order. A guide the shape did
116
+ * not previously declare is appended: the preset's default was in force and the
117
+ * drag has just overridden it.
118
+ */
119
+ declare function mergeAdjust(avLst: readonly PresetGuide[], adjust: Readonly<Record<string, number>>): readonly PresetGuide[];
120
+ /**
121
+ * Draw the overlay for one shape into a host `<svg>`, and make its handles drag.
122
+ *
123
+ * The host must be the same `<svg>` the slide is in, because the overlay is
124
+ * emitted in slide coordinates and carries the shape's own transform - the same
125
+ * arrangement `mountSlide` produces, so an editor appends this to that root.
126
+ */
127
+ declare function mountOverlay(root: SVGSVGElement, placed: Placed, options?: OverlayMountOptions): MountedOverlay;
128
+ //#endregion
129
+ //#region src/text/layer.d.ts
130
+ /** A slide's size in points, which is what a block's coordinates are in. */
131
+ interface LayerSize {
132
+ readonly widthPt: number;
133
+ readonly heightPt: number;
134
+ }
135
+ /** One shape's text, with the frame it is drawn in. */
136
+ interface LayerBlock {
137
+ readonly block: TextBlock;
138
+ /** The shape's frame, in points on the slide. */
139
+ readonly leftPt: number;
140
+ readonly topPt: number;
141
+ readonly widthPt: number;
142
+ readonly heightPt: number;
143
+ /** `p:cNvPr/@id`, so a caller can find the element for a shape. */
144
+ readonly cNvPrId: number;
145
+ readonly name?: string | undefined;
146
+ }
147
+ interface TextLayerOptions {
148
+ readonly document?: Document | undefined;
149
+ /** CSS pixels per point. One transform, never a re-layout. */
150
+ readonly scale?: number | undefined;
151
+ /** Let the layer take pointer events. Off by default: the SVG hit-tests. */
152
+ readonly interactive?: boolean | undefined;
153
+ }
154
+ interface MountedTextLayer {
155
+ readonly root: HTMLElement;
156
+ /** The element holding one shape's text. */
157
+ element(cNvPrId: number): HTMLElement | null;
158
+ /** Rescale without re-laying anything out. */
159
+ resize(scale: number): void;
160
+ unmount(): void;
161
+ }
162
+ /**
163
+ * Mount a text layer into a host, over a slide of `size` points.
164
+ *
165
+ * The host is emptied first. The layer is sized in points and scaled with one
166
+ * transform, which is the plan's rule: font metrics are not linear in point
167
+ * size, so a renderer that re-laid out per zoom would change its line breaks as
168
+ * the user zoomed.
169
+ */
170
+ declare function mountTextLayer(host: Element, blocks: readonly LayerBlock[], size: LayerSize, options?: TextLayerOptions): MountedTextLayer;
171
+ //#endregion
172
+ export { type HandleEdit, type LayerBlock, type LayerSize, type MountOptions, type MountedOverlay, type MountedSlide, type MountedTextLayer, type OverlayMountOptions, RENDER_DOM_ERROR_CODES, RenderDomError, type RenderDomErrorCode, type TextLayerOptions, createNode, isRenderDomError, mergeAdjust, mountOverlay, mountSlide, mountTextLayer, renderSlideMarkup, slidePoint };
173
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/errors.ts","../src/mount.ts","../src/overlay.ts","../src/text/layer.ts"],"mappings":";;;;;;;;;;;;KASY;;;;;cAMC,iCAAiC;cAKjC,uBAAuB;WACzB,MAAM;EAEH,YAAA,MAAM,oBAAoB;;iBAOxB,iBAAiB,iBAAiB,SAAS;;;;;;;;;;;;;;iBC6B3C,WAAW,UAAU,UAAU,MAAM,UAAU;UAW9C,qBAAqB;;;;;;;;WAQ3B,WAAW;;UAGL;WACN,MAAM;WACN,iBAAiB;;EAE1B,QAAQ,kBAAkB;;EAE1B,OAAO,eAAe;;EAEtB;;;;;;;;;iBAmBc,WACd,MAAM,SACN,OAAO,OACP,MAAM,WACN,UAAS,eACR;;;;;;;;iBA2Ca,kBACd,OAAO,OACP,MAAM,WACN,UAAS;;;;UCpHM;;WAEN;;WAEA,QAAQ,SAAS;;WAEjB,gBAAgB;;UAGV,4BAA4B;WAClC,WAAW;;EAEpB,YAAY,MAAM;;EAElB,YAAY,MAAM;;UAGH;WACN,MAAM;WACN,SAAS;;EAElB,OAAO,QAAQ;;EAEf;EACA;;;;;;;;;;iBAWc,WAAW,MAAM,eAAe,iBAAiB,kBAAkB;;;;;;;;;;iBAmBnE,YACd,gBAAgB,eAChB,QAAQ,SAAS,mCACP;;;;;;;;iBAsBI,aACd,MAAM,eACN,QAAQ,QACR,UAAS,sBACR;;;;UC9Fc;WACN;WACA;;;UAIM;WACN,OAAO;;WAEP;WACA;WACA;WACA;;WAEA;WACA;;UAGM;WACN,WAAW;;WAEX;;WAEA;;UAGM;WACN,MAAM;;EAEf,QAAQ,kBAAkB;;EAE1B,OAAO;EACP;;;;;;;;;;iBAiJc,eACd,MAAM,SACN,iBAAiB,cACjB,MAAM,WACN,UAAS,mBACR"}
package/dist/index.js ADDED
@@ -0,0 +1,477 @@
1
+ import { inverseFramePoint, num, serializeSvg, shapeOverlay, slideNode } from "@pptx-studio/render-svg";
2
+ import { dragHandle } from "@pptx-studio/geometry";
3
+ import { toCss } from "@pptx-studio/paint";
4
+ //#region src/errors.ts
5
+ const RENDER_DOM_ERROR_CODES = ["RENDER_DOM_NO_HOST", "RENDER_DOM_NO_DOCUMENT"];
6
+ var RenderDomError = class extends Error {
7
+ code;
8
+ constructor(code, message) {
9
+ super(message);
10
+ this.name = "RenderDomError";
11
+ this.code = code;
12
+ }
13
+ };
14
+ function isRenderDomError(value) {
15
+ return value instanceof RenderDomError;
16
+ }
17
+ //#endregion
18
+ //#region src/mount.ts
19
+ /**
20
+ * The live renderer: the same node tree as `render-svg`, as real elements.
21
+ *
22
+ * ## Why this is thirty lines of DOM and not a second renderer
23
+ *
24
+ * The plan asks for two renderers "over the same layout engine", so that a
25
+ * thumbnail and the editor cannot disagree about what is on the screen. Sharing
26
+ * a *layout* is not enough for that on its own: emitting a `<linearGradient>`
27
+ * twice, once as text and once as `createElementNS`, is two chances to write the
28
+ * same gradient wrong in two different ways.
29
+ *
30
+ * So the shared surface goes a level deeper. `render-svg` builds a node tree and
31
+ * serialises it; this builds the identical tree and instantiates it. Everything
32
+ * either of them knows about DrawingML is in that tree before either touches it,
33
+ * and the only thing this file decides is which DOM call makes a node.
34
+ *
35
+ * ## The namespace, and the four attributes that need their own
36
+ *
37
+ * `document.createElement` makes an HTML element that happens to be spelled
38
+ * `path`, and it draws nothing at all - silently. Every element here goes
39
+ * through `createElementNS` with the SVG namespace. Attributes are the mirror
40
+ * image: they belong to **no** namespace and must go through `setAttribute`,
41
+ * with the single exception of `xlink:href`, which this package never emits.
42
+ *
43
+ * ## Zoom is a transform, never a re-layout
44
+ *
45
+ * The mounted `<svg>` carries a `viewBox` in EMU and is sized in CSS pixels, so
46
+ * scaling it is one attribute and no arithmetic. The plan's rule is the reason:
47
+ * font metrics are not linear in point size, so a renderer that re-lays out per
48
+ * zoom level changes the line breaks as the user zooms.
49
+ */
50
+ const SVG_NS = "http://www.w3.org/2000/svg";
51
+ /**
52
+ * One node into one element. Exported because the debug overlay wants it too.
53
+ *
54
+ * A number goes through `render-svg`'s own `num`, not `String`. That is the
55
+ * whole "one tree, two renderers" claim in one line: the string emitter rounds
56
+ * to three decimals and normalises `-0`, so a DOM path using `String` would
57
+ * write `117158.34312` where a thumbnail wrote `117158.343`. Every coordinate
58
+ * in a slide render happens to be a whole number of EMU, which is why this went
59
+ * unnoticed until the overlay's text rectangle - the first fractional one -
60
+ * disagreed at the fourth decimal.
61
+ */
62
+ function createNode(document, node) {
63
+ if (node.kind === "text") return document.createTextNode(node.text);
64
+ const el = document.createElementNS(SVG_NS, node.tag);
65
+ for (const [name, value] of Object.entries(node.attrs)) {
66
+ if (value === null || value === void 0) continue;
67
+ el.setAttribute(name, typeof value === "number" ? num(value) : value);
68
+ }
69
+ for (const child of node.children) el.appendChild(createNode(document, child));
70
+ return el;
71
+ }
72
+ function ownerDocument(host, options) {
73
+ const document = options.document ?? host.ownerDocument;
74
+ if (document === null || document === void 0) throw new RenderDomError("RENDER_DOM_NO_DOCUMENT", "the host element has no ownerDocument");
75
+ return document;
76
+ }
77
+ /**
78
+ * Render a sheet into a host element.
79
+ *
80
+ * The host is emptied first. Anything a caller wants to keep beside the slide -
81
+ * the overlay canvas of 2.11, the text layer of 3.8 - is a sibling of the host,
82
+ * not a child of it.
83
+ */
84
+ function mountSlide(host, sheet, size, options = {}) {
85
+ if (host === null || typeof host !== "object" || typeof host.appendChild !== "function") throw new RenderDomError("RENDER_DOM_NO_HOST", "mountSlide needs an element to build into");
86
+ const document = ownerDocument(host, options);
87
+ const rendered = slideNode(sheet, size, options);
88
+ const root = createNode(document, rendered.node);
89
+ host.replaceChildren(root);
90
+ const byId = /* @__PURE__ */ new Map();
91
+ const walk = (node) => {
92
+ const id = node.getAttribute("data-shape");
93
+ if (id !== null && !byId.has(Number(id))) byId.set(Number(id), node);
94
+ for (const child of Array.from(node.children)) walk(child);
95
+ };
96
+ walk(root);
97
+ return {
98
+ root,
99
+ placed: rendered.placed,
100
+ element(cNvPrId) {
101
+ return byId.get(cNvPrId) ?? null;
102
+ },
103
+ resize(width, height) {
104
+ root.setAttribute("width", String(width));
105
+ root.setAttribute("height", String(height));
106
+ },
107
+ unmount() {
108
+ root.remove();
109
+ }
110
+ };
111
+ }
112
+ /**
113
+ * The same slide as a string, from this package.
114
+ *
115
+ * A convenience rather than a second implementation: it calls `render-svg`'s
116
+ * serializer on the same tree `mountSlide` instantiates, which is the property
117
+ * the tests check.
118
+ */
119
+ function renderSlideMarkup(sheet, size, options = {}) {
120
+ return serializeSvg(slideNode(sheet, size, options).node);
121
+ }
122
+ //#endregion
123
+ //#region src/overlay.ts
124
+ /**
125
+ * The debug overlay, live: the same nodes `render-svg` builds, plus a pointer.
126
+ *
127
+ * Sub-phase 2.11. Everything drawn here comes from `shapeOverlay`, so the
128
+ * handle a user grabs is at the position a thumbnail would have drawn it - the
129
+ * overlay is not a second opinion about where the geometry went. What this file
130
+ * adds is the four things a pointer needs and a node tree cannot have: an
131
+ * element to mount into, hit-testing, the drag, and the inverse transform that
132
+ * gets a pointer from the slide into the shape's own space.
133
+ *
134
+ * ## Slide space, shape space, and why the inverse has to be exact
135
+ *
136
+ * A shape's adjust handles live in *its* coordinates: `(0, 0)` at the top left
137
+ * of `a:ext`, before any rotation or mirroring. A pointer arrives in the
138
+ * slide's. `inverseFramePoint` undoes exactly what `frameTransform` did, in the
139
+ * reverse order - so the mirror is undone *after* the rotation, because it was
140
+ * applied *before* it. Get that backwards and handles work perfectly on every
141
+ * unrotated shape and drift on precisely the flipped, turned ones where a user
142
+ * would never guess the renderer was at fault.
143
+ *
144
+ * ## The drag writes adjust values, not positions
145
+ *
146
+ * `dragHandle` inverts the handle's own formula and returns `{ guideName:
147
+ * value }` for the guides that handle controls and nothing else, already
148
+ * rounded to the integers the file will hold. So a drag produces an `a:avLst`
149
+ * edit, the shape is re-evaluated from it, and the handle lands wherever the
150
+ * geometry says it lands - which is why a handle confined to a curve does not
151
+ * follow the pointer off it. That is the correct behaviour and PowerPoint's.
152
+ */
153
+ /**
154
+ * A point in the host `<svg>`'s user space - EMU - from a pointer event.
155
+ *
156
+ * Through the SVG CTM rather than by arithmetic on `getBoundingClientRect`,
157
+ * because the slide may be inside any number of CSS transforms and the browser
158
+ * already knows the composed matrix. `getScreenCTM` returns null for an element
159
+ * that is not being rendered, which a drag cannot reach but a stray event can.
160
+ */
161
+ function slidePoint(root, clientX, clientY) {
162
+ const ctm = root.getScreenCTM();
163
+ if (ctm === null) return null;
164
+ const point = root.createSVGPoint();
165
+ point.x = clientX;
166
+ point.y = clientY;
167
+ const local = point.matrixTransform(ctm.inverse());
168
+ return {
169
+ x: local.x,
170
+ y: local.y
171
+ };
172
+ }
173
+ /**
174
+ * The `a:avLst` a drag leaves behind.
175
+ *
176
+ * The dragged guides replace their namesakes and everything else is kept in
177
+ * place, because an `a:avLst` is an ordered list and a shape with three adjusts
178
+ * whose second one moves must still write three, in order. A guide the shape did
179
+ * not previously declare is appended: the preset's default was in force and the
180
+ * drag has just overridden it.
181
+ */
182
+ function mergeAdjust(avLst, adjust) {
183
+ const seen = /* @__PURE__ */ new Set();
184
+ const merged = avLst.map((guide) => {
185
+ const value = adjust[guide.name];
186
+ if (value === void 0) return guide;
187
+ seen.add(guide.name);
188
+ return {
189
+ name: guide.name,
190
+ fmla: ["val", String(Math.round(value))]
191
+ };
192
+ });
193
+ for (const [name, value] of Object.entries(adjust)) {
194
+ if (seen.has(name)) continue;
195
+ merged.push({
196
+ name,
197
+ fmla: ["val", String(Math.round(value))]
198
+ });
199
+ }
200
+ return merged;
201
+ }
202
+ /**
203
+ * Draw the overlay for one shape into a host `<svg>`, and make its handles drag.
204
+ *
205
+ * The host must be the same `<svg>` the slide is in, because the overlay is
206
+ * emitted in slide coordinates and carries the shape's own transform - the same
207
+ * arrangement `mountSlide` produces, so an editor appends this to that root.
208
+ */
209
+ function mountOverlay(root, placed, options = {}) {
210
+ if (root === null || typeof root !== "object" || typeof root.appendChild !== "function") throw new RenderDomError("RENDER_DOM_NO_HOST", "mountOverlay needs the slide <svg> to build in");
211
+ const document = options.document ?? root.ownerDocument;
212
+ if (document === null || document === void 0) throw new RenderDomError("RENDER_DOM_NO_DOCUMENT", "the host element has no ownerDocument");
213
+ let current = placed;
214
+ let overlay = shapeOverlay(current, options);
215
+ let group = createNode(document, overlay.node);
216
+ root.appendChild(group);
217
+ let active = null;
218
+ let pointer = null;
219
+ function edit(at, index) {
220
+ const source = current.geometrySource;
221
+ const handle = overlay.handles.find((h) => h.index === index);
222
+ if (source === null || handle === void 0) return null;
223
+ const local = inverseFramePoint(current.frame, at);
224
+ const adjust = dragHandle(source.geometry, {
225
+ w: current.frame.cx,
226
+ h: current.frame.cy
227
+ }, handle.handle, local, { adjust: source.adjust });
228
+ if (Object.keys(adjust).length === 0) return null;
229
+ return {
230
+ handle: index,
231
+ adjust,
232
+ avLst: mergeAdjust(source.adjust, adjust)
233
+ };
234
+ }
235
+ /**
236
+ * Hold the pointer for the duration of the drag.
237
+ *
238
+ * On the root rather than on the handle, because the handle element is
239
+ * replaced on every update and a captured element that leaves the tree takes
240
+ * the capture with it - the drag would stop the moment the shape redrew.
241
+ *
242
+ * `setPointerCapture` throws `NotFoundError` for a pointer that is no longer
243
+ * active, which is reachable whenever the button is released between the
244
+ * event being queued and this running. Losing the capture degrades the drag
245
+ * to one that stops at the window edge; throwing would abandon it entirely.
246
+ */
247
+ function capture(pointerId) {
248
+ try {
249
+ root.setPointerCapture(pointerId);
250
+ } catch {}
251
+ }
252
+ function onPointerDown(event) {
253
+ const owner = event.target?.closest?.("[data-role=\"handle\"]");
254
+ const index = owner?.getAttribute("data-handle");
255
+ if (index === null || index === void 0 || owner?.getAttribute("data-axes") === "") return;
256
+ active = Number(index);
257
+ pointer = event.pointerId;
258
+ capture(event.pointerId);
259
+ event.preventDefault();
260
+ }
261
+ function onPointerMove(event) {
262
+ if (active === null || event.pointerId !== pointer) return;
263
+ const at = slidePoint(root, event.clientX, event.clientY);
264
+ if (at === null) return;
265
+ const result = edit(at, active);
266
+ if (result !== null) options.onChange?.(result);
267
+ }
268
+ function onPointerUp(event) {
269
+ if (active === null || event.pointerId !== pointer) return;
270
+ const at = slidePoint(root, event.clientX, event.clientY);
271
+ const result = at === null ? null : edit(at, active);
272
+ active = null;
273
+ pointer = null;
274
+ if (root.hasPointerCapture(event.pointerId)) root.releasePointerCapture(event.pointerId);
275
+ if (result !== null) options.onCommit?.(result);
276
+ }
277
+ const listening = new AbortController();
278
+ const on = { signal: listening.signal };
279
+ root.addEventListener("pointerdown", onPointerDown, on);
280
+ root.addEventListener("pointermove", onPointerMove, on);
281
+ root.addEventListener("pointerup", onPointerUp, on);
282
+ root.addEventListener("pointercancel", onPointerUp, on);
283
+ return {
284
+ get root() {
285
+ return group;
286
+ },
287
+ get overlay() {
288
+ return overlay;
289
+ },
290
+ update(next) {
291
+ current = next;
292
+ overlay = shapeOverlay(current, options);
293
+ const replacement = createNode(document, overlay.node);
294
+ group.replaceWith(replacement);
295
+ group = replacement;
296
+ },
297
+ dragging() {
298
+ return active !== null;
299
+ },
300
+ unmount() {
301
+ listening.abort();
302
+ group.remove();
303
+ }
304
+ };
305
+ }
306
+ //#endregion
307
+ //#region src/text/layer.ts
308
+ function px(value) {
309
+ return `${String(Math.round(value * 1e3) / 1e3)}px`;
310
+ }
311
+ /** Through `toCss`, because `Rgba` channels are 0..1 and not bytes. */
312
+ function css(color) {
313
+ return color === null ? "inherit" : toCss(color);
314
+ }
315
+ /** The dash pattern a rule is painted with, in multiples of its thickness. */
316
+ const DASHES = {
317
+ dotted: [1, 2],
318
+ dashed: [4, 3],
319
+ longDashed: [8, 3],
320
+ dotDash: [
321
+ 4,
322
+ 3,
323
+ 1,
324
+ 3
325
+ ],
326
+ dotDotDash: [
327
+ 1,
328
+ 3,
329
+ 1,
330
+ 3,
331
+ 4,
332
+ 3
333
+ ]
334
+ };
335
+ function ruleElement(document, rule, x, baseline, color) {
336
+ const el = document.createElement("div");
337
+ const style = el.style;
338
+ style.position = "absolute";
339
+ style.left = px(x + rule.leftPt);
340
+ style.top = px(baseline + rule.topPt);
341
+ style.width = px(rule.widthPt);
342
+ style.height = px(rule.thickness);
343
+ const dash = DASHES[rule.pattern];
344
+ if (dash === void 0) {
345
+ style.background = color;
346
+ return el;
347
+ }
348
+ const on = (dash[0] ?? 1) * rule.thickness;
349
+ const off = (dash[1] ?? 1) * rule.thickness;
350
+ style.background = `repeating-linear-gradient(to right, ${color} 0 ${px(on)}, transparent ${px(on)} ${px(on + off)})`;
351
+ return el;
352
+ }
353
+ function pieceElement(document, piece) {
354
+ const el = document.createElement("span");
355
+ const style = el.style;
356
+ style.fontFamily = piece.cssFamily;
357
+ style.fontSize = px(piece.font.sz / 100);
358
+ if (piece.font.bold === true) style.fontWeight = "bold";
359
+ if (piece.font.italic === true) style.fontStyle = "italic";
360
+ if (piece.font.spc !== void 0 && piece.font.spc !== 0) style.letterSpacing = px(piece.font.spc / 100);
361
+ style.color = css(piece.color);
362
+ if (piece.highlight !== null) style.background = css(piece.highlight);
363
+ if (piece.risePt !== 0) {
364
+ style.position = "relative";
365
+ style.top = px(-piece.risePt);
366
+ }
367
+ el.textContent = piece.text;
368
+ return el;
369
+ }
370
+ /**
371
+ * One glyph stood upright inside a turned line, placed from its own pen.
372
+ *
373
+ * The span is laid out unrotated with its baseline `sizePt` below its top - what
374
+ * `UprightGlyph.strutPt` solves for - and then turned a quarter back about that
375
+ * point, so the pen lands where the SVG emitter puts the same glyph.
376
+ */
377
+ function uprightElement(document, piece, glyph) {
378
+ const el = pieceElement(document, piece);
379
+ const style = el.style;
380
+ const sizePt = piece.font.sz / 100;
381
+ style.position = "absolute";
382
+ style.left = px(glyph.alongPt - piece.risePt);
383
+ style.top = px(glyph.acrossPt - sizePt);
384
+ style.lineHeight = px(glyph.strutPt);
385
+ style.transformOrigin = `0 ${px(sizePt)}`;
386
+ style.transform = "rotate(-90deg)";
387
+ el.textContent = glyph.text;
388
+ return el;
389
+ }
390
+ function lineElement(document, line) {
391
+ const el = document.createElement("div");
392
+ const style = el.style;
393
+ style.position = "absolute";
394
+ style.left = px(line.leftPt);
395
+ style.top = px(line.topPt);
396
+ style.width = px(line.widthPt);
397
+ style.height = px(line.heightPt);
398
+ style.lineHeight = px(line.strutPt);
399
+ style.whiteSpace = "pre";
400
+ if (line.wordSpacingPt !== 0) style.wordSpacing = px(line.wordSpacingPt);
401
+ for (const piece of line.pieces) if (piece.upright.length > 0) for (const glyph of piece.upright) el.appendChild(uprightElement(document, piece, glyph));
402
+ else el.appendChild(pieceElement(document, piece));
403
+ const baseline = line.baselinePt - line.topPt;
404
+ for (const piece of line.pieces) {
405
+ const color = css(piece.color);
406
+ for (const rule of piece.rules) el.appendChild(ruleElement(document, rule, piece.leftPt, baseline - piece.risePt, color));
407
+ }
408
+ return el;
409
+ }
410
+ function blockElement(document, entry) {
411
+ const el = document.createElement("div");
412
+ const style = el.style;
413
+ style.position = "absolute";
414
+ style.left = px(entry.leftPt);
415
+ style.top = px(entry.topPt);
416
+ style.width = px(entry.widthPt);
417
+ style.height = px(entry.heightPt);
418
+ if (entry.block.turnDeg !== 0) {
419
+ style.transformOrigin = "50% 50%";
420
+ style.transform = `rotate(${String(entry.block.turnDeg)}deg)`;
421
+ }
422
+ el.setAttribute("data-text", String(entry.cNvPrId));
423
+ if (entry.name !== void 0 && entry.name !== "") el.setAttribute("data-name", entry.name);
424
+ for (const line of entry.block.lines) el.appendChild(lineElement(document, line));
425
+ return el;
426
+ }
427
+ /**
428
+ * Mount a text layer into a host, over a slide of `size` points.
429
+ *
430
+ * The host is emptied first. The layer is sized in points and scaled with one
431
+ * transform, which is the plan's rule: font metrics are not linear in point
432
+ * size, so a renderer that re-laid out per zoom would change its line breaks as
433
+ * the user zoomed.
434
+ */
435
+ function mountTextLayer(host, blocks, size, options = {}) {
436
+ if (host === null || typeof host !== "object" || typeof host.appendChild !== "function") throw new RenderDomError("RENDER_DOM_NO_HOST", "mountTextLayer needs an element to build into");
437
+ const document = options.document ?? host.ownerDocument;
438
+ if (document === null || document === void 0) throw new RenderDomError("RENDER_DOM_NO_DOCUMENT", "the host element has no ownerDocument");
439
+ const root = document.createElement("div");
440
+ const style = root.style;
441
+ style.position = "absolute";
442
+ style.left = "0";
443
+ style.top = "0";
444
+ style.width = px(size.widthPt);
445
+ style.height = px(size.heightPt);
446
+ style.transformOrigin = "0 0";
447
+ style.pointerEvents = options.interactive === true ? "auto" : "none";
448
+ root.setAttribute("data-role", "text-layer");
449
+ const byId = /* @__PURE__ */ new Map();
450
+ for (const entry of blocks) {
451
+ if (entry.block.lines.length === 0) continue;
452
+ const el = blockElement(document, entry);
453
+ byId.set(entry.cNvPrId, el);
454
+ root.appendChild(el);
455
+ }
456
+ const apply = (scale) => {
457
+ style.transform = scale === 1 ? "" : `scale(${String(scale)})`;
458
+ };
459
+ apply(options.scale ?? 1);
460
+ host.replaceChildren(root);
461
+ return {
462
+ root,
463
+ element(cNvPrId) {
464
+ return byId.get(cNvPrId) ?? null;
465
+ },
466
+ resize(scale) {
467
+ apply(scale);
468
+ },
469
+ unmount() {
470
+ root.remove();
471
+ }
472
+ };
473
+ }
474
+ //#endregion
475
+ export { RENDER_DOM_ERROR_CODES, RenderDomError, createNode, isRenderDomError, mergeAdjust, mountOverlay, mountSlide, mountTextLayer, renderSlideMarkup, slidePoint };
476
+
477
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/errors.ts","../src/mount.ts","../src/overlay.ts","../src/text/layer.ts"],"sourcesContent":["/**\n * Every failure in this package is a `RenderDomError` with a code.\n *\n * There are only two, and both mean the *caller* asked for something\n * impossible: a host that is not an element, or an element with no document to\n * build in. Everything about the slide itself is `render-svg`'s to refuse, and\n * this package never re-decides it.\n */\n\nexport type RenderDomErrorCode =\n /** `mountSlide` was handed something that is not an element. */\n | 'RENDER_DOM_NO_HOST'\n /** The host is detached and no `document` was passed. */\n | 'RENDER_DOM_NO_DOCUMENT';\n\nexport const RENDER_DOM_ERROR_CODES: readonly RenderDomErrorCode[] = [\n 'RENDER_DOM_NO_HOST',\n 'RENDER_DOM_NO_DOCUMENT',\n];\n\nexport class RenderDomError extends Error {\n readonly code: RenderDomErrorCode;\n\n constructor(code: RenderDomErrorCode, message: string) {\n super(message);\n this.name = 'RenderDomError';\n this.code = code;\n }\n}\n\nexport function isRenderDomError(value: unknown): value is RenderDomError {\n return value instanceof RenderDomError;\n}\n","/**\n * The live renderer: the same node tree as `render-svg`, as real elements.\n *\n * ## Why this is thirty lines of DOM and not a second renderer\n *\n * The plan asks for two renderers \"over the same layout engine\", so that a\n * thumbnail and the editor cannot disagree about what is on the screen. Sharing\n * a *layout* is not enough for that on its own: emitting a `<linearGradient>`\n * twice, once as text and once as `createElementNS`, is two chances to write the\n * same gradient wrong in two different ways.\n *\n * So the shared surface goes a level deeper. `render-svg` builds a node tree and\n * serialises it; this builds the identical tree and instantiates it. Everything\n * either of them knows about DrawingML is in that tree before either touches it,\n * and the only thing this file decides is which DOM call makes a node.\n *\n * ## The namespace, and the four attributes that need their own\n *\n * `document.createElement` makes an HTML element that happens to be spelled\n * `path`, and it draws nothing at all - silently. Every element here goes\n * through `createElementNS` with the SVG namespace. Attributes are the mirror\n * image: they belong to **no** namespace and must go through `setAttribute`,\n * with the single exception of `xlink:href`, which this package never emits.\n *\n * ## Zoom is a transform, never a re-layout\n *\n * The mounted `<svg>` carries a `viewBox` in EMU and is sized in CSS pixels, so\n * scaling it is one attribute and no arithmetic. The plan's rule is the reason:\n * font metrics are not linear in point size, so a renderer that re-lays out per\n * zoom level changes the line breaks as the user zooms.\n */\n\nimport {\n num,\n serializeSvg,\n slideNode,\n type Placed,\n type RenderOptions,\n type SlideSize,\n type SvgElement,\n type SvgNode,\n} from '@pptx-studio/render-svg';\nimport type { Sheet } from '@pptx-studio/model';\n\nimport { RenderDomError } from './errors.js';\n\nconst SVG_NS = 'http://www.w3.org/2000/svg';\n\n/**\n * One node into one element. Exported because the debug overlay wants it too.\n *\n * A number goes through `render-svg`'s own `num`, not `String`. That is the\n * whole \"one tree, two renderers\" claim in one line: the string emitter rounds\n * to three decimals and normalises `-0`, so a DOM path using `String` would\n * write `117158.34312` where a thumbnail wrote `117158.343`. Every coordinate\n * in a slide render happens to be a whole number of EMU, which is why this went\n * unnoticed until the overlay's text rectangle - the first fractional one -\n * disagreed at the fourth decimal.\n */\nexport function createNode(document: Document, node: SvgNode): Node {\n if (node.kind === 'text') return document.createTextNode(node.text);\n const el = document.createElementNS(SVG_NS, node.tag);\n for (const [name, value] of Object.entries(node.attrs)) {\n if (value === null || value === undefined) continue;\n el.setAttribute(name, typeof value === 'number' ? num(value) : value);\n }\n for (const child of node.children) el.appendChild(createNode(document, child));\n return el;\n}\n\nexport interface MountOptions extends RenderOptions {\n /**\n * The `document` to build in.\n *\n * Passed rather than reached for, because this package must work in a Web\n * Worker with an `OffscreenCanvas` and no global `document` - and because a\n * test that has to stub a global is a test that can pass for the wrong reason.\n */\n readonly document?: Document;\n}\n\nexport interface MountedSlide {\n readonly root: SVGSVGElement;\n readonly placed: readonly Placed[];\n /** The element drawing a given `p:cNvPr/@id`, for hit-testing and selection. */\n element(cNvPrId: number): SVGElement | null;\n /** Resize without re-laying anything out: the viewBox does the work. */\n resize(width: number, height: number): void;\n /** Detach from the host. The nodes stay valid, so a caller may re-attach. */\n unmount(): void;\n}\n\nfunction ownerDocument(host: Element, options: MountOptions): Document {\n const document = options.document ?? host.ownerDocument;\n\n if (document === null || document === undefined) {\n throw new RenderDomError('RENDER_DOM_NO_DOCUMENT', 'the host element has no ownerDocument');\n }\n return document;\n}\n\n/**\n * Render a sheet into a host element.\n *\n * The host is emptied first. Anything a caller wants to keep beside the slide -\n * the overlay canvas of 2.11, the text layer of 3.8 - is a sibling of the host,\n * not a child of it.\n */\nexport function mountSlide(\n host: Element,\n sheet: Sheet,\n size: SlideSize,\n options: MountOptions = {},\n): MountedSlide {\n // `typeof null` is 'object', so the null check has to come first or the\n // guard reads a property off it and throws the wrong error.\n if (host === null || typeof host !== 'object' || typeof host.appendChild !== 'function') {\n throw new RenderDomError('RENDER_DOM_NO_HOST', 'mountSlide needs an element to build into');\n }\n const document = ownerDocument(host, options);\n const rendered = slideNode(sheet, size, options);\n const root = createNode(document, rendered.node) as SVGSVGElement;\n\n host.replaceChildren(root);\n\n const byId = new Map<number, SVGElement>();\n const walk = (node: Element): void => {\n const id = node.getAttribute('data-shape');\n if (id !== null && !byId.has(Number(id))) byId.set(Number(id), node as SVGElement);\n for (const child of Array.from(node.children)) walk(child);\n };\n walk(root);\n\n return {\n root,\n placed: rendered.placed,\n element(cNvPrId: number): SVGElement | null {\n return byId.get(cNvPrId) ?? null;\n },\n resize(width: number, height: number): void {\n root.setAttribute('width', String(width));\n root.setAttribute('height', String(height));\n },\n unmount(): void {\n root.remove();\n },\n };\n}\n\n/**\n * The same slide as a string, from this package.\n *\n * A convenience rather than a second implementation: it calls `render-svg`'s\n * serializer on the same tree `mountSlide` instantiates, which is the property\n * the tests check.\n */\nexport function renderSlideMarkup(\n sheet: Sheet,\n size: SlideSize,\n options: RenderOptions = {},\n): string {\n return serializeSvg(slideNode(sheet, size, options).node);\n}\n\nexport type { SvgElement };\n","/**\n * The debug overlay, live: the same nodes `render-svg` builds, plus a pointer.\n *\n * Sub-phase 2.11. Everything drawn here comes from `shapeOverlay`, so the\n * handle a user grabs is at the position a thumbnail would have drawn it - the\n * overlay is not a second opinion about where the geometry went. What this file\n * adds is the four things a pointer needs and a node tree cannot have: an\n * element to mount into, hit-testing, the drag, and the inverse transform that\n * gets a pointer from the slide into the shape's own space.\n *\n * ## Slide space, shape space, and why the inverse has to be exact\n *\n * A shape's adjust handles live in *its* coordinates: `(0, 0)` at the top left\n * of `a:ext`, before any rotation or mirroring. A pointer arrives in the\n * slide's. `inverseFramePoint` undoes exactly what `frameTransform` did, in the\n * reverse order - so the mirror is undone *after* the rotation, because it was\n * applied *before* it. Get that backwards and handles work perfectly on every\n * unrotated shape and drift on precisely the flipped, turned ones where a user\n * would never guess the renderer was at fault.\n *\n * ## The drag writes adjust values, not positions\n *\n * `dragHandle` inverts the handle's own formula and returns `{ guideName:\n * value }` for the guides that handle controls and nothing else, already\n * rounded to the integers the file will hold. So a drag produces an `a:avLst`\n * edit, the shape is re-evaluated from it, and the handle lands wherever the\n * geometry says it lands - which is why a handle confined to a curve does not\n * follow the pointer off it. That is the correct behaviour and PowerPoint's.\n */\n\nimport { dragHandle, type Point, type PresetGuide } from '@pptx-studio/geometry';\nimport {\n inverseFramePoint,\n shapeOverlay,\n type OverlayOptions,\n type Placed,\n type ShapeOverlay,\n} from '@pptx-studio/render-svg';\n\nimport { RenderDomError } from './errors.js';\nimport { createNode } from './mount.js';\n\n/** What a completed drag produced. */\nexport interface HandleEdit {\n /** Which `a:ahLst` entry was dragged. */\n readonly handle: number;\n /** The adjust values this handle controls, rounded as the file will hold them. */\n readonly adjust: Readonly<Record<string, number>>;\n /** The shape's whole `a:avLst` after the edit, ready to write back. */\n readonly avLst: readonly PresetGuide[];\n}\n\nexport interface OverlayMountOptions extends OverlayOptions {\n readonly document?: Document;\n /** Called on every pointer move during a drag, and once more on release. */\n onChange?: (edit: HandleEdit) => void;\n /** Called once on release. */\n onCommit?: (edit: HandleEdit) => void;\n}\n\nexport interface MountedOverlay {\n readonly root: SVGGElement;\n readonly overlay: ShapeOverlay;\n /** Redraw against a new placement - after a drag, or a resize. */\n update(placed: Placed): void;\n /** True while a handle is being dragged. */\n dragging(): boolean;\n unmount(): void;\n}\n\n/**\n * A point in the host `<svg>`'s user space - EMU - from a pointer event.\n *\n * Through the SVG CTM rather than by arithmetic on `getBoundingClientRect`,\n * because the slide may be inside any number of CSS transforms and the browser\n * already knows the composed matrix. `getScreenCTM` returns null for an element\n * that is not being rendered, which a drag cannot reach but a stray event can.\n */\nexport function slidePoint(root: SVGSVGElement, clientX: number, clientY: number): Point | null {\n const ctm = root.getScreenCTM();\n if (ctm === null) return null;\n const point = root.createSVGPoint();\n point.x = clientX;\n point.y = clientY;\n const local = point.matrixTransform(ctm.inverse());\n return { x: local.x, y: local.y };\n}\n\n/**\n * The `a:avLst` a drag leaves behind.\n *\n * The dragged guides replace their namesakes and everything else is kept in\n * place, because an `a:avLst` is an ordered list and a shape with three adjusts\n * whose second one moves must still write three, in order. A guide the shape did\n * not previously declare is appended: the preset's default was in force and the\n * drag has just overridden it.\n */\nexport function mergeAdjust(\n avLst: readonly PresetGuide[],\n adjust: Readonly<Record<string, number>>,\n): readonly PresetGuide[] {\n const seen = new Set<string>();\n const merged: PresetGuide[] = avLst.map((guide) => {\n const value = adjust[guide.name];\n if (value === undefined) return guide;\n seen.add(guide.name);\n return { name: guide.name, fmla: ['val', String(Math.round(value))] };\n });\n for (const [name, value] of Object.entries(adjust)) {\n if (seen.has(name)) continue;\n merged.push({ name, fmla: ['val', String(Math.round(value))] });\n }\n return merged;\n}\n\n/**\n * Draw the overlay for one shape into a host `<svg>`, and make its handles drag.\n *\n * The host must be the same `<svg>` the slide is in, because the overlay is\n * emitted in slide coordinates and carries the shape's own transform - the same\n * arrangement `mountSlide` produces, so an editor appends this to that root.\n */\nexport function mountOverlay(\n root: SVGSVGElement,\n placed: Placed,\n options: OverlayMountOptions = {},\n): MountedOverlay {\n if (root === null || typeof root !== 'object' || typeof root.appendChild !== 'function') {\n throw new RenderDomError(\n 'RENDER_DOM_NO_HOST',\n 'mountOverlay needs the slide <svg> to build in',\n );\n }\n const document = options.document ?? root.ownerDocument;\n if (document === null || document === undefined) {\n throw new RenderDomError('RENDER_DOM_NO_DOCUMENT', 'the host element has no ownerDocument');\n }\n\n let current = placed;\n let overlay = shapeOverlay(current, options);\n let group = createNode(document, overlay.node) as SVGGElement;\n root.appendChild(group);\n\n let active: number | null = null;\n let pointer: number | null = null;\n\n function edit(at: Point, index: number): HandleEdit | null {\n const source = current.geometrySource;\n const handle = overlay.handles.find((h) => h.index === index);\n if (source === null || handle === undefined) return null;\n const local = inverseFramePoint(current.frame, at);\n const adjust = dragHandle(\n source.geometry,\n { w: current.frame.cx, h: current.frame.cy },\n handle.handle,\n local,\n { adjust: source.adjust },\n );\n if (Object.keys(adjust).length === 0) return null;\n return { handle: index, adjust, avLst: mergeAdjust(source.adjust, adjust) };\n }\n\n /**\n * Hold the pointer for the duration of the drag.\n *\n * On the root rather than on the handle, because the handle element is\n * replaced on every update and a captured element that leaves the tree takes\n * the capture with it - the drag would stop the moment the shape redrew.\n *\n * `setPointerCapture` throws `NotFoundError` for a pointer that is no longer\n * active, which is reachable whenever the button is released between the\n * event being queued and this running. Losing the capture degrades the drag\n * to one that stops at the window edge; throwing would abandon it entirely.\n */\n function capture(pointerId: number): void {\n try {\n root.setPointerCapture(pointerId);\n } catch {\n // No capture, and the drag continues without it.\n }\n }\n\n function onPointerDown(event: PointerEvent): void {\n const target = event.target as Element | null;\n const owner = target?.closest?.('[data-role=\"handle\"]');\n const index = owner?.getAttribute('data-handle');\n // A handle with no `gdRef` at all controls nothing and must not swallow\n // the gesture. No preset has one - all 120 with an `a:ahLst` declare at\n // least one axis - but an `a:custGeom` in a real file may.\n if (index === null || index === undefined || owner?.getAttribute('data-axes') === '') return;\n active = Number(index);\n pointer = event.pointerId;\n capture(event.pointerId);\n event.preventDefault();\n }\n\n function onPointerMove(event: PointerEvent): void {\n if (active === null || event.pointerId !== pointer) return;\n const at = slidePoint(root, event.clientX, event.clientY);\n if (at === null) return;\n const result = edit(at, active);\n if (result !== null) options.onChange?.(result);\n }\n\n function onPointerUp(event: PointerEvent): void {\n if (active === null || event.pointerId !== pointer) return;\n const at = slidePoint(root, event.clientX, event.clientY);\n const result = at === null ? null : edit(at, active);\n active = null;\n pointer = null;\n if (root.hasPointerCapture(event.pointerId)) root.releasePointerCapture(event.pointerId);\n if (result !== null) options.onCommit?.(result);\n }\n\n // One signal for all four, so `unmount` cannot detach three of them and\n // leave the fourth listening on a root that outlives this overlay. Four\n // paired `removeEventListener` calls are four chances to forget one, and a\n // half-removed listener is invisible until a second overlay is mounted on the\n // same slide and the dead one starts editing a shape that is no longer there.\n const listening = new AbortController();\n const on = { signal: listening.signal };\n root.addEventListener('pointerdown', onPointerDown, on);\n root.addEventListener('pointermove', onPointerMove, on);\n root.addEventListener('pointerup', onPointerUp, on);\n root.addEventListener('pointercancel', onPointerUp, on);\n\n return {\n get root(): SVGGElement {\n return group;\n },\n get overlay(): ShapeOverlay {\n return overlay;\n },\n update(next: Placed): void {\n current = next;\n overlay = shapeOverlay(current, options);\n const replacement = createNode(document, overlay.node) as SVGGElement;\n group.replaceWith(replacement);\n group = replacement;\n },\n dragging(): boolean {\n return active !== null;\n },\n unmount(): void {\n listening.abort();\n group.remove();\n },\n };\n}\n","/**\n * The HTML text layer: the same lines as the SVG, as real text nodes.\n *\n * The plan's bet 4 asks for HTML text beside the SVG geometry, and the reason is\n * everything a `<text>` element cannot do: shaping, bidi, an IME, a selection a\n * user can drag, a screen reader. This draws from the identical `TextBlock`\n * `render-svg` emits, so the two cannot disagree about a line box - which is\n * sub-phase 3.8's own verification.\n *\n * ## Why the line carries an odd `line-height`\n *\n * A browser puts a baseline a half-leading plus an ascent below the line box and\n * PowerPoint puts it at the face's own share of it. `TextLine` carries the\n * `line-height` that reconciles the two, computed once in the layout pass. It\n * lands within one CSS pixel and no closer, because Chromium rounds a font's\n * ascent to a whole pixel before it lays a line out - which is why the SVG\n * emitter, where the baseline is a coordinate, is the one that thumbnails and\n * exports use.\n */\n\nimport type {\n PieceRule,\n TextBlock,\n TextLine,\n TextPiece,\n UprightGlyph,\n} from '@pptx-studio/render-svg';\nimport { toCss, type Rgba } from '@pptx-studio/paint';\n\nimport { RenderDomError } from '../errors.js';\n\n/** A slide's size in points, which is what a block's coordinates are in. */\nexport interface LayerSize {\n readonly widthPt: number;\n readonly heightPt: number;\n}\n\n/** One shape's text, with the frame it is drawn in. */\nexport interface LayerBlock {\n readonly block: TextBlock;\n /** The shape's frame, in points on the slide. */\n readonly leftPt: number;\n readonly topPt: number;\n readonly widthPt: number;\n readonly heightPt: number;\n /** `p:cNvPr/@id`, so a caller can find the element for a shape. */\n readonly cNvPrId: number;\n readonly name?: string | undefined;\n}\n\nexport interface TextLayerOptions {\n readonly document?: Document | undefined;\n /** CSS pixels per point. One transform, never a re-layout. */\n readonly scale?: number | undefined;\n /** Let the layer take pointer events. Off by default: the SVG hit-tests. */\n readonly interactive?: boolean | undefined;\n}\n\nexport interface MountedTextLayer {\n readonly root: HTMLElement;\n /** The element holding one shape's text. */\n element(cNvPrId: number): HTMLElement | null;\n /** Rescale without re-laying anything out. */\n resize(scale: number): void;\n unmount(): void;\n}\n\nfunction px(value: number): string {\n // Three decimals of a point is about a nanometre, and matches `render-svg`'s\n // own rounding, so the two layers round identically.\n return `${String(Math.round(value * 1000) / 1000)}px`;\n}\n\n/** Through `toCss`, because `Rgba` channels are 0..1 and not bytes. */\nfunction css(color: Rgba | null): string {\n return color === null ? 'inherit' : toCss(color);\n}\n\n/** The dash pattern a rule is painted with, in multiples of its thickness. */\nconst DASHES: Readonly<Record<string, readonly number[]>> = {\n dotted: [1, 2],\n dashed: [4, 3],\n longDashed: [8, 3],\n dotDash: [4, 3, 1, 3],\n dotDotDash: [1, 3, 1, 3, 4, 3],\n};\n\nfunction ruleElement(\n document: Document,\n rule: PieceRule,\n x: number,\n baseline: number,\n color: string,\n): HTMLElement {\n const el = document.createElement('div');\n const style = el.style;\n style.position = 'absolute';\n style.left = px(x + rule.leftPt);\n style.top = px(baseline + rule.topPt);\n style.width = px(rule.widthPt);\n style.height = px(rule.thickness);\n const dash = DASHES[rule.pattern];\n if (dash === undefined) {\n style.background = color;\n return el;\n }\n // A dashed rule as a repeating gradient, so it stays one element and scales\n // with the layer instead of being redrawn.\n const on = (dash[0] ?? 1) * rule.thickness;\n const off = (dash[1] ?? 1) * rule.thickness;\n style.background = `repeating-linear-gradient(to right, ${color} 0 ${px(on)}, transparent ${px(on)} ${px(on + off)})`;\n return el;\n}\n\nfunction pieceElement(document: Document, piece: TextPiece): HTMLElement {\n const el = document.createElement('span');\n const style = el.style;\n style.fontFamily = piece.cssFamily;\n style.fontSize = px(piece.font.sz / 100);\n if (piece.font.bold === true) style.fontWeight = 'bold';\n if (piece.font.italic === true) style.fontStyle = 'italic';\n if (piece.font.spc !== undefined && piece.font.spc !== 0) {\n style.letterSpacing = px(piece.font.spc / 100);\n }\n style.color = css(piece.color);\n if (piece.highlight !== null) style.background = css(piece.highlight);\n if (piece.risePt !== 0) {\n style.position = 'relative';\n style.top = px(-piece.risePt);\n }\n el.textContent = piece.text;\n return el;\n}\n\n/**\n * One glyph stood upright inside a turned line, placed from its own pen.\n *\n * The span is laid out unrotated with its baseline `sizePt` below its top - what\n * `UprightGlyph.strutPt` solves for - and then turned a quarter back about that\n * point, so the pen lands where the SVG emitter puts the same glyph.\n */\nfunction uprightElement(document: Document, piece: TextPiece, glyph: UprightGlyph): HTMLElement {\n const el = pieceElement(document, piece);\n const style = el.style;\n const sizePt = piece.font.sz / 100;\n style.position = 'absolute';\n style.left = px(glyph.alongPt - piece.risePt);\n style.top = px(glyph.acrossPt - sizePt);\n style.lineHeight = px(glyph.strutPt);\n style.transformOrigin = `0 ${px(sizePt)}`;\n style.transform = 'rotate(-90deg)';\n el.textContent = glyph.text;\n return el;\n}\n\nfunction lineElement(document: Document, line: TextLine): HTMLElement {\n const el = document.createElement('div');\n const style = el.style;\n style.position = 'absolute';\n style.left = px(line.leftPt);\n style.top = px(line.topPt);\n style.width = px(line.widthPt);\n style.height = px(line.heightPt);\n style.lineHeight = px(line.strutPt);\n style.whiteSpace = 'pre';\n if (line.wordSpacingPt !== 0) style.wordSpacing = px(line.wordSpacingPt);\n\n for (const piece of line.pieces) {\n if (piece.upright.length > 0) {\n for (const glyph of piece.upright) el.appendChild(uprightElement(document, piece, glyph));\n } else el.appendChild(pieceElement(document, piece));\n }\n\n const baseline = line.baselinePt - line.topPt;\n for (const piece of line.pieces) {\n const color = css(piece.color);\n for (const rule of piece.rules) {\n el.appendChild(ruleElement(document, rule, piece.leftPt, baseline - piece.risePt, color));\n }\n }\n return el;\n}\n\nfunction blockElement(document: Document, entry: LayerBlock): HTMLElement {\n const el = document.createElement('div');\n const style = el.style;\n style.position = 'absolute';\n style.left = px(entry.leftPt);\n style.top = px(entry.topPt);\n style.width = px(entry.widthPt);\n style.height = px(entry.heightPt);\n if (entry.block.turnDeg !== 0) {\n style.transformOrigin = '50% 50%';\n style.transform = `rotate(${String(entry.block.turnDeg)}deg)`;\n }\n el.setAttribute('data-text', String(entry.cNvPrId));\n if (entry.name !== undefined && entry.name !== '') el.setAttribute('data-name', entry.name);\n for (const line of entry.block.lines) el.appendChild(lineElement(document, line));\n return el;\n}\n\n/**\n * Mount a text layer into a host, over a slide of `size` points.\n *\n * The host is emptied first. The layer is sized in points and scaled with one\n * transform, which is the plan's rule: font metrics are not linear in point\n * size, so a renderer that re-laid out per zoom would change its line breaks as\n * the user zoomed.\n */\nexport function mountTextLayer(\n host: Element,\n blocks: readonly LayerBlock[],\n size: LayerSize,\n options: TextLayerOptions = {},\n): MountedTextLayer {\n if (host === null || typeof host !== 'object' || typeof host.appendChild !== 'function') {\n throw new RenderDomError('RENDER_DOM_NO_HOST', 'mountTextLayer needs an element to build into');\n }\n const document = options.document ?? host.ownerDocument;\n if (document === null || document === undefined) {\n throw new RenderDomError('RENDER_DOM_NO_DOCUMENT', 'the host element has no ownerDocument');\n }\n\n const root = document.createElement('div');\n const style = root.style;\n style.position = 'absolute';\n style.left = '0';\n style.top = '0';\n style.width = px(size.widthPt);\n style.height = px(size.heightPt);\n style.transformOrigin = '0 0';\n style.pointerEvents = options.interactive === true ? 'auto' : 'none';\n root.setAttribute('data-role', 'text-layer');\n\n const byId = new Map<number, HTMLElement>();\n for (const entry of blocks) {\n if (entry.block.lines.length === 0) continue;\n const el = blockElement(document, entry);\n byId.set(entry.cNvPrId, el);\n root.appendChild(el);\n }\n\n const apply = (scale: number): void => {\n style.transform = scale === 1 ? '' : `scale(${String(scale)})`;\n };\n apply(options.scale ?? 1);\n host.replaceChildren(root);\n\n return {\n root,\n element(cNvPrId: number): HTMLElement | null {\n return byId.get(cNvPrId) ?? null;\n },\n resize(scale: number): void {\n apply(scale);\n },\n unmount(): void {\n root.remove();\n },\n };\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@pptx-studio/render-dom",
3
+ "version": "0.0.0",
4
+ "description": "The live DOM renderer: one element per shape, over the same layout engine as render-svg",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Dewiride-Open-Source/Dewiride-PPTX-Studio.git",
11
+ "directory": "packages/render-dom"
12
+ },
13
+ "homepage": "https://github.com/Dewiride-Open-Source/Dewiride-PPTX-Studio/tree/main/packages/render-dom#readme",
14
+ "bugs": "https://github.com/Dewiride-Open-Source/Dewiride-PPTX-Studio/issues",
15
+ "keywords": [
16
+ "ooxml",
17
+ "pptx",
18
+ "powerpoint",
19
+ "renderer",
20
+ "dom",
21
+ "svg"
22
+ ],
23
+ "engines": {
24
+ "node": ">=24.11.0"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "README.md",
32
+ "LICENSE",
33
+ "NOTICE"
34
+ ],
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "default": "./dist/index.js"
39
+ },
40
+ "./package.json": "./package.json"
41
+ },
42
+ "scripts": {
43
+ "build": "tsdown",
44
+ "pkg:qa": "publint --strict && attw --pack . --profile esm-only"
45
+ },
46
+ "dependencies": {
47
+ "@pptx-studio/geometry": "workspace:^",
48
+ "@pptx-studio/model": "workspace:^",
49
+ "@pptx-studio/paint": "workspace:^",
50
+ "@pptx-studio/render-svg": "workspace:^"
51
+ },
52
+ "devDependencies": {
53
+ "@arethetypeswrong/cli": "catalog:",
54
+ "publint": "catalog:",
55
+ "tsdown": "catalog:",
56
+ "typescript": "catalog:"
57
+ }
58
+ }