@typstream/html-compat 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ikram Hasan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,61 @@
1
- # Temporary Holding Version
1
+ # @typstream/html-compat
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Typst 0.15.1's HTML export drops layout elements it can't express, and the text inside them: `#grid[…]`, `#align(center)[…]`, `#rect[…]`, `#place(…)`, `#stack`, `#columns`, `#pad`, the transforms and `layout` all come out empty ("grid was ignored during HTML export"). This package is a Typst prelude that maps each one to HTML (or, for drawings, an inline SVG), plus the CSS for that HTML.
4
+
5
+ ```ts
6
+ import { HTML_COMPAT_PATH, htmlCompatCss, htmlCompatPreamble, htmlCompatSource } from "@typstream/html-compat";
7
+
8
+ engine.set_source(HTML_COMPAT_PATH, htmlCompatSource); // mount the prelude
9
+ engine.set_source("/main.typ", htmlCompatPreamble + document); // diagnostics: shift spans by the preamble's length
10
+ const { html } = engine.compile_html();
11
+ // In the page: <style>{htmlCompatCss}</style> and the output inside <div class="typstream">.
12
+ ```
13
+
14
+ `htmlCompat` is the same as a `TypstreamPlugin` (files, preamble, sanitizer allowances). The preamble imports the prelude, applies it with `#show: html-compat`, and replaces `layout` with one that works in HTML. The rules act only when `target()` is `"html"`, so the same preamble leaves paged output byte-identical (tested).
15
+
16
+ ## What it does
17
+
18
+ | Element | HTML |
19
+ |---|---|
20
+ | `grid` | CSS grid (`div.ts-grid`). Cells are placed like Typst places them (spans, explicit `x`/`y`); fill, stroke, align and inset may be functions of `(x, y)`; `hline`/`vline` become cell borders. |
21
+ | `table` | Native `<table>`; each cell gets a `div.ts-cell` with its resolved fill, inset and alignment. `stroke: none` removes the borders. |
22
+ | `align` | `div.ts-align` with `text-align`; blocks inside move the same way. Vertical alignment is dropped. |
23
+ | `block`, `box` | `div.ts-block` / `span.ts-box` with width, height, fill, stroke, inset, radius, baseline. |
24
+ | `rect`, `square`, `circle`, `ellipse` | `div.ts-shape` with the same properties. |
25
+ | `stack`, `columns`, `colbreak`, `pad` | flexbox, CSS columns, `break-after: column`, padding. |
26
+ | `place` | Absolutely positioned against the nearest box, block or shape, inside its `inset` as in Typst (the container writes its inset as `--ts-inset-top`/`-right`/`-bottom`/`-left`); text with a vertical alignment gets Typst's text box, cap height to baseline (`text-box`). `float: true` stays in flow. |
27
+ | `v`, `h` | Fixed-size gaps. Fractional spacing (`h(1fr)`) becomes a small gap. Weak spacing and spacing in math are left to Typst. |
28
+ | `pagebreak` | `div.ts-pagebreak`: a rule on screen, a page break in print. |
29
+ | `move`, `rotate`, `scale`, `skew` | CSS transforms. `hide` is `visibility: hidden`. `repeat` repeats its body, clipped to the box. |
30
+ | `line` | A border when horizontal; otherwise drawn. |
31
+ | `curve`, `polygon`, angled lines; boxes and blocks that hold them; boxes and blocks made only of `place`d pieces (diagrams, CeTZ canvases); equations with `cancel` | Inline SVG (`html.frame`) in a `.ts-frame` with `role="img"` and the text as `aria-label` (`aria-hidden` when it has no text). |
32
+ | Numbered block equations | `div.ts-equation` with the number. `overline` in math becomes an accent (MathML has no overline). |
33
+ | `enum` with a pattern (`"a)"`) | Explicit markers. |
34
+ | `layout(f)` | Calls `f` with a fixed size: `compat-size`, the text width of an A4 page. |
35
+
36
+ Inside a box (an inline element), the prelude writes `span.ts-b` instead of `div`: a `<div>` there would make the browser close the paragraph and move everything after it out.
37
+
38
+ ## Known gaps
39
+
40
+ - **Pages need @typstream/dom.** The prelude writes each block's page settings (size, margins, columns, binding,
41
+ numbering, header and footer) on the engine's source anchors, and marks where the page counter is set; dom's HTML
42
+ renderer lays the pages out from them. Typst still warns ("page set rule was ignored during HTML export", listed in
43
+ `KNOWN_HTML_WARNINGS`).
44
+ - **Drawn content** (`.ts-frame`) is SVG: its text is in `aria-label`, not selectable. A drawing without text is hidden from assistive technology (`aria-hidden`): there is nothing to read out.
45
+ - `enum(full: true)` with a pattern shows plain numbers; the `augment` line of a matrix is dropped by Typst's MathML export.
46
+ - **Table** strokes other than the default or `none`, and `table.hline`/`table.vline`, are not drawn.
47
+ - **fletcher 0.5.8** diagrams stay empty: its cetz 0.3.4 canvas calls `layout` inside `context`, within the package, where neither the prelude's `layout` nor its drawing detection reaches. CeTZ 0.5 canvases (cetz, cetz-plot, alchemist) become `.ts-frame` SVG and are in the matrix.
48
+
49
+ ## Tests
50
+
51
+ The tests compile through `@typstream/engine-wasm` and compare with native `typst` 0.15.1 (paged text via pdf.js), so they need the engine built, `cargo` (for Typst's fonts) and `typst` on `PATH`. CI runs them in the `Engine (wasm)` job.
52
+
53
+ - `coverage.test.ts`: the coverage matrix (`__tests__/support/matrix.ts`). Every entry compiles with no "ignored during HTML export" warning, and its HTML holds every letter and digit of its paged text, except documented gaps. Entries marked `nativeDrops` are checked to lose something without the prelude. The HTML must not put a `<div>` inside a `<p>` or `<span>`.
54
+ - `corpus.test.ts`: every `corpus/edu` document, with each one's known gap (page numbers, headers, PDF text-layer artifacts) listed.
55
+ - `paged.test.ts`: every SVG page is byte-identical with and without the prelude.
56
+
57
+ Edit `typst/html-compat.typ` and `css/html-compat.css`, then `pnpm embed` (or `pnpm build`) to regenerate `src/assets.generated.ts`. A test fails when it's stale.
58
+
59
+ ## License
60
+
61
+ MIT
@@ -0,0 +1,558 @@
1
+ /*
2
+ * @typstream/html-compat: styles for the HTML that html-compat.typ emits, and Typst-like
3
+ * defaults for the elements Typst's HTML export writes natively (tables, math).
4
+ *
5
+ * Everything is scoped to the renderer's root element, `.typstream`, and wrapped in
6
+ * :where() so an app's own rules win without !important. Inline `style` attributes
7
+ * carry the per-element values from the document (sizes, colors, strokes).
8
+ */
9
+
10
+ /* Top-level place() is positioned against the document. */
11
+ :where(.typstream) {
12
+ position: relative;
13
+ }
14
+
15
+ /* Block elements inside a box are spans (a div would end the paragraph); this makes them blocks.
16
+ Rules below that set another display (grid, flex) come later and win. */
17
+ :where(.typstream .ts-b) {
18
+ display: block;
19
+ }
20
+
21
+ /* Typst's text, as the engine's layout profile (#129) sets it on pages: Libertinus Serif at 11pt, lines a constant
22
+ 1.3em apart, ragged right, no hyphenation. The fonts are the engine's own (dom's `registerTypstFonts`), registered
23
+ with the profile's line box as their ascent and descent (1.054em and 0.246em, no gap), so a line box 1.3em tall
24
+ has the baseline where Typst puts it, in any of them and any size. `line-height: normal` would be the same box,
25
+ but browsers round a font's ascent and descent to whole pixels: lines would drift from Typst's by a fraction of
26
+ a pixel each. Text the base font lacks falls back to the engine's script fonts, as in Typst. */
27
+ :where(.typstream) {
28
+ font-family:
29
+ typstream-libertinus-serif, typstream-noto-naskh-arabic, typstream-noto-serif-bengali,
30
+ typstream-noto-serif-devanagari, "Libertinus Serif", serif;
31
+ font-size: 11pt;
32
+ font-weight: 400;
33
+ line-height: 1.3;
34
+ text-align: start;
35
+ hyphens: manual;
36
+ font-kerning: normal;
37
+ font-synthesis: none;
38
+ text-rendering: geometricPrecision;
39
+ }
40
+ :where(.typstream) p {
41
+ margin-block: 0.56em;
42
+ }
43
+ /* `par.first-line-indent` without `all`: only a paragraph right after another is indented. */
44
+ :where(.typstream) p + p.ts-indent {
45
+ text-indent: var(--ts-indent);
46
+ }
47
+ :where(.typstream) :is(h1, h2, h3, h4, h5, h6) {
48
+ font-size: 1em;
49
+ font-weight: 700;
50
+ margin-block: var(--ts-above, 0.71em) var(--ts-below, 0.3em);
51
+ }
52
+ /* A block's own spacing (html-compat.typ's `.ts-sp`), reset for what it holds. */
53
+ :where(.typstream) :is(h1, h2, h3, h4, h5, h6) > *,
54
+ :where(.typstream .ts-sp) > * {
55
+ --ts-above: initial;
56
+ --ts-below: initial;
57
+ }
58
+ :where(.typstream .ts-sp) {
59
+ margin-block: var(--ts-above, 0.56em) var(--ts-below, 0.56em);
60
+ }
61
+ :where(.typstream) h2 {
62
+ font-size: 1.4em;
63
+ }
64
+ :where(.typstream) h3 {
65
+ font-size: 1.2em;
66
+ }
67
+ :where(.typstream) :is(code, pre, kbd, samp) {
68
+ font-family: typstream-dejavu-sans-mono, "DejaVu Sans Mono", monospace;
69
+ font-size: 0.8em;
70
+ }
71
+ :where(.typstream) pre {
72
+ margin-block: 0.56em;
73
+ white-space: pre;
74
+ }
75
+ /* Text with its own line box (html-compat.typ's `.ts-edges`): a strut as tall, its letters with none of theirs. */
76
+ :where(.typstream .ts-strut) {
77
+ display: inline-block;
78
+ width: 0;
79
+ }
80
+ :where(.typstream .ts-edges) .typstream-t {
81
+ line-height: 0;
82
+ }
83
+
84
+ /* Text decorations from the font, as Typst draws them. */
85
+ :where(.typstream) :is(u, s, [style*="text-decoration"]) {
86
+ text-decoration-thickness: from-font;
87
+ text-underline-offset: from-font;
88
+ }
89
+ /* Typst sets super- and subscripts with the font's own glyphs where it has them (Libertinus Serif does). */
90
+ :where(.typstream) :is(sup, sub) {
91
+ font-size: 1em;
92
+ vertical-align: baseline;
93
+ }
94
+ :where(.typstream) sup {
95
+ font-variant-position: super;
96
+ }
97
+ :where(.typstream) sub {
98
+ font-variant-position: sub;
99
+ }
100
+ /* A highlight covers the font's ascender to its descender: the line box less 0.16em at the top. */
101
+ :where(.typstream) mark {
102
+ background: linear-gradient(transparent 0.16em, var(--ts-mark, #fffd11a1) 0.16em);
103
+ color: inherit;
104
+ }
105
+ /* Links look like the text around them, as on pages. */
106
+ :where(.typstream) a {
107
+ color: inherit;
108
+ text-decoration: none;
109
+ }
110
+
111
+ /* Lists (html-compat.typ): a marker column as wide as the widest marker, the bodies after it; the items' gap is on
112
+ the list. */
113
+ :where(.typstream .ts-list) {
114
+ display: grid;
115
+ grid-template-columns: max-content minmax(0, 1fr);
116
+ align-items: baseline;
117
+ column-gap: 0.5em;
118
+ margin-block: 0.56em;
119
+ padding: 0;
120
+ list-style: none;
121
+ }
122
+ :where(.typstream .ts-list) > li {
123
+ display: contents;
124
+ }
125
+ :where(.typstream .ts-list) > li > .ts-marker {
126
+ justify-self: end;
127
+ }
128
+ :where(.typstream .ts-terms) {
129
+ display: grid;
130
+ margin-block: 0.56em;
131
+ }
132
+ :where(.typstream .ts-terms) :is(dt, dd) {
133
+ display: inline;
134
+ margin: 0;
135
+ }
136
+
137
+ /* Math: Typst's own math font. Without a math font, browsers can't stretch delimiters
138
+ and fall back to whatever has the math alphanumerics (PLAN.md §6.5). */
139
+ :where(.typstream) math {
140
+ /* biome-ignore lint/a11y/useGenericFontNames: `math` is the generic family (CSS Fonts 4). */
141
+ font-family: "New Computer Modern Math", math;
142
+ }
143
+ /* Equations (html-compat.typ) are Typst's drawings; their MathML is for screen readers only. A display equation is
144
+ centred on its line, its number at the end, both vertically centred. */
145
+ :where(.typstream .ts-sr) {
146
+ position: absolute;
147
+ width: 1px;
148
+ height: 1px;
149
+ overflow: hidden;
150
+ clip-path: inset(50%);
151
+ white-space: nowrap;
152
+ }
153
+ /* Typst writes `display: block` on a frame it finds at a block's start; an equation is in its line. */
154
+ :where(.typstream .ts-math) svg {
155
+ display: inline !important;
156
+ /* Its bottom edge is its baseline: the equation's depth below the text's (html-compat.typ measures it). */
157
+ vertical-align: var(--ts-depth, baseline);
158
+ }
159
+ /* An equation no deeper than the line: from the bottom of the text's line box, as Typst's line holds it, raised by
160
+ what the line is deeper (0.246em less its depth). From the baseline, it would stick out of a line as tall as it:
161
+ browsers round the font's ascent and descent to pixels, which moves the baseline in the line box. */
162
+ :where(.typstream .ts-math.ts-flat) svg {
163
+ vertical-align: text-bottom;
164
+ margin-block-end: var(--ts-lift);
165
+ }
166
+ :where(.typstream .ts-equation) {
167
+ display: grid;
168
+ grid-template-columns: 1fr auto 1fr;
169
+ align-items: center;
170
+ margin-block: 0.56em;
171
+ padding-top: 0.02px;
172
+ }
173
+ :where(.typstream .ts-equation-body) {
174
+ grid-column: 2;
175
+ }
176
+ :where(.typstream .ts-equation-number) {
177
+ grid-column: 3;
178
+ justify-self: end;
179
+ }
180
+
181
+ /* Strokes (html-compat.typ's `_box-props`): centred on the element's edge, half outside, taking no room, as Typst
182
+ draws them. Each side's border and width come from custom properties, reset on every stroked element so they
183
+ aren't inherited from one around it. */
184
+ :where(.typstream .ts-stroked) {
185
+ --ts-bt: none;
186
+ --ts-br: none;
187
+ --ts-bb: none;
188
+ --ts-bl: none;
189
+ --ts-wt: 0pt;
190
+ --ts-wr: 0pt;
191
+ --ts-wb: 0pt;
192
+ --ts-wl: 0pt;
193
+ position: relative;
194
+ }
195
+ :where(.typstream .ts-stroked)::before {
196
+ content: "";
197
+ position: absolute;
198
+ inset: calc(var(--ts-wt) / -2) calc(var(--ts-wr) / -2) calc(var(--ts-wb) / -2) calc(var(--ts-wl) / -2);
199
+ border-top: var(--ts-bt);
200
+ border-right: var(--ts-br);
201
+ border-bottom: var(--ts-bb);
202
+ border-left: var(--ts-bl);
203
+ border-radius: inherit;
204
+ pointer-events: none;
205
+ }
206
+
207
+ /* Figures: centred, their caption 0.65em under them (`figure.gap`), as on pages. */
208
+ :where(.typstream) figure {
209
+ margin: 0.56em 0;
210
+ text-align: center;
211
+ }
212
+ /* A figure with its own gap to its caption (html-compat.typ's `.ts-figure`, around it) is spaced by that. */
213
+ :where(.typstream .ts-figure) {
214
+ margin: 0.56em 0;
215
+ }
216
+ :where(.typstream .ts-figure) > figure {
217
+ margin: 0;
218
+ }
219
+ :where(.typstream) figure > :not(figcaption) {
220
+ margin-inline: auto;
221
+ }
222
+ :where(.typstream) figcaption {
223
+ margin-block-start: var(--ts-gap, 0.65em);
224
+ color: inherit;
225
+ }
226
+
227
+ /* An outline (html-compat.typ): a line per entry, its dots filling the room to the page number. */
228
+ :where(.typstream .ts-outline-entry) {
229
+ display: flex;
230
+ align-items: baseline;
231
+ }
232
+ :where(.typstream .ts-outline-entry) > a {
233
+ display: contents;
234
+ }
235
+ :where(.typstream .ts-outline-prefix) {
236
+ flex-shrink: 0;
237
+ }
238
+ :where(.typstream .ts-outline-fill) {
239
+ flex: 1 1 0;
240
+ min-width: 1em;
241
+ margin-inline: 0.25em;
242
+ overflow: hidden;
243
+ white-space: nowrap;
244
+ letter-spacing: 0.15em;
245
+ text-align: justify;
246
+ text-align-last: justify;
247
+ }
248
+
249
+ /* A block quote (html-compat.typ): padded by 1em, 2.4em above and 1.8em below, its attribution 0.9em under it. */
250
+ :where(.typstream .ts-quote) {
251
+ margin: 2.4em 1em 1.8em;
252
+ }
253
+ :where(.typstream .ts-attribution) {
254
+ margin-block-start: 0.9em;
255
+ text-align: end;
256
+ }
257
+
258
+ /* Layout containers. Blocks inside an align() move like its text. An align() is spaced by what it holds (its
259
+ margins collapse through it); a pad's content, or what a vertical align() holds, is spaced as one paragraph. */
260
+ :where(.typstream) :is(.ts-align-v, .ts-pad) {
261
+ margin-block: 0.56em;
262
+ }
263
+ :where(.typstream .ts-align-center) > * {
264
+ margin-inline: auto;
265
+ }
266
+ :where(.typstream) :is(.ts-align-end, .ts-align-right) > * {
267
+ margin-inline-start: auto;
268
+ }
269
+ :where(.typstream .ts-align-v) {
270
+ display: flex;
271
+ flex-direction: column;
272
+ height: 100%;
273
+ }
274
+ :where(.typstream .ts-grid) {
275
+ display: grid;
276
+ }
277
+ /* A table's rows hold its semantics (ARIA roles); its cells are the grid's items. Tables are spaced as blocks. */
278
+ :where(.typstream .ts-row) {
279
+ display: contents;
280
+ }
281
+ :where(.typstream) :is(.ts-grid, .ts-columns, .ts-stack) {
282
+ margin-block: 0.56em;
283
+ }
284
+ :where(.typstream .ts-cell) {
285
+ min-width: 0;
286
+ box-sizing: border-box;
287
+ }
288
+ :where(.typstream .ts-valign) {
289
+ display: flex;
290
+ flex-direction: column;
291
+ }
292
+ :where(.typstream .ts-stack) {
293
+ display: flex;
294
+ }
295
+ :where(.typstream .ts-columns) {
296
+ column-fill: balance;
297
+ }
298
+ :where(.typstream .ts-colbreak) {
299
+ break-after: column;
300
+ }
301
+ :where(.typstream .ts-pagebreak) {
302
+ break-after: page;
303
+ }
304
+ /* On the renderer's pages a page break is where a page ends, nothing to see. */
305
+ :where(.typstream .typstream-flow .ts-pagebreak) {
306
+ height: 0;
307
+ }
308
+
309
+ /* Boxes and blocks. Typst draws strokes centred on the edge and counts them in the size. */
310
+ :where(.typstream) :is(.ts-block, .ts-shape, .ts-box, .ts-place) {
311
+ box-sizing: border-box;
312
+ }
313
+ :where(.typstream) :is(.ts-block, .ts-shape) {
314
+ position: relative;
315
+ margin-block: var(--ts-above, 0.56em) var(--ts-below, 0.56em);
316
+ }
317
+ /* A block is as wide as its content unless it has a width. */
318
+ :where(.typstream .ts-block) {
319
+ width: fit-content;
320
+ }
321
+ :where(.typstream .ts-box) {
322
+ display: inline-block;
323
+ position: relative;
324
+ vertical-align: baseline;
325
+ }
326
+ /* box(width: 1fr) takes the rest of a line in Typst; HTML has no inline equivalent. */
327
+ :where(.typstream .ts-box.ts-fr) {
328
+ min-width: 4em;
329
+ overflow: hidden;
330
+ white-space: nowrap;
331
+ vertical-align: bottom;
332
+ }
333
+ /* repeat() fills its box in Typst: its copies stop at the box's edge (or the line's) instead of
334
+ spilling past it. Clipped by `clip-path`, which leaves its baseline on its text's, as Typst's;
335
+ `overflow` would move it to the box's bottom edge (WebKit's even with `overflow: clip`). */
336
+ :where(.typstream .ts-repeat) {
337
+ display: inline-block;
338
+ max-width: 100%;
339
+ clip-path: inset(0);
340
+ white-space: nowrap;
341
+ }
342
+ /* Empty shapes are drawings of a size; with content they size to it. */
343
+ :where(.typstream .ts-shape:empty) {
344
+ min-height: 1em;
345
+ }
346
+ :where(.typstream .ts-line) {
347
+ height: 0;
348
+ margin: 0.56em 0;
349
+ /* Not empty: Typst spaces it from both neighbours, where CSS would collapse its margins through it. */
350
+ padding-top: 0.02px;
351
+ }
352
+ :where(.typstream .ts-empty-line) {
353
+ display: block;
354
+ height: 0.658em;
355
+ }
356
+ :where(.typstream .ts-transform) {
357
+ width: fit-content;
358
+ }
359
+ :where(.typstream .ts-hide) {
360
+ visibility: hidden;
361
+ }
362
+
363
+ /* place(): absolute, against the nearest block, shape or box (or the document), inside its inset.
364
+ A container with an inset writes it as these properties (html-compat.typ's `_inset-vars`); every
365
+ element a .ts-place can be positioned against resets them, so they never come from further out.
366
+ A place resets them for what it holds, not for itself: its own offsets read them. */
367
+ :where(.typstream),
368
+ :where(.typstream) :is(.ts-block, .ts-shape, .ts-box, .ts-transform, .ts-place > *) {
369
+ --ts-inset-top: 0pt;
370
+ --ts-inset-right: 0pt;
371
+ --ts-inset-bottom: 0pt;
372
+ --ts-inset-left: 0pt;
373
+ }
374
+ :where(.typstream .ts-place) {
375
+ position: absolute;
376
+ }
377
+ /* Placed text with a vertical alignment: its box runs from 1.054em above the baseline to 0.246em
378
+ below it, the text edges of the engine's layout profile (#129), not over the whole line, so it
379
+ lands where paged output puts it: trimmed to the cap height and the baseline, then padded out.
380
+ Only text: an image or tall math in a trimmed line would stick out above it. */
381
+ :where(.typstream .ts-place-text) {
382
+ text-box: trim-both cap alphabetic;
383
+ padding-block: calc(1.054em - 1cap) 0.246em;
384
+ }
385
+ :where(.typstream .ts-place.ts-float) {
386
+ position: static;
387
+ }
388
+
389
+ /* Spacing. */
390
+ :where(.typstream .ts-h) {
391
+ display: inline-block;
392
+ }
393
+ /* h(1fr) takes the rest of the line: the renderer's pages give one in a run of inline content
394
+ (`.typstream-fill`) what its line leaves, and make a run with more a row of its pieces (`.typstream-row`,
395
+ `.typstream-seg`), the spacing growing between them. */
396
+ :where(.typstream .ts-hfill) {
397
+ flex-basis: 0;
398
+ }
399
+
400
+ /* Drawings (html.frame): inline SVG, scaled down to fit narrow containers. */
401
+ :where(.typstream .ts-frame) svg {
402
+ max-width: 100%;
403
+ height: auto;
404
+ }
405
+ /* Typst writes `display: block` on every picture's SVG (typst-html's make_block_level); one in a line (a box, a
406
+ polygon) sits on the baseline as on pages, and the line's alignment moves it. */
407
+ :where(.typstream) span.ts-frame:not(.ts-b) {
408
+ display: inline-block;
409
+ }
410
+ :where(.typstream) :is(div.ts-frame, span.ts-frame.ts-b) {
411
+ width: fit-content;
412
+ margin-block: 0.56em;
413
+ padding-top: 0.02px;
414
+ }
415
+
416
+ /* Last, to win over the margins above. */
417
+ /* Typst spaces blocks apart, not from the edges of what contains them. */
418
+ :where(.typstream)
419
+ :is(
420
+ .ts-cell,
421
+ .ts-valign-body,
422
+ .ts-align-body,
423
+ .ts-block,
424
+ .ts-shape,
425
+ .ts-box,
426
+ .ts-stack-item,
427
+ .ts-place,
428
+ .ts-align-v,
429
+ .ts-pad,
430
+ .ts-item-body,
431
+ .ts-columns,
432
+ .ts-marginal,
433
+ figure
434
+ )
435
+ > :is(:first-child, .ts-place:first-child + *),
436
+ :where(.typstream)
437
+ :is(
438
+ .ts-cell,
439
+ .ts-valign-body,
440
+ .ts-align-body,
441
+ .ts-block,
442
+ .ts-shape,
443
+ .ts-box,
444
+ .ts-stack-item,
445
+ .ts-place,
446
+ .ts-align-v,
447
+ .ts-pad,
448
+ .ts-item-body,
449
+ .ts-columns,
450
+ .ts-marginal,
451
+ figure
452
+ )
453
+ > .ts-align:first-child
454
+ > :first-child {
455
+ margin-block-start: 0;
456
+ }
457
+ :where(.typstream)
458
+ :is(
459
+ .ts-cell,
460
+ .ts-valign-body,
461
+ .ts-align-body,
462
+ .ts-block,
463
+ .ts-shape,
464
+ .ts-box,
465
+ .ts-stack-item,
466
+ .ts-place,
467
+ .ts-align-v,
468
+ .ts-pad,
469
+ .ts-item-body,
470
+ .ts-columns,
471
+ .ts-marginal,
472
+ figure
473
+ )
474
+ > :last-child,
475
+ :where(.typstream)
476
+ :is(
477
+ .ts-cell,
478
+ .ts-valign-body,
479
+ .ts-align-body,
480
+ .ts-block,
481
+ .ts-shape,
482
+ .ts-box,
483
+ .ts-stack-item,
484
+ .ts-place,
485
+ .ts-align-v,
486
+ .ts-pad,
487
+ .ts-item-body,
488
+ .ts-columns,
489
+ .ts-marginal,
490
+ figure
491
+ )
492
+ > .ts-align:last-child
493
+ > :last-child {
494
+ margin-block-end: 0;
495
+ }
496
+ /* A column starts at its top, as a page does. */
497
+ :where(.typstream) .ts-colbreak + * {
498
+ margin-block-start: 0;
499
+ }
500
+ :where(.typstream) :has(+ .ts-colbreak) {
501
+ margin-block-end: 0;
502
+ }
503
+ /* Spacing between blocks collapses as Typst's does (typst-layout's flow, distribute.rs): the larger of two gaps wins,
504
+ unless one is stronger. A block's own `above` and `below` (a heading's; html-compat.typ's `.ts-sp-a`, `.ts-sp-b`)
505
+ beat the paragraph spacing that blocks get by default (`auto`), even when smaller. */
506
+ :where(.typstream) :is(h1, h2, h3, h4, h5, h6, .ts-sp-b) + :not(h1, h2, h3, h4, h5, h6, .ts-sp-a) {
507
+ margin-block-start: 0;
508
+ }
509
+ :where(.typstream) :not(h1, h2, h3, h4, h5, h6, .ts-sp-b):has(+ :is(h1, h2, h3, h4, h5, h6, .ts-sp-a)) {
510
+ margin-block-end: 0;
511
+ }
512
+ /* A tight list right after a paragraph (or text) is attached to it: its items' gap, `par.leading`, 0 on pages,
513
+ replaces the spacing between them (typst-layout's LIST_RULE, an attached weak `v`). Not after a blank line, which
514
+ ends the paragraph first: the renderer marks the blocks that start there (`data-typstream-start`). */
515
+ :where(.typstream)
516
+ :is(p, span, strong, em, b, i, a, code, mark, sub, sup, u, s)
517
+ + .ts-tight:not([data-typstream-start]) {
518
+ margin-block-start: 0;
519
+ }
520
+ :where(.typstream) p:has(+ .ts-tight:not([data-typstream-start])) {
521
+ margin-block-end: 0;
522
+ }
523
+ /* In a container, html-compat.typ marks the blank line itself (`.ts-start`). */
524
+ :where(.typstream .ts-start) {
525
+ display: none;
526
+ }
527
+ /* Spacing (`v`): Typst adds it to the gap its neighbours' spacing collapses to (distribute.rs, keep_spacing): the
528
+ stronger of the spacing below the block before it and above the one after (a block's own, a heading's, beats
529
+ paragraph spacing), or, when they are alike, the larger (here the one before). Weak spacing replaces that gap, as
530
+ fractional spacing does (distribute.rs trims weak spacing next to it). */
531
+ :where(.typstream) .ts-v:not(.ts-v-both) + :not(h1, h2, h3, h4, h5, h6, .ts-sp-a),
532
+ :where(.typstream) :is(h1, h2, h3, h4, h5, h6, .ts-sp-b) + .ts-v + :not(h1, h2, h3, h4, h5, h6, .ts-sp-a) {
533
+ margin-block-start: 0;
534
+ }
535
+ /* Between two blocks' own spacings both stay here, and between two paragraph spacings where the renderer marks the
536
+ `v` (`.ts-v-both`): Typst's is the larger, which @typstream/dom's pages keep (`collapseSpacing`). */
537
+ :where(.typstream) :not(h1, h2, h3, h4, h5, h6, .ts-sp-b):has(+ .ts-v + :is(h1, h2, h3, h4, h5, h6, .ts-sp-a)) {
538
+ margin-block-end: 0;
539
+ }
540
+ :where(.typstream) :has(+ :is(.ts-v-weak, .ts-vfill)) {
541
+ margin-block-end: 0;
542
+ }
543
+ /* Text that isn't in a paragraph element (a tight list item's, the renderer's `.typstream-t`) is one in Typst, spaced
544
+ from spacing after it as a paragraph is. */
545
+ :where(.typstream)
546
+ :is(.typstream-t, span, strong, em, b, i, a, code, mark, sub, sup, u, s)
547
+ + .ts-v:not(.ts-v-weak, .ts-vfill, :last-child) {
548
+ margin-block-start: 0.56em;
549
+ }
550
+ /* Spacing that ends what holds it: the weak spacing before it goes, as at the end of any region. */
551
+ :where(.typstream) :has(+ .ts-v:last-child) {
552
+ margin-block-end: 0;
553
+ }
554
+ /* Typst drops the spacing above the first block of a page (the renderer's pages, below a break too). */
555
+ :where(.typstream .typstream-flow) > :first-child,
556
+ :where(.typstream .typstream-flow) > .ts-align:first-child > :first-child {
557
+ margin-block-start: 0;
558
+ }
@@ -0,0 +1,33 @@
1
+ import { TypstreamPlugin } from '@typstream/core';
2
+
3
+ /** Where the engine mounts the prelude in its virtual file system. */
4
+ declare const HTML_COMPAT_PATH = "/@typstream/html-compat.typ";
5
+ /** The prelude's Typst source. Mount it at {@link HTML_COMPAT_PATH}. */
6
+ declare const htmlCompatSource: string;
7
+ /**
8
+ * The stylesheet for the prelude's HTML and Typst-like defaults for tables and math. Its rules
9
+ * are scoped to an element with class `typstream` (the renderer's root) and have zero
10
+ * specificity (`:where`), so an app's own rules win.
11
+ *
12
+ * Math needs the "New Computer Modern Math" font (the engine's math font) registered as a web
13
+ * font; without a math font browsers can't stretch delimiters.
14
+ */
15
+ declare const htmlCompatCss: string;
16
+ /**
17
+ * Typst to put before a document: imports the prelude and applies it. It also replaces
18
+ * `layout`, which HTML export would drop, with one that reports a fixed text width in HTML.
19
+ * The rules act only on the HTML target, so the same preamble is safe for paged output.
20
+ */
21
+ declare const htmlCompatPreamble: string;
22
+ /**
23
+ * Warnings the prelude can't prevent, as message substrings. A `#set page(...)` rule has no
24
+ * HTML equivalent (no pages); its header, footer and page numbers aren't in the HTML output.
25
+ */
26
+ declare const KNOWN_HTML_WARNINGS: readonly string[];
27
+ /**
28
+ * html-compat as a typstream plugin: the prelude file, the preamble, what the sanitizer must let
29
+ * through, and the stylesheet.
30
+ */
31
+ declare const htmlCompat: TypstreamPlugin;
32
+
33
+ export { HTML_COMPAT_PATH, KNOWN_HTML_WARNINGS, htmlCompat, htmlCompatCss, htmlCompatPreamble, htmlCompatSource };