@quario/html 0.7.0 → 0.9.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,29 +1,90 @@
1
- # Changelog
1
+ # @quario/html
2
2
 
3
- All notable changes to @quario/html are documented in this file.
3
+ ## 0.9.0
4
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).
5
+ ### Minor Changes
7
6
 
8
- ## [Unreleased]
7
+ - **Every target factory now refuses an option it does not understand.** An unknown key, a key with
8
+ a value of the wrong type, or an `options` that is not an object throws a `TypeError` at the
9
+ factory call, naming the option path — `options.meta.title`, `options.page.size`. `@quario/docx`
10
+ already behaved this way. `@quario/pdf`, `@quario/html`, `@quario/xlsx` and `@quario/layout` read
11
+ the keys they knew and ignored the rest, so a typo cost you the option you meant to set with no
12
+ signal at any point. `csv()` takes no options, and it refuses an argument rather than discarding
13
+ one.
9
14
 
10
- ## [0.7.0] - 2026-09-07
15
+ The check itself is one thing, so the engine now exports it: `hostOptions` closes a key set and
16
+ `hostMeta` validates the `{ title, author, subject }` contract the document targets share. A
17
+ fourth document property is one edit rather than three.
11
18
 
12
- ### Added
19
+ **What this changes for you.** One options object spread across several targets stops working if
20
+ any target does not know one of its keys — `const o = { page, fonts, meta }` passed to `pdf(o)`,
21
+ `html(o)` and `xlsx(o)` is three different key sets. An object holding only what its consumers
22
+ share is unaffected, so `{ page, fonts }` into both `pdf()` and `layout()` still works.
23
+ TypeScript does not warn about this: excess-property checking fires on an object literal and not
24
+ on a variable, so a bag held in a `const` compiles clean and throws when you call the factory.
25
+
26
+ Nullish is absence everywhere, at both levels, so `{ meta: config.meta ?? null }` and
27
+ `{ meta: { title: config.title } }` over a config that carries neither are both fine.
28
+ `html({ paths })` now takes `true` or `false` rather than anything truthy, so `paths: "no"` throws
29
+ instead of turning path stamping on. A `fonts` mapping given as an array is refused by
30
+ `@quario/html` and `@quario/layout` rather than read as families named `0`, `1`, `2`.
31
+
32
+ Every host-option failure is now a `TypeError` rather than a plain `Error`. A `catch` that tests
33
+ `instanceof Error` is unaffected. One that compares the constructor is not.
34
+
35
+ `@quario/docx` is relaxed in three places, each of them now accepting what it refused: `meta: null`
36
+ and `page: null` are absence rather than errors, a property written as `meta: { title: undefined }`
37
+ is a property you did not write rather than one of the wrong type, and an inherited enumerable key
38
+ is no longer reported as an option you wrote.
39
+
40
+ `@quario/pdf`, `@quario/xlsx` and `@quario/docx` now copy `meta` at the factory call. Mutating the
41
+ object you passed no longer changes what a configured target writes.
42
+
43
+ ### Patch Changes
44
+
45
+ - Updated dependencies
46
+ - quario@0.9.0
47
+
48
+ ## 0.8.0
49
+
50
+ ### Minor Changes
51
+
52
+ - **A picture states its own size, so a page stops reflowing around it.** Each
53
+ `<img>` now carries `width` and `height` attributes holding the image's
54
+ natural size in pixels, which lets a browser reserve the right aspect ratio
55
+ before the picture decodes. The `fit` mapping is unchanged and still decides
56
+ the rendered width, now paired with `height:auto` so the height follows the
57
+ width instead of staying pinned: an image capped narrower than its file
58
+ scales rather than squashing. Nothing an author writes changes, and no
59
+ existing report renders differently once its images have decoded.
60
+
61
+ ### Patch Changes
62
+
63
+ - **Every published README says where the documentation is.** Each package now
64
+ carries a Documentation section pointing at the reference, at the report schema
65
+ that normatively specifies what a report may declare, and at the package's own
66
+ API. The paragraphs that used to end on an unstated contract — the event
67
+ stream's field semantics, the style vocabulary, page columns, the Content
68
+ Security Policy a fragment with images needs, the formula mangling, and each
69
+ target's own contract — link the page that states it. Every link is an absolute
70
+ URL, so it resolves from the npm package page as readily as from an installed
71
+ copy.
72
+ - Updated dependencies
73
+ - quario@0.8.0
74
+
75
+ ## 0.7.0
76
+
77
+ ### Minor Changes
13
78
 
14
79
  - **Styled runs render as spans.** A cell value written as a list of styled runs
15
80
  emits one `<span style="…">` per styled run, carrying that run's whole
16
81
  resolved inline style; unstyled runs stay bare, so a cell with no runs emits
17
82
  exactly the markup it always did. Every interpolated value is escaped as
18
83
  before, and the span's own CSS is escaped like any other attribute value.
19
-
20
- ### Changed
21
-
22
84
  - **A `format` inside a sentence now presents where the engine says it does.** A
23
85
  cell mixing literal text and one interpolation under a `format` renders the
24
86
  value plainly, as the spreadsheet and CSV targets already did; splitting the
25
87
  value into styled runs is how a value inside a sentence is formatted.
26
-
27
88
  - **The reference stylesheet no longer paints a look no other target has.**
28
89
  `@quario/html/style.css` dropped the hairline under a table's header row, the
29
90
  hairline and bold weight on its total rows, and the rule above the report
@@ -50,17 +111,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
50
111
  }
51
112
  ```
52
113
 
53
- ## [0.6.0] - 2026-09-07
114
+ ## 0.6.0
54
115
 
55
- ### Added
116
+ ### Minor Changes
56
117
 
57
118
  - **A cell's own `currency` code is honoured** at the markup edge, ahead of the
58
119
  instance's default, so a listing whose rows arrive in different currencies
59
120
  presents each in its own. A code the engine could not accept leaves the cell
60
121
  as plain display text rather than presenting it in the instance's currency.
61
-
62
- ### Changed
63
-
64
122
  - **Numbers presented through `format` now show a fixed two fraction digits,
65
123
  matching every other target.** `format: "number"` on `1000` renders
66
124
  `1,000.00` where it rendered `1,000`, and `0.12345` renders `0.12` where it
@@ -70,22 +128,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
70
128
  are grouping separators and symbol placement. The digits come from the
71
129
  engine, so a cell reads the same here as it does in a workbook.
72
130
 
73
- ## [0.5.0] - 2026-09-05
131
+ ## 0.5.0
74
132
 
75
- ### Added
133
+ ### Minor Changes
76
134
 
77
135
  - **A spanning cell emits `colspan`**, ahead of its `style`; a span of one
78
136
  emits no attribute. The `<colgroup>` still names every column, so the
79
137
  geometry is the document's and only the cells merge — which means the header
80
138
  row can carry fewer `<th>` than there are columns.
81
-
82
139
  - **`valign`** renders as `vertical-align`, on a cell or on the row's `<tr>`,
83
140
  which the browser's own `inherit` on cells carries down. On a split slot the
84
141
  item itself becomes a grid with `align-content`, so the content moves inside
85
142
  the box the slot wrapper already stretches.
86
-
87
- ### Changed
88
-
89
143
  - **A row's box now reaches the cells** instead of being dropped. Padding and
90
144
  border on `detail.header`, `detail.row` or a total row's `style` are emitted
91
145
  inline on each `<th>`/`<td>`, where they also beat the reference
@@ -93,9 +147,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
93
147
  declarations and still never carries a box, which it could not honour.
94
148
  Fragments of documents that declared a box on a row gain borders and padding
95
149
  they previously rendered without.
96
-
97
- ### Fixed
98
-
99
150
  - **Unstyled table cells sit at the top of their row.** The reference
100
151
  stylesheet now pins `vertical-align: top` on `.q-table tr`, which the cells
101
152
  inherit.
@@ -103,9 +154,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
103
154
  painted them from the top, so the same report read differently in the two.
104
155
  A host stylesheet overrides it the ordinary way.
105
156
 
106
- ## [0.4.0] - 2026-09-03
157
+ ## 0.4.0
107
158
 
108
- ### Changed
159
+ ### Minor Changes
109
160
 
110
161
  - **A split slot's box now fills the split's height.** Before, a slot's
111
162
  background and border stopped at that slot's own content, leaving a gap under
@@ -113,48 +164,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
113
164
  `display:grid`, so the item inside it fills the row; a slot's item markup is
114
165
  unchanged. See the `@quario/layout` changelog for the rule and for what it
115
166
  costs a report that relied on the short box.
116
-
117
167
  - **A date string under `format: "date"` now presents as a date.** The
118
168
  engine's `format()` helper revives the two read forms, so a cell that
119
169
  rendered `2026-08-14` verbatim now renders the presented date. See the
120
170
  `quario` changelog for the forms and the timezone rule.
121
171
 
122
- ## [0.3.0] - 2026-09-02
172
+ ## 0.3.0
123
173
 
124
- ### Added
174
+ ### Minor Changes
125
175
 
126
176
  - **`format` stringifies at the markup edge.** The kind is presented through
127
177
  the engine's `format()` helper from the instance locale, then escaped like
128
178
  any other interpolated value.
129
-
130
179
  - **`page.margin` becomes `@page{margin:<n>pt}`.** Report-header `height`
131
180
  is unread: a CSS height would pin in the body box, not from the page top.
132
-
133
181
  - **`spaceBefore` / `spaceAfter` map to `margin-top` / `margin-bottom`.**
134
182
  Report and group containers are flex columns so adjacent authored gaps add
135
183
  rather than collapse.
136
-
137
184
  - **Padding and complete border sides map to inline CSS.**
138
185
  `box-sizing:border-box` rides the same attribute. A side that does not
139
186
  resolve all three names emits nothing, so a missing colour never becomes
140
187
  the browser's solid black. Header-row, row, and total-row boxes are
141
188
  withdrawn from `<tr>` — CSS does not box a table row — while other row
142
189
  styles still inherit.
143
-
144
- ### Changed
145
-
146
190
  - **`<tfoot>` holds every total row.** Extra rows are extra `<tr>`; the
147
191
  section still closes at `table-end`.
148
-
149
192
  - **A visible text item occupies a line, and a literal newline is a break.**
150
193
  Before, an empty item collapsed and a newline became a space. The honor
151
194
  rules now hang off `.q-item` and `.q-table th, td` in the reference
152
195
  stylesheet, the same pattern as `.q-break` — class is the hook, the rule
153
196
  is overridable. Empty table cells still collapse. Host CSS still owns
154
197
  leading; this is not `line-height: 1.4`.
155
-
156
- ### Fixed
157
-
158
198
  - **A declared `false` flag now emits its CSS off-value.** `bold: false` used
159
199
  to emit nothing, so the reference stylesheet's band-role `font-weight: bold`
160
200
  still won on screen while PDF rendered regular. Exact `false` now writes
@@ -162,9 +202,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
162
202
  the decorations), which beats the sheet. Omit and a non-boolean result still
163
203
  emit nothing.
164
204
 
165
- ## [0.2.0] - 2026-09-01
205
+ ## 0.2.0
166
206
 
167
- ### Added
207
+ ### Minor Changes
168
208
 
169
209
  - **`html({ fonts })` says what a family name means on your page.** A report
170
210
  declares the typeface it wants — `family: "Instrument Sans"`, or the
@@ -180,18 +220,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
180
220
  hashes `@font-face` names, for instance. The mapping resolves families a
181
221
  report declares; the face of text that declares none stays your
182
222
  stylesheet's, so it is still one CSS rule away.
183
-
184
223
  - **The report default rides a report root.** Every fragment is now wrapped in
185
224
  one `<div class="q-report">`, and a report that declares a `style` carries it
186
225
  there as an inline `font-family`/`font-size` — written once for the document
187
226
  rather than repeated on every item, with CSS inheritance doing the rest. The
188
227
  unlicensed marking stays outside the root, as the fragment's first element
189
228
  and the root's sibling, so no authored style can reach it.
190
-
191
229
  - **`uppercase` maps to `text-transform:uppercase`.** The markup carries the
192
230
  text exactly as authored and only its rendering is capitalised, so what a
193
231
  reader selects, copies, or hears from a screen reader is your own casing.
194
-
195
232
  - **Splits render as a flex row of sized slots.** A
196
233
  [split](https://getquario.com/docs) becomes
197
234
  `<div class="q-split q-<role>">` carrying inline `display:flex`, with each
@@ -208,9 +245,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
208
245
  One definition renders many times, so one path appears on every instance of
209
246
  it. Escaped like every generated attribute, and off by default — the
210
247
  attribute is weight a plain display pays for nothing.
211
-
212
- ### Changed
213
-
214
248
  - **A family name that could not be written as CSS now falls back instead of
215
249
  being rewritten.** A `family` carrying characters that cannot appear in a
216
250
  family name was stripped down to whatever survived and emitted anyway, which
@@ -219,35 +253,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
219
253
  text renders in the face above it: the report default, or your stylesheet's
220
254
  baseline. Ordinary names are unaffected. To resolve a name your page knows
221
255
  under a different one, map it with `html({ fonts })`.
222
-
223
256
  - **The fragment now has a root element.** `<div class="q-report">` wraps every
224
257
  render, whether or not a report declares a default. Host CSS that assumed
225
258
  items and tables were the fragment's top-level children needs a descendant
226
259
  selector; nothing else about the markup moved.
227
-
228
260
  - **The reference stylesheet's baseline face moved onto `.q-report`.** It was
229
261
  on `.q-item`, `.q-table` and `.q-unlicensed` — a direct rule, which would
230
262
  have beaten the report default an author writes on the document. The marking
231
263
  keeps its own rule, being outside the root. Same look, one rule up.
232
-
233
264
  - **A bare `Date` escapes to ISO 8601 UTC, the same on every machine.**
234
265
  Interpolated `Date` values rendered through `String(date)`, which bakes the
235
266
  host's timezone and locale into the markup. They now render as
236
267
  `toISOString()` text through the engine's shared display rule — escaped
237
268
  like every other value — so the same report produces the same HTML
238
269
  everywhere. Formatted dates keep coming from registered functions.
239
-
240
- ### Fixed
241
-
242
270
  - **The reference stylesheet sets a base font family.** A fragment dropped
243
271
  into a page with no font of its own inherited the browser default — a serif
244
272
  — where every other target rendered a grotesque. `@quario/html/style.css`
245
273
  now sets `sans-serif` on report text, as an ordinary rule any host overrides
246
274
  by source order.
247
275
 
248
- ## [0.1.0] - 2026-08-27
276
+ ## 0.1.0
249
277
 
250
- ### Added
278
+ ### Minor Changes
251
279
 
252
280
  - **HTML fragments for compiled reports.** Semantic tables, stable `q-*`
253
281
  classes, every interpolated value escaped. Pass `html()` to `report.render`
package/README.md CHANGED
@@ -69,7 +69,8 @@ in your own page shell and stylesheet.
69
69
 
70
70
  ### `html(options?)`
71
71
 
72
- The target factory returns the target you pass to `render`.
72
+ The target factory takes this target's host options, validates them at the call, and returns
73
+ the target you pass to `render`. The factory refuses an option it does not know. It throws a `TypeError` at the call for an unknown key. It throws one also for a key with a value of the wrong type.
73
74
  `report()` compiles once and `report.render(html(), data)` resolves the fragment. Compile at
74
75
  startup and render per request. Definition problems throw at `report()`, at compile time.
75
76
 
@@ -77,8 +78,8 @@ Two options. `{ paths: true }` stamps `data-q-path="<schema path>"` on each elem
77
78
  carries one (items, images, group containers, the table and its cells), mapping rendered output
78
79
  back to the definition behind it. Off by default. `{ fonts }` maps a declared `family` name to the
79
80
  CSS `font-family` value it should emit — a custom property, a font stack, a quoted name —
80
- consulted before the three built-in generics; names match case-insensitively, and a value holding
81
- `;` or `}` is rejected when the target is constructed:
81
+ consulted before the three built-in generics. Names match case-insensitively, and the factory
82
+ rejects a value holding `;` or `}` when it constructs the target:
82
83
 
83
84
  ```js
84
85
  html({ fonts: { "Instrument Sans": "var(--font-instrument-sans)", mono: "var(--font-ibm-plex-mono)" } });
@@ -95,61 +96,66 @@ report.stream(data); // the raw event generator, if you want events instead
95
96
  ```
96
97
 
97
98
  Engine-level options (`query` budgets, the license key) live on the instance
98
- (`quario({ query, license })`); see the [engine README](https://www.npmjs.com/package/quario).
99
+ (`quario({ query, license })`). See the [engine README](https://www.npmjs.com/package/quario).
99
100
 
100
- Rendering is asynchronous and hands the loop back between batches, so a large report never blocks the
101
- host. Compilation stays synchronous; render-time failures reject with the same located errors.
101
+ Rendering is asynchronous and returns the loop between batches, so a large report never blocks the
102
+ host. Compilation stays synchronous. Render-time failures reject with the same located errors.
102
103
 
103
104
  ## Output contract
104
105
 
105
106
  These classes are the contract host CSS targets. They are stable, and changing them is a breaking
106
107
  change.
107
108
 
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 |
109
+ | Emits | For |
110
+ | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
111
+ | `<div class="q-report">` | The fragment's root, holding every band |
112
+ | `<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` |
113
+ | `<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"` |
114
+ | `<table class="q-table">` | Each table, with a real `colgroup`, `thead`, `tbody`, and a `tfoot` around the total rows the walk emits. No `tfoot` when `total` is absent or every row of it is hidden. A spanning cell carries `colspan` |
115
+ | `<div class="q-item q-image q-<role>">` | Each image item, holding one `<img>`. Its `src` is a base64 `data:` URI of the event's bytes, and its `width`/`height` are the picture's natural size. `fit` sets `max-width:100%` or `width:100%` on it, paired with `height:auto`. The target escapes the rendered `alt` |
116
+ | `<div class="q-columns" style="column-count:<n>">` | The content between a node's own bands when it declares `columns`. On the root, that is the body between report header and footer. On a group, it is inside that instance's `q-group` |
117
+ | `<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
118
 
118
119
  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
120
  target. The schema never speaks in CSS. Displaying a fragment that contains images under a Content
120
- Security Policy needs `img-src data:`.
121
+ Security Policy needs `img-src data:` (see
122
+ [Trust and CSP](https://getquario.com/docs/diving-deeper/security/)).
121
123
 
122
124
  Style declarations map to inline CSS (`bold` → `font-weight:bold` or `font-weight:normal`, `size` → `font-size:<n>pt`,
123
125
  `family: "mono"` → `font-family:monospace`, …). Occupy-a-line and newline-as-break are **not** inline: they hang off
124
126
  `.q-item` and `.q-table th, td` in [`@quario/html/style.css`](#the-reference-stylesheet), the same way `.q-break`
125
- honors `break: "page"`. The class is the hook; the rule is a reference default a host overrides on source order.
126
- A fragment without that sheet still carries the classes and the text, including newlines; it does not occupy or
127
+ honors `break: "page"`. The class is the hook. The rule is a reference default a host overrides on source order.
128
+ A fragment without that sheet still carries the classes and the text, including newlines. It does not occupy or
127
129
  break until some stylesheet says so. This target supplies no defaults in the markup: no weight or size per band
128
130
  role, no leading, no padding, no spacing between bands, no borders. An inline `style` attribute would beat yours
129
- in the cascade and force `!important` on you; the defaults and the honor rules ship as an ordinary stylesheet
131
+ in the cascade and force `!important` on you. The defaults and the honor rules ship as an ordinary stylesheet
130
132
  instead. The PDF and XLSX targets carry theirs built in, because their consumers have no stylesheet. An empty
131
133
  row set still emits the table, its header, and an empty `<tbody>`. A hidden table cell keeps its `<td>`, empty.
132
134
 
133
- This target is unpaginated: it ignores schema page bands (page furniture belongs to your print CSS)
135
+ This target paginates nothing: it ignores schema page bands (page furniture belongs to your print CSS)
134
136
  and maps `break: "page"` and `reset: "page"` to the `q-break` class. Page columns are laid out:
135
137
  a node declaring `columns` wraps the content between its own bands in the `q-columns` container
136
138
  above, and the browser flows it.
137
139
 
140
+ The full contract is [The HTML target](https://getquario.com/docs/diving-deeper/html-target/),
141
+ with each declaration's fate in the
142
+ [support matrix](https://getquario.com/docs/reference/support-matrix/).
143
+
138
144
  ## Unlicensed marking
139
145
 
140
146
  An unlicensed render opens the fragment with `<div class="q-unlicensed">` holding the wording
141
147
  from `report-start.marking` as escaped text. A licensed render emits no badge. The element and its
142
- position are normative; visual treatment belongs to the host. The shipped stylesheet leaves the
148
+ position are normative. Visual treatment belongs to the host. The shipped stylesheet leaves the
143
149
  badge unstyled, because a watermark would escape the fragment's box onto the host page.
144
150
 
145
151
  ## Escaping
146
152
 
147
- **Every interpolated value is escaped.** `{{{ }}}` is a definition error, so no schema syntax can
148
- exempt a data value from escaping. Every generated attribute value is escaped too — inline styles, `data-` attributes, `src`, `alt`
149
- and `colspan`; class names are constants the target owns and never carry data.
153
+ **This target escapes every interpolated value.** `{{{ }}}` is a definition error, so no schema syntax
154
+ can exempt a data value from escaping. It escapes every generated attribute value too — inline styles,
155
+ `data-` attributes, `src`, `alt` and `colspan`. Class names are constants the target owns and never carry data.
150
156
 
151
157
  Literal template text passes through verbatim as author-controlled markup. A definition is
152
- trusted configuration. Its _data_ is untrusted, and data can never reach the document unescaped.
158
+ trusted configuration. Its _data_ is not, and data can never reach the document unescaped.
153
159
 
154
160
  ## Printing to PDF
155
161
 
@@ -169,7 +175,7 @@ Host CSS targets these classes:
169
175
  `.q-break { break-before: page }` honors the schema's own break hint, and `thead` repeats per
170
176
  printed page. Yours adds the page geometry in `@page`, which only you can decide.
171
177
  3. Print with a headless browser (`page.pdf()` in Playwright or Puppeteer, page numbers via the
172
- footer template) or a Paged-CSS engine (WeasyPrint, Prince; page numbers via `@page` margin
178
+ footer template) or a Paged-CSS engine (WeasyPrint or Prince, with page numbers via `@page` margin
173
179
  boxes).
174
180
 
175
181
  ### The reference stylesheet
@@ -189,21 +195,28 @@ const css = readFileSync(new URL(import.meta.resolve("@quario/html/style.css")),
189
195
  const page = `<!doctype html><html><head><style>${css}</style></head><body>${fragment}</body></html>`;
190
196
  ```
191
197
 
192
- Its **selectors** are contract; its **rules** are not. It is a reference default meant to be
198
+ Its **selectors** are contract. Its **rules** are not. It is a reference default meant to be
193
199
  overridden. Every rule is ordinary specificity, so your own stylesheet loaded after it wins
194
200
  without `!important` anywhere. Link nothing at all and you get unstyled markup.
195
201
 
196
202
  Two things it leaves to you: `@page` geometry and body type, which only a host can decide, and
197
203
  any watermark treatment of `.q-unlicensed`, left as plain text here because `position: fixed`
198
- would paint over your whole page. `example/print.css` in the repository is a host's half showing
204
+ would paint over your whole page. `example/print.css` in the repository is a host's half that does
199
205
  both, and `example/print.js` is the complete pipeline.
200
206
 
207
+ ## Documentation
208
+
209
+ [The quario documentation](https://getquario.com/docs/) is the reference.
210
+ The [report schema](https://getquario.com/docs/reference/report-schema/) is the normative
211
+ specification of what a report may declare, and
212
+ [`@quario/html`](https://getquario.com/docs/reference/html/) is this package's own API.
213
+
201
214
  ## License
202
215
 
203
- Commercial software with readable source. Free, unlimited, watermarked evaluation; per-developer
204
- licenses at [getquario.com](https://getquario.com). See the bundled LICENSE.
216
+ Commercial software with readable source. Evaluation is free, unlimited, and watermarked.
217
+ Per-developer licenses at [getquario.com](https://getquario.com). See the bundled LICENSE.
205
218
 
206
- Pass your license key once, on the instance; it is verified offline:
219
+ Pass your license key once, on the instance. quario verifies it offline:
207
220
 
208
221
  ```js
209
222
  const q = quario({ license: "quario_..." });
package/lib/index.d.ts CHANGED
@@ -1,10 +1,16 @@
1
1
  import type { Target } from "quario";
2
2
 
3
+ /**
4
+ * Host controls, taken and validated at the factory call: an option this
5
+ * target does not understand, or one of the wrong type, throws a `TypeError`
6
+ * there rather than costing the host the option in silence.
7
+ */
3
8
  export interface HtmlOptions {
4
9
  /**
5
10
  * Emit `data-q-path` on each element whose event carries a schema `path`,
6
11
  * mapping rendered output back to the definition behind it — the editor's
7
- * selection seam. Off by default.
12
+ * selection seam. Off by default, and a value that is neither `true` nor
13
+ * `false` throws from `html()` rather than reading as on.
8
14
  */
9
15
  paths?: boolean;
10
16
  /**
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, styledRuns, text, walk } from "quario";
12
+ import { display, format, hostOptions, isReportBand, styledRuns, text, walk } from "quario";
13
13
 
14
14
  /** @type {Record<string, string>} */
15
15
  let ESC = {
@@ -112,16 +112,30 @@ let OWN = new Set(["group-header", "group-footer"]);
112
112
  // too: one option, one message shape.
113
113
  /** @type {(path: string, value: any) => string} */
114
114
  let cssValue = (path, value) => {
115
- if (typeof value !== "string" || !value) throw Error(path + ": expected a CSS font-family value");
116
- if (UNSAFE.test(value)) throw Error(path + ": cannot contain ';' or '}'");
115
+ if (typeof value !== "string" || !value)
116
+ throw TypeError(path + ": expected a CSS font-family value");
117
+ if (UNSAFE.test(value)) throw TypeError(path + ": cannot contain ';' or '}'");
117
118
  return value;
118
119
  };
119
120
 
121
+ // A flag rather than anything truthy: `html({ paths: "no" })` reads as yes
122
+ // under a truthiness test, which is the opposite of what the host wrote
123
+ // (`docs/adr/0072`).
124
+ /** @type {(paths: any) => boolean} */
125
+ let stamping = (paths) => {
126
+ if (paths != null && typeof paths !== "boolean")
127
+ throw TypeError("options.paths: expected true or false");
128
+ return Boolean(paths);
129
+ };
130
+
120
131
  /** @type {(fonts: any) => Record<string, string>} */
121
132
  let mapping = (fonts) => {
122
133
  if (fonts == null) return {};
123
- if (typeof fonts !== "object")
124
- throw Error("options.fonts: expected an object of family names to CSS values");
134
+ // A record of family names, so the key set is open and `hostOptions` is the
135
+ // wrong tool -- but an array is a `typeof "object"` that would name families
136
+ // 0, 1, 2, so it is refused here.
137
+ if (typeof fonts !== "object" || Array.isArray(fonts))
138
+ throw TypeError("options.fonts: expected an object of family names to CSS values");
125
139
  return Object.fromEntries(
126
140
  Object.entries(fonts).map(([name, value]) => [
127
141
  name.toLowerCase(),
@@ -368,7 +382,8 @@ let cells = (list, tag, attrs, intl, markup) =>
368
382
  * The target (see SCHEMA.md, "Instances and targets").
369
383
  */
370
384
  export function html(options) {
371
- let mark = options?.paths ? pathAttr : () => "";
385
+ hostOptions(options, ["paths", "fonts"], "options");
386
+ let mark = stamping(options?.paths) ? pathAttr : () => "";
372
387
  // The factory's verdict, like `mark`: the mapping is validated once, here.
373
388
  let { attr: styleAttr, map: fontMap } = styling(options?.fonts);
374
389
  // A cell's markup, bound to this target's font table once.
@@ -534,6 +549,14 @@ export function html(options) {
534
549
  // one, and escaped when it is encoded rather than per occurrence. `alt` is report data, so it is the display-text join escaped,
535
550
  // never the markup-passing join a cell body gets; an image without one
536
551
  // carries `alt=""`, the decorative-image convention.
552
+ //
553
+ // `width`/`height` are the event's natural size, which the engine has
554
+ // already read and vouched for, so a picture reserves its aspect ratio
555
+ // before it decodes rather than reflowing the page around it. The pair
556
+ // is a presentational hint, not a ratio on its own, which is why the
557
+ // `fit` style carries `height:auto`: without it the stated height is a
558
+ // used height and a picture capped narrower than its file renders
559
+ // squashed. SCHEMA.md, "The HTML target", states that normatively.
537
560
  image: (event) => {
538
561
  let slot = placed(event);
539
562
  let source = sources.get(event.bytes);
@@ -551,10 +574,15 @@ export function html(options) {
551
574
  mark(event) +
552
575
  '><img src="' +
553
576
  source +
577
+ '" width="' +
578
+ esc(event.width) +
579
+ '" height="' +
580
+ esc(event.height) +
554
581
  '" alt="' +
555
582
  esc(text(event.alt || [])) +
556
- // Both keep the aspect ratio, since neither names a height.
557
- (event.fit === "width" ? '" style="width:100%">' : '" style="max-width:100%">') +
583
+ '" style="' +
584
+ (event.fit === "width" ? "" : "max-") +
585
+ 'width:100%;height:auto">' +
558
586
  "</div>",
559
587
  );
560
588
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quario/html",
3
- "version": "0.7.0",
3
+ "version": "0.9.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.7.0",
42
+ "quario": "^0.9.0",
43
43
  "size-limit": "^13.0.3",
44
44
  "typescript": "^7.0.2"
45
45
  },
46
46
  "peerDependencies": {
47
- "quario": "^0.7.0"
47
+ "quario": "^0.9.0"
48
48
  },
49
49
  "size-limit": [
50
50
  {