Repository navigation
Releases: shbernal/pptx-html
Release list
v0.2.1
Two rendering bugs, both of which made a correctly imported deck look wrong, and
the ts-pptx 4 upgrade. Nothing about the model or the editable surface changes,
so a document rendered by 0.2.0 still parses.
Fixed
-
A group no longer offsets its children twice. Every grouped shape was drawn
at twice its distance from the origin, because the renderer put the group's own
translateon the wrapper and then rendered the children against the slide.
The further a group sat from the origin the further its contents left it: a row
of KPI cards across a slide ends up with some cards off the slide and the rest
on top of the body text. The model has always said a child's placement is
slide-absolute and that a renderer "never composes a transform"; this makes the
renderer obey it. A table keeps its own transform, because unlike a group it is
what its cells are positioned against. -
Percentage line spacing is written as the multiple it is. A paragraph asking
for 105% got lines 16.8px apart whatever size its text was, so a large title
painted its lines on top of each other. PowerPoint'sspcPctis a multiple of
each line's own font size; a CSS percentage resolves once, against the
paragraph, before the runs inherit it, and the size lives on the runs. It is
unitless now, which is what the inherited-spacing branch beside it already did
and for the same stated reason. An exactspcPtsspacing still emitspx.
Changed
- Requires
@shbernal/ts-pptx4. The read model consolidated every colour
onto oneColorRef, renamedPart.bytestoPart.originalBytes, and made
importSliderefuse a source page whose hyperlink leaves the set being
imported. Nothing here renders differently for it. Two recorded forms move
because upstream now reads more than it did: the coverage baseline gains
text.color.inheritedon the two corpus decks that build their text as
paragraph arrays, and the canonical model gainscompany.
v0.2.0
Fixed
-
A table cell's two diagonals are modeled and drawn.
a:lnTlToBrand
a:lnBlToTr— PowerPoint's Diagonal Down/Up Border, and how a cell is struck
out — were dropped on import with noFidelityNote, which is neither modeled,
carried nor warned. They were also not a loss that had to happen: the read model
decodes both (CellBorders.tlToBr/.blToTr) and the write API takes them back
(TableCellProps.diagonal), so both sides of@shbernal/ts-pptxcould express a
diagonal and only this pipeline could not.TableCell.bordersnow carries all
six lines and the renderer draws them, the two diagonals last, across the whole
spanned region of a merged cell. -
The document's language reaches the emitted deck. The heuristic lane parses
<html lang>and assigned it topptx.langand totheme.lang. The writer has
neither: both landed as own properties on the instance and nothing wrote them
into the package, so every deck came outen-USwhatever the page said.lang
is a run option, so it now travels on eachaddTextcall and on each table
cell — a table-level one does not reach the cells, which is checked against the
emitted XML rather than read off the type. -
A gradient stop and a table cell's edge keep their colour's transform list.
Both arrived pre-flattened — a scheme token and a painted hex with nothing
between them — sotransforms: []meant "the reader had nothing to say", which
is the same shape as "the deck stated none". AlumMod-darkened stop was
indistinguishable from one that was not, and re-authoring either against a
different theme silently stopped tracking it. The cell edge lost more than the
list: its colour carried noalpha, so a transparent rule was imported opaque
and drawn as one. Both now decode through the samecolorOfa solid fill uses,
onResolvedColors that arrived in@shbernal/ts-pptx3.6.0
(ts-pptx#26). -
An inherited italic is painted.
italicwas the one run property with no
resolved counterpart, so a run taking its slant from a master'sa:defRPr i="1"
rendered upright. Paint-only —propsis what emit reads, so the deck always
round-tripped, which is exactly why nothing caught it.Run.resolvedItalic
(ts-pptx#27) closes it, and
ResolvedRunPropertiesnow carriesitalicbesidebold. -
A cell spanning rows is drawn as tall as the rows it covers.
renderTable
measured a spanning cell's two dimensions two different ways: the column arm
summed the widths of the columns the cell covers, and the row arm multiplied
this row's height by the span count. Those agree only when every row is the
same height, and a header row above body rows — the shape most tables have — is
where they do not: a two-row cell was drawn as twice the header rather than as
the header plus the body. Both arms sum now. Paint only, so nothing here reached
the island and no oracle number moved.
Changed
-
IR_VERSIONis 8.TableCell.bordersgained the two diagonals as required
fields, so a document rendered by an older build carries no key to read them
from. The parser refuses the mismatch and says to re-render, which is the true
answer; nothing migrates. -
@shbernal/ts-pptx^3.6.0is the floor, up from^3.2.0. Two of the
fixes above are upstream's (ts-pptx#26,
ts-pptx#27), so the release that
carries them is the one this reads them from. -
A delivered deck goes out as bytes.
deliverDeck'sbloband default
downloadmodes wrote the package to base64 and decoded it straight back, so a
routinely multi-megabyte archive was encoded over 32 KB chunks and decoded again
a character at a time, in the browser, to arrive where the writer started. Both
take the writer'stoBytes()now, and onlyoutput: 'base64'pays for an
encode. That method is optional on the type —opts.pptxFactorymay hand back a
mock, or a writer predating it — so the base64 path stays as the fallback.
Added
-
Five more preset geometries, and connector line ends.
round2DiagRect,
round2SameRect,rightArrow,rightArrowCalloutandwedgeRectCalloutare
resolved from their formulas instead of falling back to the obvious box, and a
modeleda:headEnd/a:tailEndis drawn as a stroke-relative SVG marker,
which is how OOXML defines an end's@wand@lenin the first place. Both
additions are on the fidelity gate. -
cell-diagonaljoins the corpus as a primitive-tier deck: three cells, one
with a single diagonal, one with both, one with none. The distinction is only
testable from a generated deck if the deck states it three ways — a lane that
carried the first cell's rule onto every cell would agree with a corpus that
used one. -
color-transformjoins the corpus as a primitive-tier deck, holding both
halves of the flattening above: a themed gradient stop with a transform beside
one without, and a table whose rules state an opacity.transparencyis the one
colour transform the write API can author, which is what makes the distinction
reachable from a generated deck at all.layout-placeholder's master now states
italic for body level 1, so the corpus contains a run whose slant is written down
nowhere but the master. -
cloneIr(ir)— a deep copy of aRenderIr, exported besideemuOfand
inchesOf. It exists because the two spellings the repo was using are not
equivalent in general:JSON.parse(JSON.stringify(...))drops
undefined-valued keys and cannot carry aUint8Array, and they agree on this
model only because of its JSON contract. A consumer holding an IR needs the
same guarantee, and the one that keeps working if that contract ever moves is
structuredClone. -
EMU_PER_POINTjoinsEMU_PER_INCHon the public surface. Both are the
model's stated units, and a consumer reading awidthPtbeside aBoxneeds
the second one to make sense of the pair. It was already in the package, one
directory down in the renderer. -
eachNode(nodes, visit)andeachTextBody(nodes, visit)— the two
traversals of a node tree, exported beside the model they walk.eachTextBody
is the answer to "where does text live in a node tree", stated once and
exhaustive overRenderNode['kind'], so a new kind is a compile error rather
than an omission wherever the question is asked.eachNodevisits a group
before its children and lets a consumer prune a subtree by returningfalse,
which is what "this node has no box, so nothing inside it was drawn" needs.
v0.1.2
Fixed
-
An SVG picture is drawn as its SVG, not as the raster fallback beside it.
A vector picture is two parts in OOXML: the art hangs off theasvg:svgBlip
extension, anda:blip/@r:embedholds a fallback for readers that cannot draw
vectors. The importer took the fallback — and@shbernal/ts-pptxwrites that
fallback as a 1×1 transparent PNG, so every icon in a deck rendered as one
invisible pixel. Nothing warned, because a picture that resolves to a real part
looks resolved: the SVG parts arrived in the asset manifest and no node
referenced them. Found by the first outside consumer, on a deck where 28 of 49
slides lost every glyph.The same change picks up the SVG-only form, where
a:blip/@r:embedis
legitimately absent (PowerPoint's Insert → Icons, and a plain SVG insert).
Those pictures previously took thedroppedarm and rendered as nothing at
all.The raster fallback is still carried in the manifest, so a consumer that cannot
draw SVG still has something to fall back to.
Added
picture-svgjoins the corpus as a primitive-tier deck. The gap above was
not a subtle mapping error; it was a construct the corpus did not contain, and
a ledger cannot report on a construct nobody generates. The fidelity ledger now
carries upstream'simage.svgnote, which states that the vector part is
carried and the source's own fallback is regenerated rather than kept.
v0.1.1 — published from CI, with provenance
The library is unchanged from v0.1.0. What changed is how it reaches you.
Changed
- Releases are published from CI, through npm's trusted publishing. The
workflow mints a short-lived OIDC token that npm exchanges for a credential
scoped to this package and that one workflow file — no long-lived token exists
to be stolen — and the package is stamped with provenance, so the npm page
links the tarball to the commit and the run that built it. 0.1.0 was published
from a laptop and carries none; this is the first version you can verify that
way. - Publishing is triggered by publishing the GitHub release rather than by
pushing the tag, keeping the deliberate half of a release a human act.
v0.1.0 — first public release
First public release.
pnpm add pptx-htmlRun the loop in your browser →
Added
- The loop.
importDeck,renderDeck,parseDeckandemitDeck—.pptx
to a paint model, to HTML, back to the model, back to.pptx. Anything the
model does not express is carried across as its original XML rather than
approximated, and anything that can be neither modeled nor carried is warned
about. - Invariant R and the oracle that gates it. A generated corpus of 26 decks
runs the full loop on every CI build, compared under a normalized read model
rather than byte-for-byte. The per-construct fidelity ledger is snapshotted so
coverage cannot move without a visible diff. - The JSON island.
renderDeckembeds the model beside the visible SVG and
parseDeckreads that, nevergetComputedStyle— which is what lets the
visible channel approximate freely without any of it reaching the emitted deck.
A document whose integrity hashes do not match is refused, not downgraded. - A declared editable surface.
projectnames what a rendered document may
change andfreezeis its complement; both are exported, so the question "what
may I safely edit in this HTML?" is answerable without reading the renderer.
It is run text,bold/italic/underline/strike/sizePt/
color, a paragraph'salign/bullet/marginLeftPt/indentPt, and
deleting a node. A property is in only if a write-API option can author every
one of its values, including the absent one — everything else is detected as
drift rather than interpreted. - Lane reporting.
parseDeckstates per slide how it read the document
(exact,reconciled,drifted,heuristic) instead of leaving a caller to
infer success from the fact that a file appeared. - A preview drawn from the model, never rasterized. The renderer resolves
twelve preset geometries exactly from ECMA-376's Annex D definitions and marks
the rest as an obvious box rather than guessing an outline, and it draws the
layout's and master's non-placeholder shapes aschrome— visible, and
editable nowhere, because an edit to it would belong to every slide bound to
that layout. - The heuristic lane.
convertDeck/convertSlideinfer a model from a
rendered DOM, for HTML this library did not produce. Browser-only,
explicitly best-effort, and sharing no code path with the loop. - A public site, at https://shbernal.github.io/pptx-html/, deployed from the
same workflow that gates the library — the publish hangs offneeds: validate,
so a site whose round-trip oracle is failing cannot go out.- A playground that runs
importDeck → renderDeck → parseDeck → emitDeckin
the visitor's browser. The sample decks are the oracle's own corpus rather
than decks written for the page, editing goes throughEDITABLE_SURFACEand
nothing else, the lane is reported per slide, and the round-tripped.pptx
is downloadable beside the original. Nothing is uploaded anywhere. - The fidelity ledger, generated from the oracle's coverage reporter over
the corpus, so the page cannot claim more than CI knows. Every row links to
that deck running in the playground. docs/published as written.site/docs/is a generated mirror; the
tracked design record stays the source of truth, and the generator fails
rather than warns when the two drift.
- A playground that runs
- A runnable example.
examples/round-trip.mjsruns all four legs, edits a
run, and re-imports the emitted deck to show the edit arrived. CI runs it,
because it is the only thing here that imports the package by name and so the
only check that theexportsmap works the way a consumer will use it.
Notes on scope
- The guarantee covers decks written by
@shbernal/ts-pptx, which is what the
corpus is made of. Decks authored in PowerPoint are a deliberate second tier
and are not yet gated. - There is no rasterizer, on purpose. A flattened slide is the one output that
can never re-enter the loop — see
docs/decisions.md. - ESM only, Node
>=24.@shbernal/ts-pptx^3.2.0is the one runtime
dependency.