@quario/html 0.6.0 → 0.8.0

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/CHANGELOG.md CHANGED
@@ -1,23 +1,79 @@
1
- # Changelog
2
-
3
- All notable changes to @quario/html are documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
8
- ## [Unreleased]
9
-
10
- ## [0.6.0] - 2026-09-07
11
-
12
- ### Added
1
+ # @quario/html
2
+
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - **A picture states its own size, so a page stops reflowing around it.** Each
8
+ `<img>` now carries `width` and `height` attributes holding the image's
9
+ natural size in pixels, which lets a browser reserve the right aspect ratio
10
+ before the picture decodes. The `fit` mapping is unchanged and still decides
11
+ the rendered width, now paired with `height:auto` so the height follows the
12
+ width instead of staying pinned: an image capped narrower than its file
13
+ scales rather than squashing. Nothing an author writes changes, and no
14
+ existing report renders differently once its images have decoded.
15
+
16
+ ### Patch Changes
17
+
18
+ - **Every published README says where the documentation is.** Each package now
19
+ carries a Documentation section pointing at the reference, at the report schema
20
+ that normatively specifies what a report may declare, and at the package's own
21
+ API. The paragraphs that used to end on an unstated contract — the event
22
+ stream's field semantics, the style vocabulary, page columns, the Content
23
+ Security Policy a fragment with images needs, the formula mangling, and each
24
+ target's own contract — link the page that states it. Every link is an absolute
25
+ URL, so it resolves from the npm package page as readily as from an installed
26
+ copy.
27
+ - Updated dependencies
28
+ - quario@0.8.0
29
+
30
+ ## 0.7.0
31
+
32
+ ### Minor Changes
33
+
34
+ - **Styled runs render as spans.** A cell value written as a list of styled runs
35
+ emits one `<span style="…">` per styled run, carrying that run's whole
36
+ resolved inline style; unstyled runs stay bare, so a cell with no runs emits
37
+ exactly the markup it always did. Every interpolated value is escaped as
38
+ before, and the span's own CSS is escaped like any other attribute value.
39
+ - **A `format` inside a sentence now presents where the engine says it does.** A
40
+ cell mixing literal text and one interpolation under a `format` renders the
41
+ value plainly, as the spreadsheet and CSV targets already did; splitting the
42
+ value into styled runs is how a value inside a sentence is formatted.
43
+ - **The reference stylesheet no longer paints a look no other target has.**
44
+ `@quario/html/style.css` dropped the hairline under a table's header row, the
45
+ hairline and bold weight on its total rows, and the rule above the report
46
+ footer. A report that declares no borders now renders without them on screen,
47
+ which is what the PDF and the worksheet have always shown for the same
48
+ report — the difference was the stylesheet's alone, and it also surfaced as a
49
+ cell whose computed border colour failed keeping a black stroke here while
50
+ every other target drew nothing. The sheet now carries the band-role defaults,
51
+ the table's structure and the honor and pagination rules, and no look beyond
52
+ them. The classes are unchanged, so a host that wants the old look adds three
53
+ rules to its own sheet after the shipped one:
54
+
55
+ ```css
56
+ .q-table thead th {
57
+ border-bottom: 0.5pt solid #000;
58
+ }
59
+ .q-table tfoot td {
60
+ border-top: 0.5pt solid #000;
61
+ font-weight: bold;
62
+ }
63
+ .q-item.q-report-footer {
64
+ border-top: 1pt solid #000;
65
+ padding-top: 4pt;
66
+ }
67
+ ```
68
+
69
+ ## 0.6.0
70
+
71
+ ### Minor Changes
13
72
 
14
73
  - **A cell's own `currency` code is honoured** at the markup edge, ahead of the
15
74
  instance's default, so a listing whose rows arrive in different currencies
16
75
  presents each in its own. A code the engine could not accept leaves the cell
17
76
  as plain display text rather than presenting it in the instance's currency.
18
-
19
- ### Changed
20
-
21
77
  - **Numbers presented through `format` now show a fixed two fraction digits,
22
78
  matching every other target.** `format: "number"` on `1000` renders
23
79
  `1,000.00` where it rendered `1,000`, and `0.12345` renders `0.12` where it
@@ -27,22 +83,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
27
83
  are grouping separators and symbol placement. The digits come from the
28
84
  engine, so a cell reads the same here as it does in a workbook.
29
85
 
30
- ## [0.5.0] - 2026-09-05
86
+ ## 0.5.0
31
87
 
32
- ### Added
88
+ ### Minor Changes
33
89
 
34
90
  - **A spanning cell emits `colspan`**, ahead of its `style`; a span of one
35
91
  emits no attribute. The `<colgroup>` still names every column, so the
36
92
  geometry is the document's and only the cells merge — which means the header
37
93
  row can carry fewer `<th>` than there are columns.
38
-
39
94
  - **`valign`** renders as `vertical-align`, on a cell or on the row's `<tr>`,
40
95
  which the browser's own `inherit` on cells carries down. On a split slot the
41
96
  item itself becomes a grid with `align-content`, so the content moves inside
42
97
  the box the slot wrapper already stretches.
43
-
44
- ### Changed
45
-
46
98
  - **A row's box now reaches the cells** instead of being dropped. Padding and
47
99
  border on `detail.header`, `detail.row` or a total row's `style` are emitted
48
100
  inline on each `<th>`/`<td>`, where they also beat the reference
@@ -50,9 +102,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
50
102
  declarations and still never carries a box, which it could not honour.
51
103
  Fragments of documents that declared a box on a row gain borders and padding
52
104
  they previously rendered without.
53
-
54
- ### Fixed
55
-
56
105
  - **Unstyled table cells sit at the top of their row.** The reference
57
106
  stylesheet now pins `vertical-align: top` on `.q-table tr`, which the cells
58
107
  inherit.
@@ -60,9 +109,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
60
109
  painted them from the top, so the same report read differently in the two.
61
110
  A host stylesheet overrides it the ordinary way.
62
111
 
63
- ## [0.4.0] - 2026-09-03
112
+ ## 0.4.0
64
113
 
65
- ### Changed
114
+ ### Minor Changes
66
115
 
67
116
  - **A split slot's box now fills the split's height.** Before, a slot's
68
117
  background and border stopped at that slot's own content, leaving a gap under
@@ -70,48 +119,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
70
119
  `display:grid`, so the item inside it fills the row; a slot's item markup is
71
120
  unchanged. See the `@quario/layout` changelog for the rule and for what it
72
121
  costs a report that relied on the short box.
73
-
74
122
  - **A date string under `format: "date"` now presents as a date.** The
75
123
  engine's `format()` helper revives the two read forms, so a cell that
76
124
  rendered `2026-08-14` verbatim now renders the presented date. See the
77
125
  `quario` changelog for the forms and the timezone rule.
78
126
 
79
- ## [0.3.0] - 2026-09-02
127
+ ## 0.3.0
80
128
 
81
- ### Added
129
+ ### Minor Changes
82
130
 
83
131
  - **`format` stringifies at the markup edge.** The kind is presented through
84
132
  the engine's `format()` helper from the instance locale, then escaped like
85
133
  any other interpolated value.
86
-
87
134
  - **`page.margin` becomes `@page{margin:<n>pt}`.** Report-header `height`
88
135
  is unread: a CSS height would pin in the body box, not from the page top.
89
-
90
136
  - **`spaceBefore` / `spaceAfter` map to `margin-top` / `margin-bottom`.**
91
137
  Report and group containers are flex columns so adjacent authored gaps add
92
138
  rather than collapse.
93
-
94
139
  - **Padding and complete border sides map to inline CSS.**
95
140
  `box-sizing:border-box` rides the same attribute. A side that does not
96
141
  resolve all three names emits nothing, so a missing colour never becomes
97
142
  the browser's solid black. Header-row, row, and total-row boxes are
98
143
  withdrawn from `<tr>` — CSS does not box a table row — while other row
99
144
  styles still inherit.
100
-
101
- ### Changed
102
-
103
145
  - **`<tfoot>` holds every total row.** Extra rows are extra `<tr>`; the
104
146
  section still closes at `table-end`.
105
-
106
147
  - **A visible text item occupies a line, and a literal newline is a break.**
107
148
  Before, an empty item collapsed and a newline became a space. The honor
108
149
  rules now hang off `.q-item` and `.q-table th, td` in the reference
109
150
  stylesheet, the same pattern as `.q-break` — class is the hook, the rule
110
151
  is overridable. Empty table cells still collapse. Host CSS still owns
111
152
  leading; this is not `line-height: 1.4`.
112
-
113
- ### Fixed
114
-
115
153
  - **A declared `false` flag now emits its CSS off-value.** `bold: false` used
116
154
  to emit nothing, so the reference stylesheet's band-role `font-weight: bold`
117
155
  still won on screen while PDF rendered regular. Exact `false` now writes
@@ -119,9 +157,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
119
157
  the decorations), which beats the sheet. Omit and a non-boolean result still
120
158
  emit nothing.
121
159
 
122
- ## [0.2.0] - 2026-09-01
160
+ ## 0.2.0
123
161
 
124
- ### Added
162
+ ### Minor Changes
125
163
 
126
164
  - **`html({ fonts })` says what a family name means on your page.** A report
127
165
  declares the typeface it wants — `family: "Instrument Sans"`, or the
@@ -137,18 +175,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
137
175
  hashes `@font-face` names, for instance. The mapping resolves families a
138
176
  report declares; the face of text that declares none stays your
139
177
  stylesheet's, so it is still one CSS rule away.
140
-
141
178
  - **The report default rides a report root.** Every fragment is now wrapped in
142
179
  one `<div class="q-report">`, and a report that declares a `style` carries it
143
180
  there as an inline `font-family`/`font-size` — written once for the document
144
181
  rather than repeated on every item, with CSS inheritance doing the rest. The
145
182
  unlicensed marking stays outside the root, as the fragment's first element
146
183
  and the root's sibling, so no authored style can reach it.
147
-
148
184
  - **`uppercase` maps to `text-transform:uppercase`.** The markup carries the
149
185
  text exactly as authored and only its rendering is capitalised, so what a
150
186
  reader selects, copies, or hears from a screen reader is your own casing.
151
-
152
187
  - **Splits render as a flex row of sized slots.** A
153
188
  [split](https://getquario.com/docs) becomes
154
189
  `<div class="q-split q-<role>">` carrying inline `display:flex`, with each
@@ -165,9 +200,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
165
200
  One definition renders many times, so one path appears on every instance of
166
201
  it. Escaped like every generated attribute, and off by default — the
167
202
  attribute is weight a plain display pays for nothing.
168
-
169
- ### Changed
170
-
171
203
  - **A family name that could not be written as CSS now falls back instead of
172
204
  being rewritten.** A `family` carrying characters that cannot appear in a
173
205
  family name was stripped down to whatever survived and emitted anyway, which
@@ -176,35 +208,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
176
208
  text renders in the face above it: the report default, or your stylesheet's
177
209
  baseline. Ordinary names are unaffected. To resolve a name your page knows
178
210
  under a different one, map it with `html({ fonts })`.
179
-
180
211
  - **The fragment now has a root element.** `<div class="q-report">` wraps every
181
212
  render, whether or not a report declares a default. Host CSS that assumed
182
213
  items and tables were the fragment's top-level children needs a descendant
183
214
  selector; nothing else about the markup moved.
184
-
185
215
  - **The reference stylesheet's baseline face moved onto `.q-report`.** It was
186
216
  on `.q-item`, `.q-table` and `.q-unlicensed` — a direct rule, which would
187
217
  have beaten the report default an author writes on the document. The marking
188
218
  keeps its own rule, being outside the root. Same look, one rule up.
189
-
190
219
  - **A bare `Date` escapes to ISO 8601 UTC, the same on every machine.**
191
220
  Interpolated `Date` values rendered through `String(date)`, which bakes the
192
221
  host's timezone and locale into the markup. They now render as
193
222
  `toISOString()` text through the engine's shared display rule — escaped
194
223
  like every other value — so the same report produces the same HTML
195
224
  everywhere. Formatted dates keep coming from registered functions.
196
-
197
- ### Fixed
198
-
199
225
  - **The reference stylesheet sets a base font family.** A fragment dropped
200
226
  into a page with no font of its own inherited the browser default — a serif
201
227
  — where every other target rendered a grotesque. `@quario/html/style.css`
202
228
  now sets `sans-serif` on report text, as an ordinary rule any host overrides
203
229
  by source order.
204
230
 
205
- ## [0.1.0] - 2026-08-27
231
+ ## 0.1.0
206
232
 
207
- ### Added
233
+ ### Minor Changes
208
234
 
209
235
  - **HTML fragments for compiled reports.** Semantic tables, stable `q-*`
210
236
  classes, every interpolated value escaped. Pass `html()` to `report.render`
package/README.md CHANGED
@@ -105,19 +105,20 @@ host. Compilation stays synchronous; render-time failures reject with the same l
105
105
  These classes are the contract host CSS targets. They are stable, and changing them is a breaking
106
106
  change.
107
107
 
108
- | Emits | For |
109
- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
110
- | `<div class="q-report">` | The fragment's root, holding every band |
111
- | `<div class="q-item q-<role>">` | Every item. Roles: `q-report-header`, `q-empty`, `q-group-header`, `q-detail`, `q-group-footer`, `q-report-footer` |
112
- | `<div class="q-group" data-group="name">` | Each group instance, wrapping its header, nested content, and footer. Adds `q-break` when the group declares `break: "page"` or `reset: "page"` |
113
- | `<table class="q-table">` | Each table, with a real `colgroup`, `thead`, `tbody`, and a `tfoot` around the total rows the walk emits — none when `total` is absent or every row of it is hidden. A spanning cell carries `colspan` |
114
- | `<div class="q-item q-image q-<role>">` | Each image item, holding one `<img>` whose `src` is a base64 `data:` URI of the event's bytes; `fit` sets `max-width:100%` or `width:100%` on it, and the rendered `alt` is escaped |
115
- | `<div class="q-columns" style="column-count:<n>">` | The content between a node's own bands when it declares `columns`: on the root, the body between report header and footer; on a group, inside that instance's `q-group` |
116
- | `<div class="q-split q-<role>">` | Each split, carrying inline `display:flex`; its slots are `<div class="q-slot">` carrying inline `display:grid` and their share (a slot's `valign` as `align-content`), each holding the slot item's ordinary container |
108
+ | Emits | For |
109
+ | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
110
+ | `<div class="q-report">` | The fragment's root, holding every band |
111
+ | `<div class="q-item q-<role>">` | Every item. Roles: `q-report-header`, `q-empty`, `q-group-header`, `q-detail`, `q-group-footer`, `q-report-footer` |
112
+ | `<div class="q-group" data-group="name">` | Each group instance, wrapping its header, nested content, and footer. Adds `q-break` when the group declares `break: "page"` or `reset: "page"` |
113
+ | `<table class="q-table">` | Each table, with a real `colgroup`, `thead`, `tbody`, and a `tfoot` around the total rows the walk emits — none when `total` is absent or every row of it is hidden. A spanning cell carries `colspan` |
114
+ | `<div class="q-item q-image q-<role>">` | Each image item, holding one `<img>` whose `src` is a base64 `data:` URI of the event's bytes and whose `width`/`height` are the picture's natural size; `fit` sets `max-width:100%` or `width:100%` on it, paired with `height:auto`, and the rendered `alt` is escaped |
115
+ | `<div class="q-columns" style="column-count:<n>">` | The content between a node's own bands when it declares `columns`: on the root, the body between report header and footer; on a group, inside that instance's `q-group` |
116
+ | `<div class="q-split q-<role>">` | Each split, carrying inline `display:flex`; its slots are `<div class="q-slot">` carrying inline `display:grid` and their share (a slot's `valign` as `align-content`), each holding the slot item's ordinary container |
117
117
 
118
118
  A column `width` becomes an inline `width:<n>%` on its `<col>`. A row's `style` lands on its `<tr>`, except the box, which the engine has already resolved onto the cells (each `<td>` carries `box-sizing:border-box`). The classes above belong to this
119
119
  target. The schema never speaks in CSS. Displaying a fragment that contains images under a Content
120
- Security Policy needs `img-src data:`.
120
+ Security Policy needs `img-src data:` (see
121
+ [Trust and CSP](https://getquario.com/docs/diving-deeper/security/)).
121
122
 
122
123
  Style declarations map to inline CSS (`bold` → `font-weight:bold` or `font-weight:normal`, `size` → `font-size:<n>pt`,
123
124
  `family: "mono"` → `font-family:monospace`, …). Occupy-a-line and newline-as-break are **not** inline: they hang off
@@ -135,6 +136,10 @@ and maps `break: "page"` and `reset: "page"` to the `q-break` class. Page column
135
136
  a node declaring `columns` wraps the content between its own bands in the `q-columns` container
136
137
  above, and the browser flows it.
137
138
 
139
+ The full contract is [The HTML target](https://getquario.com/docs/diving-deeper/html-target/),
140
+ with each declaration's fate in the
141
+ [support matrix](https://getquario.com/docs/reference/support-matrix/).
142
+
138
143
  ## Unlicensed marking
139
144
 
140
145
  An unlicensed render opens the fragment with `<div class="q-unlicensed">` holding the wording
@@ -175,8 +180,11 @@ Host CSS targets these classes:
175
180
  ### The reference stylesheet
176
181
 
177
182
  The package ships the default look as a real stylesheet, `@quario/html/style.css`: the band-role
178
- weights and sizes, the table look, occupy and newline-as-break on `.q-item` and table cells, and
179
- the four pagination rules above. Link it, import it through a bundler, or read and inline it.
183
+ weights and sizes, the table's structure (full width, collapsed borders, cell padding, an
184
+ unstyled cell pinned to the top of its row), occupy and newline-as-break on `.q-item` and table
185
+ cells, and the four pagination rules above. It carries no look beyond that — no rule under the
186
+ header row, no bold totals, no stroke above the report footer — because no other target has one
187
+ either. Add them to your own sheet if you want them. Link it, import it through a bundler, or read and inline it.
180
188
  Plain Node cannot `import` a `.css` file, so on the server:
181
189
 
182
190
  ```js
@@ -195,6 +203,13 @@ any watermark treatment of `.q-unlicensed`, left as plain text here because `pos
195
203
  would paint over your whole page. `example/print.css` in the repository is a host's half showing
196
204
  both, and `example/print.js` is the complete pipeline.
197
205
 
206
+ ## Documentation
207
+
208
+ [The quario documentation](https://getquario.com/docs/) is the reference.
209
+ The [report schema](https://getquario.com/docs/reference/report-schema/) is the normative
210
+ specification of what a report may declare, and
211
+ [`@quario/html`](https://getquario.com/docs/reference/html/) is this package's own API.
212
+
198
213
  ## License
199
214
 
200
215
  Commercial software with readable source. Free, unlimited, watermarked evaluation; per-developer
package/lib/index.js CHANGED
@@ -9,7 +9,7 @@
9
9
  * never carries markup-escaped text.
10
10
  */
11
11
 
12
- import { display, format, isReportBand, text, walk } from "quario";
12
+ import { display, format, isReportBand, styledRuns, text, walk } from "quario";
13
13
 
14
14
  /** @type {Record<string, string>} */
15
15
  let ESC = {
@@ -27,8 +27,8 @@ let esc = (value) => display(value).replace(/[&<>"']/g, (char) => ESC[char]);
27
27
  // exceptions. The engine's own `text` join is the other one -- display text,
28
28
  // where a literal is words rather than markup -- and an attribute value takes
29
29
  // that one, escaped whole.
30
- /** @type {(tokens: any[], style?: any, intl?: any) => string} */
31
- let markup = (tokens, style, intl) => {
30
+ /** @type {(tokens: any[], style: any, intl: any) => string} */
31
+ let joined = (tokens, style, intl) => {
32
32
  let out = "";
33
33
  for (let token of tokens)
34
34
  out += "literal" in token ? token.literal : esc(shown(token.value, style, intl));
@@ -40,6 +40,29 @@ let shown = (value, style, intl) => {
40
40
  return text != null ? text : value;
41
41
  };
42
42
 
43
+ // One styled run: a `<span>` carrying that run's whole resolved inline style,
44
+ // rather than a difference against the cell's -- the engine already composed
45
+ // the two, and a delta would have this target merge them back. An unstyled run
46
+ // stays bare, so a cell with no authored runs emits exactly the markup it
47
+ // always did. The span's own CSS is `esc()`d like any other attribute value.
48
+ /** @type {(styled: { style: any, tokens: any[] }, style: any, intl: any, map: any) => string} */
49
+ let piece = (styled, style, intl, map) => {
50
+ let body = joined(styled.tokens, styled.style ?? style, intl);
51
+ if (!styled.style) return body;
52
+ let inline = css(styled.style, map);
53
+ return inline ? '<span style="' + esc(inline) + '">' + body + "</span>" : body;
54
+ };
55
+
56
+ // A cell's markup: its styled runs in order. The grouping is the engine's
57
+ // `styledRuns`, so this target cannot drift from the others (SCHEMA.md, "Event
58
+ // stream").
59
+ /** @type {(map: any) => (tokens: any[], style?: any, intl?: any) => string} */
60
+ let cellMarkup = (map) => (tokens, style, intl) => {
61
+ let out = "";
62
+ for (let styled of styledRuns(tokens)) out += piece(styled, style, intl, map);
63
+ return out;
64
+ };
65
+
43
66
  // base64 for an image's `data:` URI, without Buffer: this package runs in Node
44
67
  // and in ES2024 browsers off the same `lib/`, and `btoa` is a standard global
45
68
  // in both. The walk is chunked because `fromCharCode(...bytes)` spreads one
@@ -249,15 +272,20 @@ let css = (style, map) => {
249
272
  // columns and splits are placement, not the visual defaults this target
250
273
  // refuses to supply, so they ride the same escaped attribute path as
251
274
  // everything else rather than a second one (CLAUDE.md, the markup edge).
252
- /** @type {(fonts: any) => (event: { style?: any }, own?: string) => string} */
275
+ /** @type {(fonts: any) => { attr: (event: { style?: any }, own?: string) => string, map: Record<string, string> }} */
253
276
  let styling = (fonts) => {
254
277
  // The spread is the precedence the option promises: a host mapping is
255
278
  // consulted before the three generics, and may replace one. `mapping`
256
- // lowercased its keys to match the ones already here.
279
+ // lowercased its keys to match the ones already here. The table is handed
280
+ // back beside the attribute writer, because a styled run's span resolves a
281
+ // `family` against the same one.
257
282
  let map = { ...FAMILY, ...mapping(fonts) };
258
- return ({ style }, own = "") => {
259
- let out = [own, style && css(style, map)].filter(Boolean).join(";");
260
- return out ? ' style="' + esc(out) + '"' : "";
283
+ return {
284
+ map,
285
+ attr: ({ style }, own = "") => {
286
+ let out = [own, style && css(style, map)].filter(Boolean).join(";");
287
+ return out ? ' style="' + esc(out) + '"' : "";
288
+ },
261
289
  };
262
290
  };
263
291
 
@@ -302,8 +330,8 @@ let justified = (own, style) =>
302
330
  // event to "" through the same function rather than re-deciding per call site.
303
331
  /** @type {(event: { path?: string }) => string} */
304
332
  let pathAttr = ({ path }) => (path ? ' data-q-path="' + esc(path) + '"' : "");
305
- /** @type {(list: any[], tag: string, attrs: (event: any) => string, intl: any) => string} */
306
- let cells = (list, tag, attrs, intl) =>
333
+ /** @type {(list: any[], tag: string, attrs: (event: any) => string, intl: any, markup: any) => string} */
334
+ let cells = (list, tag, attrs, intl, markup) =>
307
335
  list
308
336
  .map(
309
337
  (cell) =>
@@ -342,7 +370,9 @@ let cells = (list, tag, attrs, intl) =>
342
370
  export function html(options) {
343
371
  let mark = options?.paths ? pathAttr : () => "";
344
372
  // The factory's verdict, like `mark`: the mapping is validated once, here.
345
- let styleAttr = styling(options?.fonts);
373
+ let { attr: styleAttr, map: fontMap } = styling(options?.fonts);
374
+ // A cell's markup, bound to this target's font table once.
375
+ let markup = cellMarkup(fontMap);
346
376
  // The pair every cell carries, bound once rather than threaded as two.
347
377
  /** @type {(cell: any) => string} */
348
378
  // A span covers several columns; `colspan` is what that is on this surface.
@@ -357,7 +387,7 @@ export function html(options) {
357
387
  // the cells, where it arrives as ordinary inline CSS.
358
388
  /** @type {(event: any, list: any[], tag: string, intl: any) => string} */
359
389
  let tr = (event, list, tag, intl) =>
360
- "<tr" + styleAttr(event) + ">" + cells(list, tag, attrs, intl) + "</tr>";
390
+ "<tr" + styleAttr(event) + ">" + cells(list, tag, attrs, intl, markup) + "</tr>";
361
391
  return {
362
392
  name: "html",
363
393
  compile: (stream) => async (data) => {
@@ -504,6 +534,14 @@ export function html(options) {
504
534
  // one, and escaped when it is encoded rather than per occurrence. `alt` is report data, so it is the display-text join escaped,
505
535
  // never the markup-passing join a cell body gets; an image without one
506
536
  // carries `alt=""`, the decorative-image convention.
537
+ //
538
+ // `width`/`height` are the event's natural size, which the engine has
539
+ // already read and vouched for, so a picture reserves its aspect ratio
540
+ // before it decodes rather than reflowing the page around it. The pair
541
+ // is a presentational hint, not a ratio on its own, which is why the
542
+ // `fit` style carries `height:auto`: without it the stated height is a
543
+ // used height and a picture capped narrower than its file renders
544
+ // squashed. SCHEMA.md, "The HTML target", states that normatively.
507
545
  image: (event) => {
508
546
  let slot = placed(event);
509
547
  let source = sources.get(event.bytes);
@@ -521,10 +559,15 @@ export function html(options) {
521
559
  mark(event) +
522
560
  '><img src="' +
523
561
  source +
562
+ '" width="' +
563
+ esc(event.width) +
564
+ '" height="' +
565
+ esc(event.height) +
524
566
  '" alt="' +
525
567
  esc(text(event.alt || [])) +
526
- // Both keep the aspect ratio, since neither names a height.
527
- (event.fit === "width" ? '" style="width:100%">' : '" style="max-width:100%">') +
568
+ '" style="' +
569
+ (event.fit === "width" ? "" : "max-") +
570
+ 'width:100%;height:auto">' +
528
571
  "</div>",
529
572
  );
530
573
  },
package/lib/style.css CHANGED
@@ -73,15 +73,6 @@
73
73
  white-space: pre-line;
74
74
  }
75
75
 
76
- .q-table thead th {
77
- border-bottom: 0.5pt solid #000;
78
- }
79
-
80
- .q-table tfoot td {
81
- border-top: 0.5pt solid #000;
82
- font-weight: bold;
83
- }
84
-
85
76
  .q-item.q-report-header {
86
77
  font-size: 14pt;
87
78
  font-weight: bold;
@@ -98,8 +89,6 @@
98
89
 
99
90
  .q-item.q-report-footer {
100
91
  margin-top: 10pt;
101
- border-top: 1pt solid #000;
102
- padding-top: 4pt;
103
92
  }
104
93
 
105
94
  /* A columned container is a fragmentation context on screen, not only in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quario/html",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "The HTML render target for quario — in the makings, not yet released",
5
5
  "homepage": "https://getquario.com",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -39,12 +39,12 @@
39
39
  "devDependencies": {
40
40
  "@arethetypeswrong/cli": "^0.18.3",
41
41
  "@size-limit/preset-small-lib": "^13.0.3",
42
- "quario": "^0.6.0",
42
+ "quario": "^0.8.0",
43
43
  "size-limit": "^13.0.3",
44
44
  "typescript": "^7.0.2"
45
45
  },
46
46
  "peerDependencies": {
47
- "quario": "^0.6.0"
47
+ "quario": "^0.8.0"
48
48
  },
49
49
  "size-limit": [
50
50
  {