@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 +21 -0
- package/README.md +60 -2
- package/css/html-compat.css +558 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +37 -0
- package/package.json +52 -3
- package/typst/html-compat.typ +1155 -0
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
|
-
#
|
|
1
|
+
# @typstream/html-compat
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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 };
|