render-dom
The same slide as live SVG elements you can hit-test, zoom and overlay, plus an HTML text layer.
The same slide as live SVG elements you can hit-test, zoom and overlay, plus an HTML text layer for selection and screen readers.
npm install @pptx-studio/render-dom @pptx-studio/opc @pptx-studio/model @pptx-studio/render-svg
What it is
mountSlide builds the tree render-svg describes as real <svg> elements in a host you give it, so a shape is an element you can find by its id and hit-test with a click. mountOverlay draws the debug chrome - paths, text rectangle, connection sites, draggable adjust handles - over one shape, and mountTextLayer puts real HTML text over the same line boxes for selection, an IME and a screen reader.
30-second example
const store = PartStore.open(bytes);
const doc = loadDocument(store);
const slide = doc.slides[0];
const host = document.querySelector('#stage');
if (slide === undefined || host === null) throw new Error('nothing to mount');
const text = createTextEngine({ defaultTextStyle: doc.defaultTextStyle }); // one measurer, every mount
const mounted = mountSlide(host, slide, doc.slideSize, {
width: 960, // one CSS pixel per point
devicePixelRatio: window.devicePixelRatio, // strokes round to device pixels
media: mediaFromStore(store),
text,
});
const first = mounted.placed[0];
if (first !== undefined) mountOverlay(mounted.root, first, { unit: doc.slideSize.cx / 960 });
// Zoom: unmount and mount again at the new width. The line breaks do not move.
mounted.unmount();Key concepts
One tree, two renderers
renderSlideMarkup from this package is the string renderer applied to the same tree mountSlide instantiates; the tests check that the two agree.
A zoom is a fresh mount
Strokes are rounded to the device pixels of the mounted width, so the SVG for 25% is not the SVG for 400%. There is no resize(): unmount and mount at the new width. The text layout does not change.
The overlay writes a:avLst
Dragging a handle on the overlay produces a HandleEdit with the adjust list a save would write; mergeAdjust folds it into the shape's own.
The text layer
One div per shape, one per line, a span per piece, over the identical line boxes the SVG used. Real text nodes, so the browser's selection and accessibility tree see words, not paths.
API reference
20 exports of @pptx-studio/render-dom 0.3.7, read from the package this site installed. Click a name for its declaration.
src/errors.ts
RenderDomErrorCodetypeEvery failure in this package is aRenderDomErrorwith a code.type RenderDomErrorCode = /** `mountSlide` was handed something that is not an element. */ 'RENDER_DOM_NO_HOST' | /** The host is detached and no `document` was passed. */ 'RENDER_DOM_NO_DOCUMENT';
RENDER_DOM_ERROR_CODESconstconst RENDER_DOM_ERROR_CODES: readonly RenderDomErrorCode[];
RenderDomErrorclassclass RenderDomError extends Error { readonly code: RenderDomErrorCode; constructor(code: RenderDomErrorCode, message: string); }isRenderDomErrorfunctionfunction isRenderDomError(value: unknown): value is RenderDomError;
src/mount.ts
createNodefunctionOne node into one element. Exported because the debug overlay wants it too.function createNode(document: Document, node: SvgNode): Node;
MountOptionstypeinterface MountOptions extends RenderOptions { /** * The `document` to build in. * * Passed rather than reached for, because this package must work in a Web * Worker with an `OffscreenCanvas` and no global `document` - and because a * test that has to stub a global is a test that can pass for the wrong reason. */ readonly document?: Document; }MountedSlidetypeinterface MountedSlide { readonly root: SVGSVGElement; readonly placed: readonly Placed[]; /** The element drawing a given `p:cNvPr/@id`, for hit-testing and selection. */ element(cNvPrId: number): SVGElement | null; /** Detach from the host. The nodes stay valid, so a caller may re-attach. */ unmount(): void; }mountSlidefunctionRender a sheet into a host element.function mountSlide(host: Element, sheet: Sheet, size: SlideSize, options?: MountOptions): MountedSlide;
renderSlideMarkupfunctionThe same slide as a string, from this package.function renderSlideMarkup(sheet: Sheet, size: SlideSize, options?: RenderOptions): string;
src/overlay.ts
HandleEdittypeWhat a completed drag produced.interface HandleEdit { /** Which `a:ahLst` entry was dragged. */ readonly handle: number; /** The adjust values this handle controls, rounded as the file will hold them. */ readonly adjust: Readonly<Record<string, number>>; /** The shape's whole `a:avLst` after the edit, ready to write back. */ readonly avLst: readonly PresetGuide[]; }OverlayMountOptionstypeinterface OverlayMountOptions extends OverlayOptions { readonly document?: Document; /** Called on every pointer move during a drag, and once more on release. */ onChange?: (edit: HandleEdit) => void; /** Called once on release. */ onCommit?: (edit: HandleEdit) => void; }MountedOverlaytypeinterface MountedOverlay { readonly root: SVGGElement; readonly overlay: ShapeOverlay; /** Redraw against a new placement - after a drag, or a resize. */ update(placed: Placed): void; /** True while a handle is being dragged. */ dragging(): boolean; unmount(): void; }slidePointfunctionA point in the host<svg>'s user space - EMU - from a pointer event.function slidePoint(root: SVGSVGElement, clientX: number, clientY: number): Point | null;
mergeAdjustfunctionThea:avLsta drag leaves behind.function mergeAdjust(avLst: readonly PresetGuide[], adjust: Readonly<Record<string, number>>): readonly PresetGuide[];
mountOverlayfunctionDraw the overlay for one shape into a host<svg>, and make its handles drag.function mountOverlay(root: SVGSVGElement, placed: Placed, options?: OverlayMountOptions): MountedOverlay;
src/text/layer.ts
LayerSizetypeA slide's size in points, which is what a block's coordinates are in.interface LayerSize { readonly widthPt: number; readonly heightPt: number; }LayerBlocktypeOne shape's text, with the frame it is drawn in.interface LayerBlock { readonly block: TextBlock; /** The shape's frame, in points on the slide. */ readonly leftPt: number; readonly topPt: number; readonly widthPt: number; readonly heightPt: number; /** `p:cNvPr/@id`, so a caller can find the element for a shape. */ readonly cNvPrId: number; readonly name?: string | undefined; }TextLayerOptionstypeinterface TextLayerOptions { readonly document?: Document | undefined; /** CSS pixels per point. One transform, never a re-layout. */ readonly scale?: number | undefined; /** Let the layer take pointer events. Off by default: the SVG hit-tests. */ readonly interactive?: boolean | undefined; }MountedTextLayertypeinterface MountedTextLayer { readonly root: HTMLElement; /** The element holding one shape's text. */ element(cNvPrId: number): HTMLElement | null; /** Rescale without re-laying anything out. */ resize(scale: number): void; unmount(): void; }mountTextLayerfunctionMount a text layer into a host, over a slide ofsizepoints.function mountTextLayer(host: Element, blocks: readonly LayerBlock[], size: LayerSize, options?: TextLayerOptions): MountedTextLayer;
Errors
Throws RenderDomError, whose code is one of:
- RENDER_DOM_NO_HOST
- RENDER_DOM_NO_DOCUMENT
Try it live
Reading the deck…