npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pptx-studio/model

v0.5.0

Published

The document model: sheets, the inheritance chain, and the resolver everything else reads

Readme

@pptx-studio/model

The document model: the sheets, the chain between them, and the resolver everything else reads.

A .pptx renders correctly or it does not, and almost all of the difference is here rather than in any renderer. A slide placeholder that PowerPoint wrote has no geometry, no fill, no line and no style of its own; every one of those comes down a chain of two hops whose rules are asymmetric, undocumented, and different from what every implementation this project has read assumes.

Sub-phase 2.9. Everything below was measured against Microsoft PowerPoint rather than argued from the standard; the fixture is corpus/ground-truth/sheets.json and the reasoning is docs/adr/phase-2-geometry-and-paint/0024-model-parse-and-resolve.md.

undefined means the file did not say

Every visual property is T | undefined, and absence is a fact the file states rather than a gap to be filled in.

PowerPoint proves this about its own output. Create a slide from a layout and its title placeholder has no a:xfrm at all. Type into it and it still has none. Nudge it one point to the right and the whole resolved rectangle appears at once — inherited two hops, from the master, baked in:

<!-- untouched, and after typing -->
<p:sp><p:nvSpPr>…<p:ph type="title"/>…</p:nvSpPr><p:spPr/>…</p:sp>

<!-- after moving it one point -->
<p:sp>…<p:spPr><a:xfrm><a:off x="850900" y="365125"/><a:ext cx="10515600" cy="1325563"/></a:xfrm>…

So a parser that defaults geometry at parse time destroys the only record of which shapes are still bound to their layout. That record is what Change Layout reads, what the inspector's provenance chips read, and what tells a theme swap which shapes to invalidate.

The two hops do not use the same key, and neither uses the pair

This is the finding of the sub-phase, and it contradicts the plan that led to it.

| hop | matches on | when nothing matches | | --------------- | -------------------------------------------- | -------------------- | | slide → layout | @idx alone. @type is never consulted | orphan | | layout → master | the folded @type, taking the first | orphan |

Measured over 46 probes, each a slide placeholder with no geometry of its own whose rendered position names the layout placeholder it matched. Scored against the 34 that had a candidate set:

| candidate rule | right | | --------------------------------- | ----- | | @idx alone | 34/34 | | family (title/content) and @idx | 24/34 | | (type, idx) | 13/34 | | @type alone | 6/34 |

A slide title at idx 0 takes a layout body at idx 0. A slide body at idx 2 takes a layout sldNum at idx 2. A slide title at idx 9, against a layout that holds a title at idx 0, matches nothing and renders at the origin with zero size. No type is privileged — not the title family, not the header-and-footer trio, which is what the shuffled-type deck was built to find.

The asymmetry is not arbitrary:

  • Every stock PowerPoint layout numbers its date, footer and slide-number placeholders 10, 11 and 12, and every stock master numbers the same three 2, 3 and 4. A second hop that looked at @idx would inherit nothing in the templates Office ships.
  • A slide master's placeholder vocabulary is only title, body, dt, ftr, sldNum and hdr. A master carrying ctrTitle, subTitle, obj or pic is repaired on open. So a second hop that looked at the type as written would find nothing for the ctrTitle in layout 1 or the <p:ph idx="1"/> in "Title and Content".

ctrTitle folds to title and every content type folds to body on the way up. And each hop asks with the placeholder of the sheet it is leaving: a slide obj at idx 0 reaching a layout ctrTitle lands on the master's title, not its body.

<p:ph/> with no attributes is type="obj" idx="0" — read back through PowerPoint's own object model as ppPlaceholderObject. ECMA's default for the attribute is body. The two agree at the first hop because it ignores the type, and at the second because obj folds to body; they agree by construction rather than by luck, which is worth knowing.

Every binding comes from the part's own .rels

A slide part does not name its layout, a layout does not name its master, and a master does not name its theme. Each binding lives in exactly one place, and every other candidate is a trap:

  • p:sldLayoutIdLst is not the binding. A two-master deck saved by PowerPoint puts all 22 layouts in one flat ppt/slideLayouts/ folder numbered 1 to 22, and which master owns which is recorded only in the two masters' relationship parts.
  • ppt/presentation.xml.rels has a theme relationship, it always points at theme1.xml, and reading it gives every slide the first master's palette. On a one-master deck — which is every deck most people ever test with — that is indistinguishable from correct. Measured: two masters, two themes, and accent1 resolves to a different colour on each.
  • A .rels file is not in rId order. PowerPoint routinely writes rId8 first.

A missing binding is not an error. PowerPoint repairs a slide with no layout, a layout with no master and a master with no theme rather than refusing them, so loadDocument records a problem and leaves parent or theme as null. Refusing to write one is @pptx-studio/validate's job.

The style matrix, phClr, and the background share one rule

a:fillRef and p:bgRef index the same two lists with the same offset, and they were measured independently against the same six theme entries. All six agreed.

| @idx | means | | ------------ | ------------------------------ | | 0 | nothing at all | | 1…999 | a:fillStyleLst[idx − 1] | | 1000 | nothing at all | | 1001… | a:bgFillStyleLst[idx − 1001] | | out of range | clamps to the last entry |

Out of range clamps: a fillRef idx="4" against a three-entry list paints the third, and a bgRef idx="9999" paints the last background entry. PowerPoint opens both without repairing, so a renderer that throws refuses a deck PowerPoint shows.

Each entry is a function of a colour. <a:solidFill><a:schemeClr val="phClr"><a:lumMod val="60000"/></a:schemeClr></a:solidFill> is the body and the reference is the call, with the reference's own colour child as the argument — and that argument may carry transforms of its own. PowerPoint's shape-style gallery writes <a:schemeClr val="accent1"><a:shade val="15000"/></a:schemeClr> on seven of its forty-two entries. Miss phClr and every themed shape renders black.

a:fontRef/@idx is not a number. It is major, minor or none, and PowerPoint repairs a numeric one.

The background takes the nearest sheet that declares a p:bg — slide, else layout, else master. An absent p:bg is the only thing that inherits: a slide declaring <p:bgPr><a:noFill/> paints nothing and does not fall through, which is why Background is a value rather than a nullable fill.

masterClrMapping means parent, not master

a:masterClrMapping says "the map already in force". For a slide, that is the layout's map. A layout carrying an a:overrideClrMapping changes the colours of every slide bound to it, and a slide's own override beats its layout's — measured with two overrides that disagreed, because two identical ones cannot tell you which applied. dk1/lt1/dk2/lt2 bypass the map either way, as 2.6 established.

And the map that resolves a p:bgRef's colour is the one in force on the sheet being asked about, not on the sheet the bgRef was written on. A slide whose clrMapOvr remaps bg1 gets a different background out of the same master.

The resolver reports where it looked

resolve(shape, sheet, (s) => s.xfrm);
// → { value, origin: 'masterPh', explicit: false, sheet, shape }

Carrying the origin costs one field and buys four features from one function — provenance chips, per-property Reset, layout-compatibility scoring, and correct theme invalidation — all reading the same function the renderer reads, so none of them can drift from what is on the screen. explicit is not the same as origin === 'shape': a layout placeholder that declares a fill is explicit about that fill, and the slide inheriting it is not.

Geometry is not special. Fills, lines, effects and p:style all travel the same chain, and all four were measured arriving from the layout placeholder and from the master placeholder through a layout that declared nothing.

A table is its grid, and the spans are the whole of a merge

A p:graphicFrame whose a:graphicData is a table carries shape.table: the a:tblPr flags and style source, every a:gridCol, every a:tr with its cells, and each cell's spans (absent reads as 1), flags (absent reads as false), body and a:tcPr. tableGrid(table) is what PowerPoint draws from it, and the rule was measured in C7 (corpus/ground-truth/tables.json) on 82 tables, 52 of them built to disagree with themselves:

  • Each a:tc takes the next column. A short row is padded with empty positions; a fifth cell in a four-column grid is dropped.
  • gridSpan and rowSpan on the anchor are the whole story, 76 of 76. A position an earlier span claimed is covered whatever its own attributes say; a span stops at the grid edge or at the first claimed position; zero is one and a negative runs to the edge.
  • hMerge and vMerge change nothing. The flags-only reading a person writes first fits 52 of 76, and PowerPoint rewrites the flags from the spans on save.
  • a:tr/@h is the least a row is drawn at, never the most (72/72 against 68/72 for a fixed height); a column is never narrower than its cells' side margins plus 2 pt; and the frame's a:ext says nothing about the drawn size, which is the grid's sums (76/76 against 62/76).

Cell text is a TextBody like any other.

A table draws the built-in its GUID names, or none

BUILTIN_TABLE_STYLES is PowerPoint's own Table Styles gallery: the 74 styles, in gallery order, each with its GUID, its name and the a:tblStyle PowerPoint writes for it, byte for byte. They were enumerated by driving the gallery itself and read back as PowerPoint serialised them (C8, corpus/ground-truth/table-styles.json); nothing was transcribed from another implementation.

tableStyleOf(table) is the style PowerPoint draws a table with, and C8 found one rule for it across 84 packages in three themes:

  • The a:tableStyleId - or an inline a:tableStyle's @styleId - names one of the 74, in any case, and that built-in is drawn (230/230 across every style in two themes).
  • Nothing in ppt/tableStyles.xml changes the drawing: a redefined built-in draws PowerPoint's own (7/7), a custom style defined there draws nothing of itself, and @def is never applied.
  • Any other id, or none, returns null: PowerPoint draws a 1-pt black grid with no fill, whatever the theme (3/3 against the theme's dk1 and tx1).

builtinTableStyle(id) parses one of the 74 afresh; parseTableStyle reads any CT_TableStyle, defaulting nothing the file did not say. A GUID without braces or with padding, an edge with neither a:ln nor a:lnRef, and an empty a:fill throw: PowerPoint repairs each.

The thirteen parts compose in schema order, property by property

Which part of a style reaches a cell, and which wins, was measured in C9 on 10,505 slides of every built-in in three themes (corpus/ground-truth/table-cascade*.json), against what PowerPoint's object model reported for each cell and what it drew at 4 px a point:

  • tablePartsAt(props, rows, cols, row, col) is ECMA-376's reading: the end rows and columns where their flags are on, bands counted past a first row or column, a corner where both of its ends apply. TABLE_PART_ORDER is the schema's order, wholeTbl lowest and nwCell highest, and it is the order they compose in for fills, text and edges alike.
  • Every property is found on its own: the highest part that states a fill, a colour, bold or italic wins it, whatever that part leaves unsaid.
  • tableCellFill: the cell's own a:tcPr fill, else the style's. tableBackground: a:tblPr's fill and effect list, each in place of the style's tblBg one, an empty list included, painted under the cells over the grid and never the frame as written. themedEffects follows an effectRef into the theme.
  • tableEdgeLine: an edge's owner writes the line it draws, replacing the style's whole; a line no owner writes is the highest part's claim on that edge. The cell above or left owns a segment where its anchor is level with it, in the anchor's column or row; else the cell below or right, where its anchor is; else the cell above or left. An unmerged cell is always level. A table that resolves to no style draws DEFAULT_GRID_LINE, 1 pt black. A position no a:tc reached owns its edges as a cell writing nothing does, and a dashed line is drawn dashed.
  • A merged cell takes its anchor's parts and every covered position's end and corner parts; its bands are the anchor's. A covered cell's a:tcPr is never read, though PowerPoint keeps it on save.
  • tableCellDiagonals: the anchor's own a:lnTlToBr and a:lnBlToTr, across its whole span, as drawn: mirrored in an rtl table. No built-in states a diagonal.
  • tableTextLayer is what the style gives a cell's text, and the text cascade reads it between the cell's own list style and the master's p:otherStyle; p:defaultTextStyle is never read, and under a master with no p:txStyles a cell takes the built-in other style whatever its frame.
  • A position or edge the table does not have throws MODEL_TABLE_POSITION.

What is not here

  • Nothing is painted. The renderers are 2.10.
  • Table drawing and editing are 4.4; SmartArt is 4.5.
  • Commands, undo and history are Phase 5; Change Layout is 7.4. What PowerPoint rewrites when a layout changes is recorded in the fixture and not acted on.
  • a:effectDag is not modelled — a directed graph of effect primitives PowerPoint has never been observed to write. An undefined there means the shape re-emits byte for byte.
  • Group child coordinate spaces are parsed (a:chOff/a:chExt) and not applied; that is 2.10.

Fixtures

corpus/ground-truth/sheets.json — 114 probes in 37 packages, sub-phase 2.9. corpus/ground-truth/tables.json — 82 tables, one package each, and 21 slides PowerPoint authored, sub-phase 4.1. corpus/ground-truth/table-styles.json — the 74 built-in table styles and 84 probe packages, sub-phase 4.2. corpus/ground-truth/table-cascade.json and its five table-cascade-*.json siblings — 250 packages of tables in three themes, every cell and edge PowerPoint reported and drew, sub-phase 4.3.

The measurement is a position, and PowerPoint reports it directly. C3 and C4 sampled bitmaps because a fill and a stroke are pictures; an inheritance is not. A placeholder with no a:xfrm of its own still has a position, Shape.Left reports it in points, and that is the resolver's own answer read out of the resolver rather than reconstructed from what it painted. So every candidate parent sits in a rectangle no other candidate shares, and the rectangle a probe lands in names the parent it matched — one number, no fitting, no error bars.

23 of the 37 packages are hostile and each is alone in its own file. 16 were refused or repaired.

model.test.ts re-derives the matcher from the fixture's 34 recorded cases rather than comparing against a summary of them, so a rule that drifts fails on the measurements.

See docs/adr/phase-0-foundation/0007-ground-truth.md, docs/adr/phase-2-geometry-and-paint/0021-colour.md, docs/adr/phase-2-geometry-and-paint/0022-fills.md, docs/adr/phase-2-geometry-and-paint/0023-lines.md, docs/adr/phase-2-geometry-and-paint/0024-model-parse-and-resolve.md docs/adr/phase-4-tables-and-smartart/0056-the-spans-are-the-merge.md, docs/adr/phase-4-tables-and-smartart/0063-a-table-draws-the-built-in-its-guid-names.md and docs/adr/phase-4-tables-and-smartart/0064-the-parts-compose-in-schema-order.md.