Repository navigation
typeset.us 4.3.0
4.3.0 follows 4.2.0. It is a minor release: no public API was renamed or
removed, nothing new is required, and new API is additive. Default rendering
changes only where 4.2.0 had a verified defect; each change is listed under
"Rendering changes" with the number of test paragraphs whose output changed.
Rendering changes
Every count compares the published 4.2.0 build with the 4.3.0 candidate, in
the same pages, in Chromium, WebKit and Firefox:
scripts/v4/verify-golden.mjs: 3,932 cells per engine (85 corpus
paragraphs at 240, 320, 400 and 560 px in Georgia and the bundled Fraunces,
plain, with a link and emphasis, with React's server-rendering comment
separators, through the legacy renderer, with quotes and hanging, as
titles and justified, plus a 33-paragraph adversarial set).
A cell changes when its outcome, finishing features, breaks or characters
differ.- A golden A/B of 316 blocks per engine (audit fixture, 40 corpus
paragraphs and React adapters at 320, 375 and 768 px, default and loader
options): element screenshots at DPR 2,measureLayoutline boxes,
outcomes, feature statuses, copied text and HTML, and markup. - The React adapters: 2,160 settled
TypesetTextandTypesetRichText
blocks (60 corpus paragraphs, four configurations, 320, 390 and 560 px).
Outside the changes below, 0 of these differ in any engine.
- Generated line breaks are word separators again (accessibility, C2). A
generated<br>that stands in for a collapsed space is no longer
aria-hidden, so engine accessibility trees stop joining the words on
either side of it ("galleryguide"), including inside link and heading
names. A break after a hyphen or dash stays hidden, so "public-health" is
still one word. Spacing and hanging markers aredisplay: inlineinstead
ofinline-block, which stops Chromium dropping the word space beside
them. The same applies toTypesetRichText. Assistive technology now meets
a line boundary at each generated break, and WebKit accessible names
contain a newline there.scripts/v4/verify-native-ax.mjsfinds 0
unmatched paragraph words in Chromium and Firefox (WebKit's inspector
exposes no paragraph text) and 0 wrong link or heading names in Chromium,
WebKit and Firefox, also after a narrowing resize and, in Chromium and
Firefox, with a live tree across a translation. In the same lanes 4.2.0
leaves 1,138 words unmatched in Chromium and 761 in Firefox, and 69 of 108
link names and 41 of 42 heading names wrong in Chromium, 53 of 78 and 29
of 30 in WebKit, and 56 of 108 link names in Firefox (on the original
acceptance, corpus and React fixtures alone: 581 and 381 words, 38 of 63
links and 23 of 24 headings). Attributes only: in the golden A/B, markup changed
in 311 (310 in WebKit) blocks, and 1,087 of 1,103 generated breaks
are now exposed (16 hyphen breaks stay hidden), with 0
screenshots, line boxes, outcomes, feature statuses or copied texts
changed. One effect reaches beyond the page: a reader view that extracts
the live DOM (Firefox's Reader View, Readability-based read-later tools)
dropsaria-hiddennodes, so 4.2.0's breaks never reached it, and now
keeps them without their style, showing the page's composed lines in its
own wider column: with Firefox's Readability.js, 35 of 40 corpus
paragraphs composed at 375 px are double-wrapped at a 660 px reader
measure (36 composed at 1024 px; 4.2.0: 0). SUPPORT.md lists it under
known limitations. In verify-golden 2,016 (WebKit 2,011) of 2,724 corpus cells per engine
differ only in these attributes and C9's below, 173 of them also in the
position of a break before a link (below). - Generated breaks are displayed through
--ts-break-display(C9). The
break's inline style isdisplay: var(--ts-break-display, inline) !important(4.2.0:display: inline !important), and the
TypesetRichTextbreak carriesdisplay: var(--ts-break-display, inline).
On screen nothing moves: the same cells as above change markup only. In
print the property isnoneand spacing, hanging and tracking are
neutralized, so printed text wraps natively at the paper's width, where
4.2.0 printed the screen breaks and the page re-wrapped them into
alternating long and short lines. - A line that starts where a link starts breaks before the link. 4.2
put the generated<br>first inside a link or other inline element whose
text began the line, so the element's first fragment was an empty stub at
the end of the line before: Firefox's default focus ring, author outlines
and hover backgrounds painted a sliver there, a padded link's padding sat
at that line's end, and WebKit link names began with a newline (16 to 19
of 79 corpus links at 300 px). The break now goes before the outermost
element that starts the line, in the DOM renderer andTypesetRichText
(there also when the text before it ends inside another element or a
fragment, as in<strong>Note: </strong><a>, where the first 4.3
candidates still broke inside the link); the framework text an element
holds is not split for it. Characters are unchanged, and so are lines
and widths unless the element has horizontal padding, border or margin,
which now starts its line as the compositor planned. 4.2.0's stub put
that padding on the line before, and where the planned widths then
failed verification the paragraph kept its native layout
(native:verification); such a paragraph now composes: in a probe of
inline<code>starting lines at 280 to 800 px (step 8) in three fonts,
2 of 198 cells per engine with horizontal padding and 1 of 198 with
horizontal borders go fromnative:verificationtocomposed:rich, in
Chromium, WebKit and Firefox, and none the other way (0 of 198 without
padding or border). Where the element that starts the line has other font
metrics, such as inline<code>, 4.2.0's empty fragment also made the
paragraph taller than the browser's own layout, by 1 px in Chromium and
about 0.5 px in WebKit and Firefox; its height now matches native (a
word of each corpus paragraph in<code>at 288 to 400 px: 134 of 1,476
cells change height in Chromium, all by 1 px, 283 in WebKit, by 0.52 or
0.05 px, and 287 in Firefox, by 0.5 px; 0 with<em>, whose metrics
match). Golden diff: 173 of 680 rich cells per engine in
verify-golden, 3 (WebKit 5) of 316 blocks in the golden A/B and 71 of
2,160 React blocks, all markup only (the break's position), with 0
screenshots, line boxes, outcomes or copied texts changed; 0 of the rest
(these sets have no inline code or padded element at a line start). - The automatic loader composes
.demoand[data-no-smooth]content.
The website loader (go@4.2.0.js, and 4.3'stypeset.us/auto) always
skipped elements with the classdemoor the attribute
data-no-smooth, and everything inside them, even under an explicit
data-typeset-selector; these were typeset.us-internal exclusions that
no document mentioned, and a page whose prose sat in a.democontainer
got nothing composed and a console note that nothing matched. From
go@4.3.0.js they compose like any other prose;data-no-typesetremains
the way to exclude content. Loader fixture: 2 of 2 such paragraphs now
compose (0 under go@4.2.0.js); no typeset.us page that loads go.js uses
either. The npmtypeset.us/gonever had these exclusions. - Lines holding text right after an HTML comment are not tracked (C6).
Frameworks find some Text nodes by their place after a comment (Lit's part
markers, Solid's), so text right after any comment stays where it is,
whole and unwrapped, and letter spacing, which needs a wrapper, is not
applied to the line that holds it; the paragraph's other lines are
tracked, and when none can be the status isnative:tracking-comment.
4.2.0 moved such text into its tracking wrappers. The first 4.3 candidate
rolled tracking back for the whole paragraph instead, or tracked a line
part way. This includes React's server-rendering separator (<!-- -->
between interpolated strings in static HTML; hydrated React text is
tracked as React's own) and WordPress's<!--more-->. Golden diff: in
the new "comments" cells (43 corpus paragraphs with a separator at every
fourth word), 314 (WebKit 312) of 344 differ from 4.2.0: 4.2.0 tracked
287 (WebKit, Firefox 285), 4.3.0 tracks the comment-free lines of 106
(WebKit 104) and reportsnative:tracking-commentfor 181, and breaks
and spacing markers go before a comment rather than between it and its
text. Line breaks and characters are unchanged; 0 of the other cells
change. - Justified text is left as the author set it (
native:justify, C3). A
generated break ends its line, so every composed line took the last-line
alignment:text-align: justifybecame ragged right, and a
text-align-lastthat differs fromtext-alignapplied to every line,
whileaudit()still passed. Multi-line paragraphs whose computed
text-alignisjustifyorjustify-all, or whosetext-align-last
differs fromtext-align, are now declined under every entry point
(typeset(),mount(), the loaders,TypesetText,TypesetRichTextand
the legacy renderer). A single line still reportsnative:fits. Golden
diff: 336 of 336 justified cells per engine (42 corpus paragraphs), 0 of
the rest. - Abbreviations, units, honorifics, labels and letter designators stay
with their words (English, C13). Every word ending in a period counted
as a sentence end, so the compositor paid to break after "Dr.", "Fig.",
"a.m." and "U.S.", and the single-letter penalty pushed units and
designators to the next line ("1,200 / m", "hepatitis / C", "World War /
I"). Now an abbreviation (Mr, Mrs, Ms, Mx, Dr, Prof, Rev, St, Mt, Jr, Sr,
vs, etc, e.g, i.e, a.m, p.m, p, pp, Fig, No, Vol, Ch, Inc, Ltd, Co, dotted
initialisms and single initials) ends no sentence unless a capitalized
common sentence opener follows it ("…in the U.S. The results", "at 7 p.m.
Most", "Plan B. It"); "etc." ends one before any capitalized word ("etc.
Staff"), "a.m." and "p.m." before any but a day, a month or a time zone
("9 a.m. Parking", but "9 a.m. Monday"), and "No." before any but a roman
numeral. Splitting a number from its unit, an honorific from a name, a
label from its number, a word from its letter designator, or a capital
letter after a function word from the lowercase word it modifies ("and B
students", "the X chromosome") costs what a weak line end costs; and only
the article and the pronoun "I" pay the single-letter penalty. "I" is a letter designator only after a
head that takes a roman numeral ("World War I", "Phase I", "Title I") or a
name after a regnal title ("King Henry I"), never after any other
capitalized word ("In March I", "At Kaiser I"). Golden diff: 0 of 2,724
corpus cells per engine (the corpus has none of these constructions); 63
(Chromium, Firefox) or 64 (WebKit) of 264 cells of the new adversarial set
(tests/v4-corpus-adversarial.json, 33 paragraphs), and 55 of 264 through
the legacy renderer. On that set, line-end review items (the C16 audit)
fall from 152 under 4.2.0 to 110 (WebKit 151 to 110), against 485 in the
browser's own layout; pairs split at line ends fall from 60 to 21 (WebKit
58 to 19). Openers stranded after "St." or an initial ("Main St. Doors",
"vitamin K. Parents") are not charged, since those abbreviations often
continue a sentence: 8 such line ends in the adversarial set where 4.2.0,
which read every period as a sentence end, had none. The same holds after
a dotted initialism, a company suffix or "Jr." that ends a sentence before
a capitalized noun ("across the U.S. Health officials", "Novagen Inc.
Shares", "King Jr. Organizers"), which reads as mid-sentence, and there
4.3 strands the opener more often than 4.2.0 and than the browser: in a
sweep of seven such sentences at 131 widths (280 to 800 px) in Georgia,
Fraunces and Arial, the next sentence's first word ends a line in 483 of
2,751 cells in Chromium (WebKit 478, Firefox 483), against 302 (300 in
Firefox) in the browser's own layout and 51 (WebKit 50) under 4.2.0, 332
(WebKit 339) of them where neither does. The C16 audit does not report
these line ends. The four adversarial paragraphs added for them have 12
(WebKit 11) such line ends (native 5, 4.2.0 2), and at 320 px in Fraunces
"…across the U.S. Health" also adds a weak line end (a recorded
trade-off). Telling the two readings apart needs a continuation list
("U.S. Army" against "U.S. Health"), left for 4.4. The binding is a
trade, not a free win: across 105 widths (240 to 760 px) of the first 20
of those paragraphs, split pairs fall by about 86% (797 to 113 in
Chromium) while weak line ends rise by about 7% (554 to 591), because a
split pair costs what a weak line end costs. Two recorded trade-offs at
the golden widths: that "U.S. Health" paragraph, and at 320 px in
Fraunces the recipe paragraph, which ends a line on "the" where neither
the browser nor 4.2.0 does (the tight-line ranking, to be retuned in
4.4). Dropping 4.2.0's false sentence
ends also moves breaks where no pair is involved, since their penalties had
steered some layouts: at the phone widths of 288 and 343 px, the review
found 16 (Chromium), 21 (WebKit) and 18 (Firefox) changed cells without a
split pair in 4.2.0 that now end one line on a weak or linking word 4.2.0
avoided ("…audit by Ms. Lindqvist was | complete"); by the 4.3 audit, 450
changed cells got better and 26 worse of 625 in Chromium. That retune
goes to 4.4 with the tight-line trade. No orphan, overflow or text
change. - Live regions are no longer composed (accessibility, C4). Text whose
nearest region hasaria-live="polite"or"assertive", or (without
aria-live="off")role="status",alert,log,marqueeortimer,
or is an<output>, keeps native wrapping and reports
native:live-region. 4.2 composed it and rewrote it on every resize, font
load and idle pass (51 mutation records on mount and 180 more across two
resizes for one status paragraph), and Chrome announced those rewrites, so
screen readers repeated status messages. A paragraph that contains a live
region (an inline result count or "saved" status) is declined the same
way; an emptyaria-livecounts as absent and any other value butoff
as live, as Chromium treats them; composed text moved into a live region
(a toast) is released in the same mutation callback as the move; and
TypesetTextin a region keeps the quotes it curled while rendering and
writes a new value once, curled. Regions are found through open shadow
roots as assistive technology sees them: text slotted into a component's
live wrapper (a toast or alert withrole="status"around a<slot>), a
paragraph containing a component whose slot is live, and text in a shadow
root under a live region, as they are when the text is composed. A shadow
tree that turns live later sends no mutation (a component defined after
the text composed, a role set inside its shadow root): the text is
released at the next resize or font load, with one update inside the
region (98 mutation records for a paragraph in three engines). Roles set
throughElementInternalsor inside a closed shadow root cannot be read
from outside; SUPPORT.md givesdata-no-typesetas the workaround for
these and for lazily defined components. Golden diff: 9 of 13 paragraphs
on the live-region fixture, per loader and engine (a paragraph that
contains an<output>now reportsnative:live-region, not
native:rich-element); 0 in the golden sets, which have no live regions. - Smart quote corrections (C15). A single quote right after a curled
opening double quote now opens too:"'Quoted' inside,"gives
“‘Quoted’ inside,” (4.2 gave “’Quoted’). Rock ’n’ roll, ’bout, ’round and
’nuff are elisions (4.2 gave ‘n’ and ‘bout): 'n' only in a pair such as
fish ’n’ chips, so a quoted key letter still opens with a left quote
(“Type ‘n’ to cancel”), and 'round, 'bout and 'nuff only when no closing
single quote follows in the sentence (“the ‘round robin’ plan”). Glyph
substitutions only and length-preserving; ’90s, ’Tis, ’em, primes such as
5'10" and possessives are unchanged, and single quotes never become
double. Golden diff: 0 of
173 corpus texts (14 with straight quotes).TypesetTextalso curls quotes
during render, so its server HTML has them (a Next production build: 8
curled, 0 straight, no hydration messages in three engines; 4.2 had 2 of
10 curled before hydration). - Unrendered text is not measured (C8).
typeset()on an element in a
display: nonesubtree keeps an existing composition (4.2.0 restored
native text and recordedunmeasurable), and text in a skipped
content-visibility: autosection recordsunmeasurableinstead of being
composed from forced layout (4.2.0 composed it in Chromium and Firefox and
cachednative:verificationin WebKit).mount()and the React adapters
compose it when it comes into range; a directtypeset()caller calls
again. The React adapters also compose text they mounted inside a closed
<details>,hidden="until-found"orcontent-visibility: hiddenonce
it is shown, and text in acontent-visibility: autosection on screen at
page load, as 4.2.0 did (the first 4.3 candidate left all of it native for
good). 0 golden cells change (the golden sets have no hidden text). TypesetRichTextplans once from the native text and no longer
re-plans over its own composed markup afterdocument.fonts.readywhen
nothing changed (P4, P5). 4.2.0's final breaks could depend on that
history. React golden diff: 2 of 2,160 blocks, both in WebKit at 320 px.
In one, 4.3 now gives the same breaks astypeset()for the same markup,
where 4.2.0's re-plan did not; the other is a block on which 4.2.0 differs
from itself between runs. Chromium and Firefox: 0. (2,040 more differ only
in the C2 and C9 attributes and in the order of the host's attributes, 71
of them also in the position of a break before a link.)- The legacy
renderFrozenLines()export no longer sets the non-ARIA
role="text", which emptied a composed heading's accessible name in
WebKit, and clears the element withreplaceChildren()instead of
innerHTML, so it runs under Trusted Types. - Transient states with no static golden effect: while a block's width is
changing and its composed lines no longer fit, it shows native wrapping
(data-ts-stale) until 100 ms after the size holds, or, when it is more
than a viewport height offscreen, until it comes that near (released to
native in idle time meanwhile); while a page is
machine-translated, owned text is released to native (native:translated);
where nothing can be measured (jsdom, happy-dom, engines without the
required APIs) text stays native (native:environment) where 4.2 threw.
Fixed
- Hidden content stays off the clipboard (C11). A copy that touches a
composed paragraph is serialized by the engine; 4.2 built its HTML from
range.cloneContents(), which keeps what native copy leaves out, so a
select-all or a cross-paragraph copy pasteddisplay:nonenotes, hidden
inputs such as CSRF tokens,visibility:hiddentext, templates, scripts
and styles. The clone is now walked in step with its source and those
nodes are dropped; if the two ever disagree, only plain text is written.
Copied hosts also lose the engine's residue, which pasted into an editor
that keeps inline styles: thetext-wrap-style: auto !importantoverride
(the author's own inline value is put back, so a pasted paragraph no
longer turns offtext-wrap: prettyorbalance),data-ts-stale, and
the React adapters'data-typeset-reactattributes, which made the
loaders skip pasted markup. Plain text from a copy inside one composed
paragraph is its text as it renders, as the browser's copy gives it:
4.2 pasted the source's own white space, so hard-wrapped HTML (Markdown
output, templated pages) pasted a newline and indentation mid-sentence,
with tabs and double spaces kept; and in Chromium and WebKit every copy
kept no-break spaces, which the browser's copy turns into spaces. - Strict Content Security Policy and Trusted Types (C5). Measurement and
line-wrap styles are restored through the CSSOM, never by writing the
style attribute. Understyle-srcwithout'unsafe-inline', 4.2 left
white-space: nowrapon paragraphs and links in Chromium and WebKit,
erased the author's CSSOM styles in Firefox (a 300 px width became the
container width), and logged a CSP error on every pass;TypesetRichText
declined asnative:rich-whitespace. 4.3 runs understyle-src 'self'; script-src 'self'; require-trusted-types-for 'script'; trusted-types 'none'with no violation in three engines. Published bundles no longer
read the bind-weight research global__TYPESET_BIND__. - Framework text updates never leave stale text (C6). When Svelte, Vue,
Solid, Lit or React (outside the adapters) set the.dataof a Text node
the engine had split, 4.2 replaced only line 1 and left the old lines 2..n
on screen, merged them back onrestore(), anddisconnect()kept them.
Solid and Lit updates were lost outright once a marker or tracking wrapper
took their node's position, and React threw (removeChild) when it removed
a Text node tracking had moved, unmounting the app. Now the mount observer
removes the stale fragments within the mutation's microtask, before any
frame, and recomposes; every cleanup drops the fragments of a node that
was written to or removed; and Text nodes Solid or Lit address by
position, and React's, stay in place (emptied, their text wrapped beside
them). Nothing goes between a comment and the Text node after it, which
keeps its text unwrapped: Lit starts each item of an array,map()or
repeat(), and a top-levelrender(), with an empty comment and writes
to the node after it, so those updates were lost too (4.2 lost them as
well). Tested with a vanilla renderer, React 19, Svelte 5, Vue 3.5, Solid
1.9 and Lit 3 (template parts, and iterables with a top-levelrender())
through twenty updates each in three engines. - Composed text follows text metrics, not only width (C7). Fonts that
finish loading (including CSS-requested fonts in WebKit, which fires no
loading events and left 59 of 195 paragraphs stale under 4.2.0), the
text-spacing overrides of WCAG 1.4.12, a browser font-size setting, CSSOM
rule changes, and transitions or animations of font, spacing or
line-height properties now recompose. A same-width height change makes
the controller check rendered lines against the composition, and makes
TypesetTextandTypesetRichTextcompare their layout key, so a CSSOM
rule, the font-size setting or a FontFace added by script (none of which
mutates the DOM or fires an event) no longer leaves them double-wrapped. - Hidden text keeps its composition (C8). A tab, dialog, accordion or
stack card hidden withdisplay:none, thehiddenattribute or
content-visibility, and shown again at the same width, paints its
composed lines in the first frame instead of native lines re-broken a
moment later, withmount(),TypesetTextandTypesetRichText. Text an
attribute change reveals at a new width composes before that frame paints.
Text that is hidden while the window's width changes shows native
wrapping from then on, so a reveal by CSS alone (a:checkedtab, a media
query) or by an attribute nothing observes paints no alternating long and
short lines; any other reveal at a new width shows native wrapping from
its second frame until it is recomposed. No reveal raises a
ResizeObserver loop error (the React adapters composed inside the
observer callback). - No double-wrapped frames during resizes or in print (C9). A block
whose width changes is recomposed once the size has held for 100 ms, not
every frame (withmount()and the loaders, a block more than a viewport
height offscreen once it comes that near); meanwhile, if it is narrower
than its widest composed line, it shows native wrapping. The switch is
written before the frame's layout. The rules live in a constructable
stylesheet, which the engine adds back if the page's own assignment to
document.adoptedStyleSheetsremoved it (printed text was double-wrapped
in three engines after such an assignment), and which a shadow root
holding composed text adopts too: a document's sheets do not reach into
shadow trees, so text composed withmount(shadowRoot, ...)was marked
stale to no effect through a resize (double-wrapped in 21 to 22 of 24
frames, against 5 to 18 in 4.2.0, which recomposed per frame) and kept
its tracking and spacing in print. Offscreen text left waiting is released
to native in idle time, so assistive technology never reads it with its
breaks hidden: Chromium drops the word space beside a hidden break and
WebKit joins words at the engine's markers, which after a narrowing
resize read 14 joined pairs in 9 offscreen blocks of a 48-block page.
Print shows native wrapping and pauses composition. A width only layout
can show (a stylesheet rule, a composed block moved into a narrower
container) can still paint its first frame double-wrapped. - Ancestor transforms no longer leave text native for good. A transform
changes no line, so a composition made before one is now kept: a drawer
that scales the page behind it or a card's hover scale no longer rewraps
composed text. Text declined asnative:transformedbecause a transform
was still animating (a dialog or popover's@starting-stylescale-in, a
class-driven scale, a card inserted withelement.animate(), the drawer
closing) is composed when that transition or animation ends, with
mount(), the loaders andTypesetText; 4.2.0 and the earlier 4.3
candidate left it native until an unrelated trigger. An animation that
never ends (an infinite pulse) is not waited on, so text under it stays
native as in 4.2.0, and blocks replaced or unmounted under it are not
kept in memory (the first round-2 candidate held every one while the
animation ran). In WebKit, font keys
no longer change when the engine collects an unreferencedFontFace
wrapper, which made the first recheck after composition recompose every
block on pages with@font-facerules.verify-reflow-triggerscovers
the drawer, the dialog and the scale-in in three engines (15 checks failed
before). The golden sets have no transforms: 0 changed. - Machine translation no longer garbles or loses text (C10). When the
page is translated (thetranslated-ltr/translated-rtlclass Google
Translate and Chrome set, a<font>wrapper inside composed text, or
Edge's_msttexthash),mount(), the loaders,typeset()and
TypesetTextremove their breaks and wrappers by moving the existing Text
nodes, never splitting, merging, editing or removing one, and compose
again when the translation ends.TypesetRichTextfreezes, and stays
frozen when its container narrows during the translation (a re-render
there would make React remove Text nodes the translator had replaced and
unmount the whole root). 4.2.0 merged and edited the Text nodes the
translator was filling, which lost sentences. The Text nodes a
composition split stay split while the page is translated, so a
translation can show a stray space before punctuation ("dijo ,", 3 of 9
paragraphs in a live Google Translate check); no text is lost
(SUPPORT.md). When the translation ends, they are merged and the smart
quotes straightened before the text is composed again, wherever the
translator left them as they were: a framework writing to its own Text
node after "show original" no longer leaves the old text's split tails on
screen beside the new text, anddisconnect()returns the author's markup
exactly instead of curled quotes and some 220 split Text nodes. A word
space beside a removed break is moved in place, so a screen reader's
live Chromium tree no longer reads joined words at every former break. mount()works in same-origin iframes (C12). Inserted paragraphs,
text edits, resizes and fonts inside a parent-mounted iframe document are
picked up, and copied links keep absolute URLs there (nodes from another
realm failedinstanceofchecks).keepworks in body text (C14). It was typed and exposed on both
React adapters, but body composition never received it. A kept phrase
that fits the measure is now never split, a phrase the browser splits can
earn one extra line (not withdensity: 'compact'), and a phrase longer
than the measure is split as few times as possible. Matching ignores case,
NBSP and punctuation around the phrase, in titles too. Output withkeep
omitted is unchanged (golden diff 0).- The audit is accurate (C16).
issue.targetresolves: every selector
startedhtml:nth-of-type(0)and matched nothing; targets now start from
a unique id, frombody, or are:root. Line-end reviews follow the
compositor's own policy: a word before sentence punctuation and a letter
designator are valid line ends, an abbreviation is not a sentence end, and
untagged text gets no English word lists.audit()fails composed output
that is no longer correct:stale-layout(a font, spacing or size change
after composition left a block double-wrapped, which 4.2.0 passed) and
alignment-lost. 4.2.0's own output carried thehidden-breakand
isolated-spaceerrors (C2); 4.3.0's carries neither. A hanging indent
(padding-leftwith a negativetext-indent, as in a bibliography) is no
longer reported asoverflow: 4.2.0 failed the CI gate with a hard error
for the first line of every such element in scope, native or excluded,
so a correct page with a reference list could not pass. Overflow is
still an error on any element, composed or not. - Installs beside any React 18.2+ or 19, and any Playwright (K3). The
React peer was^19.2.3and Playwright was a peer: npm failed with
ERESOLVE beside React 18, React 19.0 or 19.1 and older Playwright, and
upgradedreactalone in apps locked to 19.0 or 19.1, which left
react-dombehind ("Incompatible React versions").TypesetTextno longer
schedules a layout effect during server rendering, which React 18 warned
about. The published declarations no longer inline @types/react 19.2
internals, so they compile against @types/react 18.3 and 19.0. - Nothing throws in test runners or older engines (K4). Under jsdom and
happy-dom (Jest, Vitest), and whereIntl.Segmenter,ResizeObserveror
MutationObserveris missing,typeset(),planRichText()and both
adapters reportnative:environment,mount()returns an inert
controller anddocument.fontsis optional. Importing no longer
constructs anIntl.Segmenteror inflates the Unicode line-break trie
(the vendored module initializes on first use; all 19,338 LineBreakTest
cases give identical breaks), so a missing API cannot blank an application
at import. The script-tag builds, includingtypeset.us/auto, do nothing
where there is nowindow. typeset-auditworks on pages under a strict Content Security
Policy. The CLI injected its inspector as an inline<script>, which
script-src 'self'(helmet's default) and Trusted Types refuse, so the
documented CI command exited 2 with a runtime error on exactly the sites
that follow SUPPORT.md's CSP guidance (4.2.0's CLI did the same). It now
runs the inspector through the browser automation protocol; the page's
policy still governs everything the engine does there, so a site whose
own composition breaks under its policy still fails the audit. Tested
understyle-src 'self'; script-src 'self'; require-trusted-types-for 'script'; trusted-types 'none'in Chromium, WebKit and Firefox.typeset-auditpasses correct pages longer than one screen. Without
--apply, the CLI waited a fixed 150 ms after load, whilemount()and
the loaders compose what is on screen first and the rest in later
batches, so the documented CI command failed a correct 200-paragraph
article with 176 to 193 paragraphs "unprocessed" in every engine (so did
4.2.0's CLI withgo@4.2.0.js). It now waits for the page's own
composition: the loader'swindow.TypesetReady, or an outcome on every
element in scope for pages that compose from their own code, for up to
--timeoutseconds (default 30) per width; a page whose composition makes
no progress for 2 s is audited as it is. Each report records what it
waited for (waited), and names the elements still unprocessed.- Clear errors (K11).
typeset()throwsTypeError: [typeset] typeset() expects an HTMLElement (received ...)for a non-element, andmount()a
TypeErrornaming what it received, instead of raw TypeErrors from inside
the engine. An invalidkeepvalue no longer throws oncekeepreaches
body composition; like every invalid option it keeps its 4.2 behaviour. - The React adapters keep their composition while hidden under the new
per-document registry, instead of treating a width of 0 as a resize and
showing native lines when revealed. - Text in a box sized by its content settles (React adapters). A
TypesetTextorTypesetRichTextblock in a box that shrinks to fit its
content (a flex item withoutflex: 1ormin-width: 0,w-fit,
inline-block, a float, an auto table cell) narrows that box to its
composed lines. The 4.3.0 release candidate counted that narrowing as a
resize, and measured a line that starts with an optically hung quote or
capital with the hang, which the narrowed box leaves no room for, so the
block showed native lines, widened, composed and narrowed again every
~100 ms without end: on newworldadvertising.org/awards 7 of 8 widths in
Chromium, with up to 3,026 DOM mutations in 3 s per block. The adapters
now measure composed lines from the content box's edge, asmount()does,
record the size a composition leaves as their own rather than as a
resize, and keep a composed block whose lines still fit when only
compositions changed its size (its own, or a neighbour's in two auto grid
tracks or table cells, which took three rounds of a second each to settle
offscreen).mount()and the loaders settled in these boxes, but not in
two auto grid tracks (next entry). 4.2.0's
TypesetRichTextalso recomposed without end in Chromium in a
shrink-to-fit flex item and an inline-block at 768 and 1,280 px (27,360
to 71,520 DOM mutations in 3 s). The newverify-settlesuite holds
mount(), go.js and both adapters to no DOM mutation or outcome change
from 2 s to 5 s after the page is ready and after a resize, on a page of
20 layouts (the /awards card, shrink-to-fit flex items,w-fit,
inline-block, auto table cells, auto andmin-contentgrid tracks,
floats, absolutely positioned boxes, a centred flex column, nested
scrollers,<details>and<dialog>) at 320, 375, 424, 492, 728, 768 and
1,280 px in Chromium, WebKit and Firefox: the release candidate failed 21
of 24 of these checks forTypesetTextand 15 or 16 of 24 for
TypesetRichText(two runs, at the four widths the suite then had), in 12
and 9 layouts; 4.3.0 fails none of the 168 it now makes for the four
integrations. With its layout workaround
removed, no text block on the /awards page changes in 3 s after settling,
at 8 widths in three engines. - Two auto grid tracks settle under
mount()and the loaders. Two
blocks side by side ingrid-template-columns: auto autoeach change the
other's width when they compose. The 4.3.0 release candidate'smount()
and go.js counted that as a resize, with no resize or other outside
change: the tracks traded about 3 px every 110 ms without end, and the
first block's height flipped between six and eight lines, moving
everything below it. From 15 s after loading, that was 10,366 to 13,572
DOM mutations in 3 s undermount()at 424, 492 and 728 px in Chromium
(9,472 at 728 px in WebKit), and 9,850 to 13,286 under go.js (4,771 in
WebKit); 4.2.0 made about 137,000.mount()now keeps a composed block
whose lines still fit when only its own compositions changed its width
since the last resize observation, as the React adapters do, and records
it as composed at that width: no DOM mutation from 2 s after loading at
those widths in Chromium, WebKit and Firefox, or after resizing across
640 to 740 px. A block whose lines no longer fit still shows native lines
and recomposes once the size holds.verify-settleholds this layout at
424, 492 and 728 px; the release candidate fails itsmount()and go.js
checks at 492 and 728 px. - Smart quotes stay curly when text falls back to native lines. With
smartQuotes="en", aTypesetTextblock showing native lines while its
width changed (data-ts-stale), or on a reveal whose composition changed
its height, showed the source's straight quotes, because releasing the
composition put them back: on newworldadvertising.org/team, with the
release candidate, up to 8 of 9 quote-bearing blocks did in 63 of 356
frames of a window resize in Chromium, 84 of 176 in WebKit and 355 of 357
in Firefox; 0 now. It keeps the characters its composition showed. A
TypesetRichTextkept native because a child is a component
(native:react-component) rendered its text as written while it reported
quotesenabled; it now educates the text it renders and the text it
passes to that component (4.2.0 did not either).verify-settlechecks
every animation frame of loading, resizing, stale, declined and
offscreen states: the release candidate showed a straight quote in 11 of
12TypesetTextand 12 of 12TypesetRichTextchecks, 4.3.0 in none of
48, andmount()and the loaders in none before or after. Copying text
in any of these states copies the curly quotes, as copying composed text
does.
Added
- Outcome reference and types:
OUTCOMESlists every outcome code; the
Outcome,FeatureStatus,QuoteStatus,HangingStatus,
SpacingStatusandTrackingStatustypes name them.Result.outcomeand
Result.featuresuse these types while still accepting any string, so
existing code compiles unchanged. OUTCOMES.md (in the package and at
docs/outcomes.md) says what each of the 42 outcomes and 33 feature
statuses means, whether it is expected, and what to do. New outcomes:
native:justify,native:live-region,native:translatedand
native:environment; new tracking status:native:tracking-comment. - React adapters (K5): a
refresolves to the host element (both are
forwardRefcomponents; a ref was dropped before, andTypesetRichText's
gave its class instance).asalso acceptsdiv,li,blockquote,
figcaption,dd,dt,td,th,caption,label,legendand
summary, withcite,colSpan,rowSpan,headers,scope,htmlFor
andvalueattributes.onResult(result)reports each composition as a
Result.priority?: 'auto' | 'sync':'sync'composes in the commit, as
4.2 did for every block. New typesTypesetTag,TypesetAdapterPropsand
Priority. - A CommonJS React entry (
dist/react.cjs) forrequire()and Jest;.
and./reactgiveimportandrequiretheir own types (.d.tsand
.d.cts), so CommonJS consumers are not told the package is ESM-only;
./reacthas adefaultcondition;./globaland./goship
global.d.tsandgo.d.tsdeclaringwindow.Typesetand
window.TypesetReady,./autohas the same types as./go, and
typesVersionsmaps all three formoduleResolution: node.react-dom, which the React entry imports for
flushSync, is an optional peer besidereact. mount('article p', options), the same asmount(document, 'article p', options); a string first argument used to throw.- Development warnings: an invalid option value, an unknown option or a
non-string selector logs oneconsole.warneach (the ESM and CommonJS
entries unlessprocess.env.NODE_ENVisproduction; always in
typeset.global.js,go.jsandauto.js), for example[typeset] smartQuotes must be "en" or false (received true). Production bundles
contain none of these checks; SUPPORT.md lists the messages.
TypesetRichTextwithsmartQuotes="en"but nolangof its own warns
once in development builds. - CSS hooks: the custom property
--ts-break-displaydrives every generated
break (set it toinlinein print CSS to print a composition), and
data-ts-stalemarks a block temporarily showing native wrapping.
dist/styles.cssships the rules; the engine also installs them as a
constructable stylesheet. - Audit: errors
stale-layoutandalignment-lost; reviewsbound-split
(a number and its unit, an honorific and a name, a label and its number,
or a word and its letter designator split across lines),split-ellipsis,
line-initial-punctuationandregressed-vs-native(a composed element
with more line-end reviews than its native layout had).schemaVersion
is still 1 and field shapes are unchanged. - The automatic website loader on npm as
typeset.us/auto(dist/auto.js),
byte for byte the same file ashttps://typeset.us/go@<version>.js, so
npm, jsDelivr and typeset.us serve one file with one integrity hash. Each
loader logs oneconsole.infowhen no element matches.
Performance
Measured with npm run bench on an Apple M2 Pro, 4.2.0 and the 4.3.0
candidate back to back (load average 4.6 to 6.2); docs/BENCHMARKS.md has every
table.
- Composition costs less: per paragraph (200-paragraph article), median
5.7 to 5.0 ms and p95 20.3 to 14.9 ms in Chromium; 25.8 to 21.1 ms and
88.2 to 64.9 ms at 4x CPU; 24 to 19 ms and 100 to 67 ms in WebKit. The
paragraph search is memoized and its segmenters are shared and created on
first use, with byte-identical output (P6). mount()finishes sooner and blocks less: a 200-paragraph article is
fully composed in 1.48 s (4.2.0: 1.74 s), 6.4 s at 4x (8.0 s) and 8.2 s in
WebKit (10.1 s); total blocking time at 4x falls from 1,100 to 396 ms, and
from 8,293 to 3,501 ms for 1,000 paragraphs. The scheduler no longer
starves on a busy page: text near the viewport composes in the next task,
an idle callback that fires on its timeout still gets its 8 ms budget, and
offscreen blocks wait until they come near after a resize (P2).- Ancestor class and style changes recompose nothing when layout is
unchanged: 30 class toggles on an ancestor of 200 paragraphs recomposed 26
times with 9,984 DOM mutation records in 4.2.0, and 0 times with 30
records in 4.3.0 (P3). Each such change still rechecks a layout key for
every block inside it (about 15 to 23 ms for 1,000 paragraphs at 4x CPU).
A translation or fade (onlytransform: translate(...),translateor
opacitychanged), which JavaScript animations write every frame, is not
rechecked at all: 120 frames of an ancestor translation over 1,000
paragraphs at 4x CPU in Chromium average 8.3 ms with none over 20 ms (the
first 4.3 candidate: 22.8 ms, 84 of 118 over 20 ms). - React screens (P4, P5): pushing 38
TypesetTextblocks takes 192 ms from
the click to its first paint at 4x CPU (INP proxy; 4.2.0: 232 ms, plain
React: 56 ms), with one pre-paint task of 161 ms, and 64 ms at 1x (72
ms), with every on-screen block composed in that paint. The commit alone
falls from 160 to 23 ms at 4x (42 to 7 ms at 1x), but that is mostly
work moved, not removed: 4.2.0 composed every block inside the commit,
while 4.3.0 composes there only what fits a 6 ms budget and the rest of
the on-screen blocks in the next frame, before it paints. Earlier 4.3
candidates counted the page's one-time setup (80 to 90 ms at 4x) against
that frame's 120 ms safety limit, so a cold first screen at 4x painted
half its on-screen blocks natively and rewrapped them a frame later;
their 152 ms was measured to that partial paint. 1,000 blocks commit in
22 ms (1,517 ms) with an INP proxy of 88 ms (1,656 ms). 38
TypesetRichTextblocks commit in 28 ms at 4x (76 ms) with an INP proxy
of 208 ms (288 ms) and 835 DOM mutation records (3,893), and 1,000 in 44
ms (272 ms) with 3 ms of blocking time (5,742 ms). Every adapter in a
document shares one registry:
2 MutationObservers, 1 ResizeObserver and 1 IntersectionObserver however
many blocks render (4.2.0: one of each per block), parent re-renders that
change nothing (an inlinekeeparray, fresh JSX, inline styles or
callbacks) write nothing, and unmounting no longer restores the host React
discards. Offscreen blocks compose only in idle periods of 20 ms or more
(no frame pending), or one at a time after waiting 1 s, never in what is
left of an animation frame, so a screen push drops no frames for them: 4.2
dropped about 8 frames per slide at 4x CPU in Chrome, and the first 4.3
candidate still composed in 41 of a 900 ms slide's short idle periods
(Firefox 17). Engines without idle callbacks (WebKit) keep a 50 ms timer.
Near means within a viewport height of the window or of the scroll
container the text scrolls in: in an app shell'soverflow: autopane,
text below the pane's fold counted as far until it was on screen, so
blocks scrolled in within about 0.5 to 1 s of a load or screen push
(while animations kept frames pending) painted native lines and were
rewrapped 2 to 5 frames later, which 4.2 did not do; now none within a
viewport height of the window or pane do in the verify-scheduler
fixtures, in three engines.mount()measures nearness the same way.
Three limits remain, all under Known limitations: a wrapper that scrolls
only horizontally counts as a scroll container too, so offscreen blocks
under anoverflow-x: hiddenapp root or in a carousel row count as
near and can compose during a push; blocks further than a viewport
height away still wait for idle time; and in Chromium, text scrolled
into view during its first frames after it mounts can still paint native
lines for one frame. - React hosts under continuous change: a translation or fade written every
frame on their container is no longer checked at all, as withmount()
(16 blocks, 60 frames: about 10,000 computed-style reads to 0 in three
engines). A host whose text metrics change again within 100 ms of a check
composing it (a font-size or spacing transition, a text-size slider)
shows native lines until they hold for 100 ms and is composed once (a
font face finishing, even right after another, composes at once and
never shows native lines), where
the first 4.3 candidate recomposed every on-screen host in every frame: a
60-frame text-size slider over 16 blocks at 4x CPU went from 466
compositions and 30 or more long tasks to about 40 and 2, and a 1.2 s
font-size transition from 3 or 4 long tasks to 1. - Revealed hidden text: 10 compositions instead of 196 and 83 ms of blocking
time at 4x instead of 1,075 ms. The first viewport of never-composed
revealed text finishes later (428 ms at 4x against 249 ms), because
on-screen reveals compose for at most 24 ms before the frame and
offscreen revealed text waits until it is near. - Per document the engine now adds one lifecycle hub: one MutationObserver
(stylesheets and the translation class; other<head>writes, such as a
ticking title, an injected script or a favicon badge, trigger no
recheck), three document listeners
(transitionend,animationend,contentvisibilityautostatechange) and
one more font listener, shared by every controller and adapter. - Download, gzip (esbuild bundles importing one entry point, tree-shaken):
mountonly 37.7 to 49.8 KB,TypesetTextonly 38.0 to 49.9 KB,
TypesetRichTextonly 33.4 to 46.2 KB,smartQuotesonly 11.7 to 1.5 KB
(the line-break tables now tree-shake away),go.js42.3 to 55.9 KB and
typeset.global.js42.1 to 55.6 KB. A bundler that does not tree-shake
(Metro, the Expo and React Native Web default) ships all of
typeset.us/react,dist/react.jsand its shared chunk:TypesetText
43.2 to 62.2 KB, a larger increase than any tree-shaken figure (an Expo
web export of one app grew by 17.0 KB gzip, 434,777 to 451,735 bytes,
with a 4.3.0 release candidate).
The growth is the code the fixes above need, measured per group and
recorded in scripts/v4/budgets.json.
Known limitations
- Offscreen text under a wrapper that scrolls horizontally counts as
near. The nearest ancestor that scrolls on either axis is a text's
scrollport. An app root withoverflow-x: hidden(itsoverflow-ythen
computes toauto) or a horizontal carousel row is such an ancestor, so
every block in it is near however far below the fold, and during a
screen transition those blocks compose in animation frames instead of
idle time: in verify-scheduler's fixture, 44 of 44 offscreen blocks
under such an app root and 22 in 11 carousel rows composed during a
900 ms push in Chromium and Firefox. This costs frames on a slow device.
In WebKit it can also cost the typesetting itself: when the cards in
such a carousel run a CSS entrance animation, about half of them decline
asnative:verificationand keep the browser's own line breaks (23 of
48 in verify-scheduler's fixture, where 4.2.0 composed 47). The text is
always correct: every block paints what it reports, its composed lines
or native ones, never double-wrapped. On an app root,
overflow-x: clipclips the same way without making a scroll
container. Four attempts in the 4.3 cycle to measure such text against
the window each broke text that a real pane hides (rows a virtualized
list places with a transform, a pane whose content grows above its
text), so 4.3.0 keeps the round-2 release candidate's rule, and a fix is
planned for 4.4. - Text scrolled into view during its first frames, in Chromium. A
block that a scroll brings on screen within its first frames after it
mounts (verify-scheduler's nested scroller scrolled 80 px a frame from
0 ms) can paint its native lines for one frame and rewrap in the next:
8 of 20 runs in Chromium, as in the round-2 release candidate, and 0 of
8 in Firefox and WebKit. 4.2.0 did not do this. Planned for 4.4. - Prose far below an app shell's fold, on a jump soon after load.
Text more than one viewport or pane height below the fold composes in
idle time, so a jump (a scrollbar drag, End, an anchor link, one large
wheel delta) that brings it on screen soon after a load, or while an
animation keeps frames pending, paints its native lines for one frame
and rewraps in the next (Wellth's home screen, 150 to 300 ms after its
prose rendered: 4 of 4 jumps at 1x, 2 of 4 at 4x; 4.2.0, which had
composed everything by then: 0). With the React adapters,
priority="sync"composes a block in its commit instead. Planned for
4.4.
SUPPORT.md lists both.
Packaging, loaders and CDN
- npm metadata: a plain description, keywords (typography, line-breaking,
text-wrap, orphans, widows, knuth-plass, hanging-punctuation, react and
others),repository.directoryso README links resolve on npmjs.com, and
bugs.url. Theenginesfield (node >=22) is removed: it made Yarn 1
refuse installs of a browser library on older Node. Thetypeset-audit
CLI needs Node 18.3 or later.licenseisMIT AND Unicode-3.0. - The unpacked package is 1.88 MB (87 files), down from 2.57 MB, 73% of
which was source maps embedding every engine source. The ESM and CommonJS
builds stay readable and unminified and ship without maps (your bundler
minifies them; stack traces name real functions);typeset.global.jsand
go.jskeep maps without embedded sources. Each bundle ends with the
license notices of the code it embeds (@cto.af/linebreak,
unicode-trie-runtime, fflate and the Unicode line-break data), which
minification had stripped. - Bare
cdn.jsdelivr.net/npm/typeset.usandunpkg.com/typeset.usURLs serve
dist/typeset.global.js(thejsdelivrandunpkgfields), not the
CommonJS build that browsers refuse to run. - From the 4.3.0 cut, typeset.us publishes versioned
typeset@<v>.min.jsand
typeset@<v>.esm.js, andsri.jsonlists only immutable paths; the
go@4.2.0.js entry is unchanged.go@4.jsfollows 4.x, andgo.jsand the
other unversioned aliases follow 4.x only: a 5.0 release will never move
them. Versioned files are cached for a year, aliases and indexes for five
minutes, all withAccess-Control-Allow-Origin: *.
docs/ops/vercel-firewall.md has the firewall bypass that stops bot
challenges on these paths; it is applied in the Vercel project, not here. - The build recipe is versioned (
scripts/build-recipe.mjsRECIPES): a cut
at an older tag uses that line's recipe, andverify-recipe-reproduces
rebuilds the 4.2.0 tarball from its tag byte for byte (CI packs with npm
11.6.0, the version that packed it).
Release trust
- 4.3.0, like 4.2.0, is published to npm by the maintainer account from the
reproducible tarball the cut recorded in the ledger,
public/releases/4.3.0/typeset.us-4.3.0.tgz, and the registry's
integrity is checked against the ledger. It has no npm provenance
attestation: npm trusted publishing is not configured for the package yet. .github/workflows/release.ymlpublishes from av*tag, with npm
provenance, once trusted publishing is configured (docs/OWNER-ACTIONS.md).
It runs only after CI passed on the tagged commit,
scripts/v4/release-check.mjsrebuilt the ledger-recorded tarball from the
tag byte for byte, and every suite passed against the committed dist; the
publish job checks out that verified commit, not the tag by name, and
stops if the tag was moved during the approval wait. It publishes exactly
public/releases/<v>/typeset.us-<v>.tgzwith npm trusted publishing and
provenance, checks the registry's integrity and attestation, and creates a
GitHub Release from the CHANGELOG section with the evidence attached. A
version the maintainer account already published with the ledger's
integrity (scripts/v4/registry-state.mjs) is not published again: the
workflow skips the attestation check and creates the GitHub Release with
notes that say it has no provenance. Other bytes under the version on npm
fail the workflow. docs/RELEASING.md describes both paths;
docs/OWNER-ACTIONS.md lists the GitHub, npm and Vercel settings only the
owner can apply.- CI and nightly run with
contents: readonly, every action is pinned to a
commit SHA, and Dependabot watches npm and GitHub Actions. - SECURITY.md (also in the package): supported versions, private reporting,
response targets. The DOM XSS in the 3.xTypeset.auto()heading branch,
fixed silently in 3.4.1, now has an advisory (its proof of concept
reproduced against the 3.3.2 and 3.4.0 pins), a deprecation command
for 3.0.0 to 3.4.0 and anadvisorieslist that each cut copies into
release.json and sri.json. The vulnerable files stay online unchanged. - STABILITY.md states what a version number promises: API names,
auditJSONschemaVersion1, outcome codes, CLI exit codes and published
bytes do not break in 4.x, and a minor release changes default rendering
only to fix a verified defect, listed under "Rendering changes" with its
golden-diff count. Install withnpm i -E, or pingo@<version>.jswith
its integrity hash.
Website (typeset.us, not the package)
/api/fetch-urlno longer reaches private networks: it connects only to a
checked address, refuses loopback, private, link-local (including
169.254.169.254), CGNAT, multicast and reserved addresses and their IPv6
forms, follows at most five redirects and checks each, allows ports 80,
443, 8080 and 8443 only, caps the body at 500 KB after decompression, and
rate-limits each client to 12 requests a minute./auditand/dnarender fetched or pasted HTML inert (scripts, frames,
plugins,<base>, refresh<meta>, event handlers andjavascript:URLs
removed;/auditin a shadow root,/dnain a sandboxed iframe)./dna
no longer fails on pages with inline SVG./pairing-cardsbuilds its downloadable cards from DOM properties and
text instead of markup, and takes from a shared link's query string only a
listed font and hex colours. A crafted link could insert markup into the
generated card with one click on "Generate Cards": under the new CSP a
refresh<meta>that redirected to any site and images that pinged one,
and without it (as on typeset.us until now) event handlers that ran.- Every page has a nonce-based Content Security Policy (
script-srcwith a
per-request nonce andstrict-dynamic,object-src 'none',base-uri 'self',frame-ancestors 'self'), plusnosniff,X-Frame-Options: SAMEORIGINand a referrer policy. Pages are rendered per request so each
gets a fresh nonce. Next.js is 16.3.6;npm auditreports no
vulnerabilities. - The homepage names its baseline by engine, leads with the short words the
browser leaves at line ends, and claims a one-word last line only in an
engine that produces one. A "For developers" band gives the pinned script
tag, npm and React, a "Do I need it?" table, the measured gzip size from
release.json and links to framework recipes at /install/frameworks
(Next.js, Vite, Astro, SvelteKit, Vue). Every install line on the site is
generated frompublic/sri.json. /, /support and /library have their own
titles, descriptions and unfurl images; /faq is new.
Documentation
- The npm README is an introduction, not release notes: what Typeset does, a
375 px before/after image againsttext-wrap: pretty, a "Do I need it?"
table, pinned install paths, every option with its default, the common
outcomes, what it costs, what it won't do, browser requirements,
accessibility, baseline CSS, an FAQ, the stability promise and a glossary.
Every link is absolute, so it works on npmjs.com. The repository README's
install lines are generated from the published version and sri.json at
each cut. - SUPPORT.md describes 4.3.0 and lists known limitations: generated line
breaks and find-in-page, Text Fragments,innerTextand selection; print;
machine translation; CSP and Trusted Types; no hyphenation or
justification; right-to-left text; the browser floor; framework-owned
text; far text brought on screen by a jump; offscreen text under a
wrapper that scrolls horizontally. MIGRATION.md covers 4.2 to 4.3 and
links each older guide in its archive. - The README, the agent contract and
typeset-audit --helpsay that any
uncaught error or unhandled rejection on the audited page fails the run
(exit 1, listed undererrors) even when every audit passes, as it has
since 4.0; they described exit 1 only as a failed audit. - Stale 3.x claims are gone from current docs and the site ("audit() returns
[]", 1.4 to 1.6 ms per paragraph, 20 KB, English only, cloned links). The
Show HN kit, the essay, SKILL.md's frontmatter, the agent contract and
llms.txt describe 4.3; the 3.5 agent pages are marked historical.
docs/BENCHMARKS.md is generated from the 4.3.0 benchmark, with 4.2.0
alongside. - CONTRIBUTING.md, CODE_OF_CONDUCT.md (the Contributor Covenant 2.1),
ROADMAP.md, a "Bad line break" issue form that asks for the URL, width,
font, browser, version,auditJSONoutput and a screenshot, an
integration question form, and a pull request template with the
rendering-change checklist.
Development
npm run build:dist(alsobuild:candidate) builds the engine into
output/candidate/only. Releases are cut bynpm run release:cut, the
only step that writespackages/typeset-v4/dist,public/releases/<v>/,
public/go@<v>.jsor a version number.public/releases/published.jsonrecords every published artifact by hash;
verify:ledgerand CI fail if any changes.npm testbuilds the candidate and runs every suite against it;npm run test:releaseruns them against the committed dist.npm run benchis the V4 benchmark (scripts/v4/bench-v4.mjs), and
scripts/v4/bench-report.mjsrenders docs/BENCHMARKS.md from it, with
--baselinefor a comparison. Size budgets run on every change; runtime
budgets (verify-budgets --runtime) nightly and at release cut.- New suites in
npm test, each in Chromium, WebKit and Firefox where it
uses a browser:verify-native-ax(engine accessibility trees),
verify-golden,verify-alignment,verify-keep,verify-audit,
verify-break-semantics,verify-strict-csp,verify-copy-privacy,
verify-framework-text(with committed Svelte, Vue, Solid and Lit
fixture bundles intests/frameworks/),verify-live-regions,
verify-smart-quotes,verify-iframe-mount,verify-recompose-storms,
verify-reflow-triggers,verify-visibility,verify-print-resize,
verify-scheduler,verify-settle,verify-translation,verify-options,verify-react,
verify-react-node,verify-recipe-reproduces,verify-site-index,
verify-docs,verify-release-trustandverify-package-contents. Each
behaviour suite fails on the published 4.2.0 build. CI-only and nightly
lanes:verify-test-runners(Vitest, Jest, attw, publint),
verify-react-matrix,scripts/site/verify-site.mjsand
verify-safe-fetch.
Install
npm i -E typeset.us@4.3.0<script src="https://typeset.us/go@4.3.0.js" integrity="sha384-yHVZ46QyyOffm12e72GsBgkcZ96FcCaqK7POZ5/BGDkmfjEZ1dLV81vmBvCusTUX" crossorigin="anonymous" defer></script>Pinned files never change. Archive, docs and every artifact:
https://typeset.us/releases/4.3.0/
Integrity
- npm tarball
typeset.us-4.3.0.tgz: sha15d74e1a39a6c72f8bed4f1f95cd166a8cbd14abd,sha512-IoQn8oRTO87DSGq+imKIFLRybvse3f/+uLxu7pGeC65Mff8R1Vvcy+Sz1mFnZC0/Ii6PEGpr45ZQTz6wbDMStw== - The same tarball is attached below. It is recorded in
published.json, the
append-only ledger of every published file. - Published to npm by the maintainer account, not by GitHub Actions, so it has no npm provenance attestation. The release workflow (run 36296406776) rebuilt this tarball from the tag byte for byte (
release-check.json), andverify-ledger.mjs --networkconfirmed that the registry serves the ledger's integrity.
Evidence
Attached to this release (retained with the release, unlike CI artifacts):
acceptance.json: 78 checks, 0 failed.alignment.json: 78 checks, 0 failed.audit.json: 78 checks, 0 failed.break-semantics.json: 71 checks, 0 failed.budget-verification.json: 8 checks, 0 failed.cli-verification.json: 31 checks, 0 failed.controller-regression.json: 66 checks, 0 failed.copy-privacy.json: 75 checks, 0 failed.docs-verification.json: 43 checks, 0 failed.fixture-invariants.json: 72 checks, 0 failed.framework-text.json: 339 checks, 2 failed.golden.json: 194 checks, 0 failed.iframe-mount.json: 42 checks, 0 failed.immutable-4.2.json: 17 checks, 0 failed.inline-code-regression.json: 267 checks, 0 failed.inline-nowrap-regression.json: 150 checks, 0 failed.keep.json: 254 checks, 0 failed.ledger-guards.json: 6 checks, 0 failed.ledger-verification.json: 61 checks, 0 failed.live-regions.json: 57 checks, 0 failed.loaders.json: 240 checks, 0 failed.mount-ownership-regression.json: 69 checks, 0 failed.name-groups-regression.json: 162 checks, 0 failed.native-ax.json: 76 checks, 0 failed.options.json: 86 checks, 0 failed.package-contents.json: 29 checks, 0 failed.print-resize.json: 53 checks, 0 failed.promise.json: 3086 checks, 0 failed.react-node.json: 61 checks, 0 failed.react.json: 231 checks, 0 failed.recipe-reproduces.json: 5 checks, 0 failed.recompose-storms.json: 53 checks, 1 failed.reflow-triggers.json: 46 checks, 0 failed.release-check.json: 12 checks, 0 failed.release-craft.json: 81 checks, 0 failed.release-trust.json: 60 checks, 0 failed.scheduler.json: 91 checks, 1 failed.settle.json: 420 checks, 0 failed.site-index.json: 40 checks, 0 failed.smart-quotes.json: 85 checks, 0 failed.spacing.json: 5384 checks, 0 failed.strict-csp.json: 30 checks, 0 failed.test-v4-summary.json: 45 suites, 42 passed, 0 known failing, 2 slow on the hosted runner (speed-calibrated checks reported, not enforced; see scripts/v4/speed-calibrated.json), 1 failed; 13602 checks (release).tracking-clipping.json: 810 checks, 0 failed.translation.json: 36 checks, 0 failed.visibility.json: 291 checks, 0 failed.wrap-ownership-regression.json: 90 checks, 0 failed.
The evidence comes from the release workflow run above. Its only enforced failure is a timing check in recompose-storms.json: in Chromium at 4x CPU, a React font-size transition made 88 compositions for 16 blocks, against a limit of 64, in the first of three runs. It still ended composed with no stale block, and the other two runs made 36 and 37. CI run 36294621257 on the same commit passed that check. The failures in framework-text.json and scheduler.json are speed-calibrated checks, which are reported but not enforced on hosted runners.
Support range and testing limits:
https://typeset.us/releases/4.3.0/SUPPORT.md