@quario/html 0.5.0 → 0.6.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
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.0] - 2026-09-07
11
+
12
+ ### Added
13
+
14
+ - **A cell's own `currency` code is honoured** at the markup edge, ahead of the
15
+ instance's default, so a listing whose rows arrive in different currencies
16
+ presents each in its own. A code the engine could not accept leaves the cell
17
+ as plain display text rather than presenting it in the instance's currency.
18
+
19
+ ### Changed
20
+
21
+ - **Numbers presented through `format` now show a fixed two fraction digits,
22
+ matching every other target.** `format: "number"` on `1000` renders
23
+ `1,000.00` where it rendered `1,000`, and `0.12345` renders `0.12` where it
24
+ rendered `0.123`; `format: "percent"` on `0.21` renders `21.00%` where it
25
+ rendered `21%`. `format: "currency"` follows the currency's own minor units,
26
+ so a JPY amount loses the two decimals it never had. `date` is unchanged, as
27
+ are grouping separators and symbol placement. The digits come from the
28
+ engine, so a cell reads the same here as it does in a workbook.
29
+
10
30
  ## [0.5.0] - 2026-09-05
11
31
 
12
32
  ### Added
package/README.md CHANGED
@@ -61,8 +61,9 @@ const page = await report.render(html(), data);
61
61
  </div>
62
62
  ```
63
63
 
64
- The output is a **fragment**: no `<html>`, no `<head>`, no styles of its own. Wrap it in your own
65
- page shell and stylesheet.
64
+ The output is a **fragment**: no `<html>`, no `<head>`, no stylesheet of its own the one
65
+ `<style>` it writes is an `@page { margin }` rule when the schema declares `page.margin`. Wrap it
66
+ in your own page shell and stylesheet.
66
67
 
67
68
  ## API
68
69
 
@@ -72,9 +73,16 @@ The target factory returns the target you pass to `render`.
72
73
  `report()` compiles once and `report.render(html(), data)` resolves the fragment. Compile at
73
74
  startup and render per request. Definition problems throw at `report()`, at compile time.
74
75
 
75
- One option: `{ paths: true }` stamps `data-q-path="<schema path>"` on each element whose event
76
+ Two options. `{ paths: true }` stamps `data-q-path="<schema path>"` on each element whose event
76
77
  carries one (items, images, group containers, the table and its cells), mapping rendered output
77
- back to the definition behind it. Off by default.
78
+ back to the definition behind it. Off by default. `{ fonts }` maps a declared `family` name to the
79
+ 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:
82
+
83
+ ```js
84
+ html({ fonts: { "Instrument Sans": "var(--font-instrument-sans)", mono: "var(--font-ibm-plex-mono)" } });
85
+ ```
78
86
 
79
87
  ```js
80
88
  const report = quario().report(schema, funcs);
@@ -97,15 +105,17 @@ host. Compilation stays synchronous; render-time failures reject with the same l
97
105
  These classes are the contract host CSS targets. They are stable, and changing them is a breaking
98
106
  change.
99
107
 
100
- | Emits | For |
101
- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
102
- | `<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` |
103
- | `<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"` |
104
- | `<table class="q-table">` | Each table, with a real `colgroup`, `thead`, `tbody`, and a `tfoot` when totals are declared |
105
- | `<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; the rendered `alt` is escaped |
106
- | `<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, each holding the slot item's ordinary container |
107
-
108
- A column `width` becomes an inline `width:<n>%` on its `<col>`. The classes above belong to this
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 |
117
+
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
109
119
  target. The schema never speaks in CSS. Displaying a fragment that contains images under a Content
110
120
  Security Policy needs `img-src data:`.
111
121
 
@@ -121,8 +131,9 @@ instead. The PDF and XLSX targets carry theirs built in, because their consumers
121
131
  row set still emits the table, its header, and an empty `<tbody>`. A hidden table cell keeps its `<td>`, empty.
122
132
 
123
133
  This target is unpaginated: it ignores schema page bands (page furniture belongs to your print CSS)
124
- and maps `break: "page"` and `reset: "page"` to the `q-break` class. Page columns are accepted on
125
- the schema and ignored here until a target honors them.
134
+ and maps `break: "page"` and `reset: "page"` to the `q-break` class. Page columns are laid out:
135
+ a node declaring `columns` wraps the content between its own bands in the `q-columns` container
136
+ above, and the browser flows it.
126
137
 
127
138
  ## Unlicensed marking
128
139
 
@@ -134,8 +145,8 @@ badge unstyled, because a watermark would escape the fragment's box onto the hos
134
145
  ## Escaping
135
146
 
136
147
  **Every interpolated value is escaped.** `{{{ }}}` is a definition error, so no schema syntax can
137
- exempt a data value from escaping. Generated attribute values, including classes and inline
138
- styles built from expressions, are escaped too.
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.
139
150
 
140
151
  Literal template text passes through verbatim as author-controlled markup. A definition is
141
152
  trusted configuration. Its _data_ is untrusted, and data can never reach the document unescaped.
@@ -154,6 +165,7 @@ Host CSS targets these classes:
154
165
  2. The shipped sheet already puts band behavior on the emitted classes:
155
166
  `.q-item { min-height: 1lh; white-space: pre-line }` occupies a line and breaks on newlines,
156
167
  `.q-group { break-inside: avoid }` keeps a group header with its rows,
168
+ `.q-table tr { break-inside: avoid }` keeps a row whole,
157
169
  `.q-break { break-before: page }` honors the schema's own break hint, and `thead` repeats per
158
170
  printed page. Yours adds the page geometry in `@page`, which only you can decide.
159
171
  3. Print with a headless browser (`page.pdf()` in Playwright or Puppeteer, page numbers via the
package/lib/index.js CHANGED
@@ -12,7 +12,13 @@
12
12
  import { display, format, isReportBand, text, walk } from "quario";
13
13
 
14
14
  /** @type {Record<string, string>} */
15
- let ESC = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
15
+ let ESC = {
16
+ "&": "&amp;",
17
+ "<": "&lt;",
18
+ ">": "&gt;",
19
+ '"': "&quot;",
20
+ "'": "&#39;",
21
+ };
16
22
  /** @type {(value: any) => string} */
17
23
  let esc = (value) => display(value).replace(/[&<>"']/g, (char) => ESC[char]);
18
24
 
@@ -30,7 +36,7 @@ let markup = (tokens, style, intl) => {
30
36
  };
31
37
  /** @type {(value: any, style: any, intl: any) => any} */
32
38
  let shown = (value, style, intl) => {
33
- let text = format(value, style?.format, intl);
39
+ let text = format(value, style, intl);
34
40
  return text != null ? text : value;
35
41
  };
36
42
 
@@ -441,7 +447,11 @@ export function html(options) {
441
447
  // edge, so the badge stays childless whatever stream a host compiles
442
448
  // against.
443
449
  "report-start": (event) => {
444
- intl = { locale: event.locale, currency: event.currency, timeZone: event.timeZone };
450
+ intl = {
451
+ locale: event.locale,
452
+ currency: event.currency,
453
+ timeZone: event.timeZone,
454
+ };
445
455
  if (event.marking) out += '<div class="q-unlicensed">' + esc(event.marking) + "</div>";
446
456
  if (event.margin != null)
447
457
  out += "<style>@page{margin:" + esc(event.margin) + "pt}</style>";
@@ -557,13 +567,6 @@ export function html(options) {
557
567
  for (let column of /** @type {any[]} */ (event.columns))
558
568
  cols +=
559
569
  "<col" + (column.width ? ' style="width:' + esc(column.width) + '%"' : "") + ">";
560
- // The header row is the cells the columns declared: a column a
561
- // neighbour's span covers has none, and contributes only its `<col>`.
562
- let headers = event.columns
563
- .filter((/** @type {any} */ column) => column.header)
564
- .map((/** @type {any} */ column) =>
565
- Object.assign({}, column.header, { path: column.path }),
566
- );
567
570
  out +=
568
571
  '<table class="q-table"' +
569
572
  mark(event) +
@@ -572,7 +575,9 @@ export function html(options) {
572
575
  cols +
573
576
  "</colgroup>" +
574
577
  "<thead>" +
575
- tr(event, headers, "th", intl) +
578
+ // The header row arrives shaped like a `row` payload, so the same
579
+ // `<tr>` writer takes it whole (docs/adr/0053).
580
+ tr(event.header, event.header.cells, "th", intl) +
576
581
  "</thead><tbody>";
577
582
  },
578
583
  row: (event) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quario/html",
3
- "version": "0.5.0",
3
+ "version": "0.6.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.5.0",
42
+ "quario": "^0.6.0",
43
43
  "size-limit": "^13.0.3",
44
44
  "typescript": "^7.0.2"
45
45
  },
46
46
  "peerDependencies": {
47
- "quario": "^0.5.0"
47
+ "quario": "^0.6.0"
48
48
  },
49
49
  "size-limit": [
50
50
  {