@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 +20 -0
- package/README.md +29 -17
- package/lib/index.js +16 -11
- package/package.json +3 -3
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
|
|
65
|
-
page
|
|
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
|
-
|
|
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
|
|
101
|
-
|
|
|
102
|
-
| `<div class="q-
|
|
103
|
-
| `<div class="q-
|
|
104
|
-
| `<
|
|
105
|
-
| `<
|
|
106
|
-
| `<div class="q-
|
|
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 |
|
|
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
|
|
125
|
-
the
|
|
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.
|
|
138
|
-
|
|
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 = {
|
|
15
|
+
let ESC = {
|
|
16
|
+
"&": "&",
|
|
17
|
+
"<": "<",
|
|
18
|
+
">": ">",
|
|
19
|
+
'"': """,
|
|
20
|
+
"'": "'",
|
|
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
|
|
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 = {
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
47
|
+
"quario": "^0.6.0"
|
|
48
48
|
},
|
|
49
49
|
"size-limit": [
|
|
50
50
|
{
|