@quario/html 0.8.0 → 0.10.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 +66 -0
- package/README.md +46 -33
- package/lib/index.d.ts +7 -1
- package/lib/index.js +149 -164
- package/lib/style.css +7 -1
- package/package.json +11 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,71 @@
|
|
|
1
1
|
# @quario/html
|
|
2
2
|
|
|
3
|
+
## 0.10.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- **A group's `break` names a position, and `break: "page"` is retired.** Its values are `"before"`, `"between"`, `"after"` and `"around"`. Write `break: "before"` where you wrote `break: "page"`. That is the whole of the migration, and it changes nothing the report produces.
|
|
8
|
+
|
|
9
|
+
The boundary between two consecutive instances always turns a page. `before` adds the leading edge of the run, so the first instance opens a page of its own. `between` adds neither edge, which keeps the first instance on the page the report header opened — the case the old vocabulary could not express, and the reason for the change. `after` adds the trailing edge, and `around` adds both. A trailing edge turns the page for what follows the run, so a report whose last band is that group reads `after` as `between` and `around` as `before`. A nested group takes its two edges from each instance of the group above it, rather than from the document. `"page"` said which unit a break used, where the four say where it falls; one set cannot say both and still read at a glance.
|
|
10
|
+
|
|
11
|
+
**`reset: "page"` turns no page of its own, and now needs a `break` beside it.** It says only that a new `page.number` / `page.total` sequence starts at this instance. A sequence owns whole pages, so `reset` requires `break` to be `"before"` or `"around"`. Any other `break`, and `reset` with no `break` at all, is a definition error the compile reports. Add `break: "before"` to a group that declares `reset` alone today.
|
|
12
|
+
|
|
13
|
+
The HTML target adds `q-break` to an instance whose leading edge turns, as before, and the new `q-break-after` to one whose trailing edge turns. `@quario/html/style.css` gains `.q-break-after { break-after: page }` beside the rule it already shipped for `.q-break`.
|
|
14
|
+
|
|
15
|
+
The Word target also stops losing a page break a table would swallow. A table carries no paragraph properties, so a break owed where one starts had nowhere to sit and reached the next paragraph instead, on the wrong page or on none. It now gets a paragraph of its own, the same carrier a section break already took.
|
|
16
|
+
|
|
17
|
+
The render-event stream states the two edges rather than the four positions. `group-start` carries `break` where a page turns before the instance, and the new `breakAfter` where one turns after it, so a target reads boundaries and never the position that asked for them.
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- Updated dependencies
|
|
22
|
+
- quario@0.10.0
|
|
23
|
+
|
|
24
|
+
## 0.9.0
|
|
25
|
+
|
|
26
|
+
### Minor Changes
|
|
27
|
+
|
|
28
|
+
- **Every target factory now refuses an option it does not understand.** An unknown key, a key with
|
|
29
|
+
a value of the wrong type, or an `options` that is not an object throws a `TypeError` at the
|
|
30
|
+
factory call, naming the option path — `options.meta.title`, `options.page.size`. `@quario/docx`
|
|
31
|
+
already behaved this way. `@quario/pdf`, `@quario/html`, `@quario/xlsx` and `@quario/layout` read
|
|
32
|
+
the keys they knew and ignored the rest, so a typo cost you the option you meant to set with no
|
|
33
|
+
signal at any point. `csv()` takes no options, and it refuses an argument rather than discarding
|
|
34
|
+
one.
|
|
35
|
+
|
|
36
|
+
The check itself is one thing, so the engine now exports it: `hostOptions` closes a key set and
|
|
37
|
+
`hostMeta` validates the `{ title, author, subject }` contract the document targets share. A
|
|
38
|
+
fourth document property is one edit rather than three.
|
|
39
|
+
|
|
40
|
+
**What this changes for you.** One options object spread across several targets stops working if
|
|
41
|
+
any target does not know one of its keys — `const o = { page, fonts, meta }` passed to `pdf(o)`,
|
|
42
|
+
`html(o)` and `xlsx(o)` is three different key sets. An object holding only what its consumers
|
|
43
|
+
share is unaffected, so `{ page, fonts }` into both `pdf()` and `layout()` still works.
|
|
44
|
+
TypeScript does not warn about this: excess-property checking fires on an object literal and not
|
|
45
|
+
on a variable, so a bag held in a `const` compiles clean and throws when you call the factory.
|
|
46
|
+
|
|
47
|
+
Nullish is absence everywhere, at both levels, so `{ meta: config.meta ?? null }` and
|
|
48
|
+
`{ meta: { title: config.title } }` over a config that carries neither are both fine.
|
|
49
|
+
`html({ paths })` now takes `true` or `false` rather than anything truthy, so `paths: "no"` throws
|
|
50
|
+
instead of turning path stamping on. A `fonts` mapping given as an array is refused by
|
|
51
|
+
`@quario/html` and `@quario/layout` rather than read as families named `0`, `1`, `2`.
|
|
52
|
+
|
|
53
|
+
Every host-option failure is now a `TypeError` rather than a plain `Error`. A `catch` that tests
|
|
54
|
+
`instanceof Error` is unaffected. One that compares the constructor is not.
|
|
55
|
+
|
|
56
|
+
`@quario/docx` is relaxed in three places, each of them now accepting what it refused: `meta: null`
|
|
57
|
+
and `page: null` are absence rather than errors, a property written as `meta: { title: undefined }`
|
|
58
|
+
is a property you did not write rather than one of the wrong type, and an inherited enumerable key
|
|
59
|
+
is no longer reported as an option you wrote.
|
|
60
|
+
|
|
61
|
+
`@quario/pdf`, `@quario/xlsx` and `@quario/docx` now copy `meta` at the factory call. Mutating the
|
|
62
|
+
object you passed no longer changes what a configured target writes.
|
|
63
|
+
|
|
64
|
+
### Patch Changes
|
|
65
|
+
|
|
66
|
+
- Updated dependencies
|
|
67
|
+
- quario@0.9.0
|
|
68
|
+
|
|
3
69
|
## 0.8.0
|
|
4
70
|
|
|
5
71
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
definition to an HTML fragment with semantic tables, a stable class contract, and every
|
|
5
5
|
interpolated value escaped.
|
|
6
6
|
|
|
7
|
+
## Contents
|
|
8
|
+
|
|
9
|
+
- [Install](#install)
|
|
10
|
+
- [Quick start](#quick-start)
|
|
11
|
+
- [API](#api)
|
|
12
|
+
- [Output contract](#output-contract)
|
|
13
|
+
- [Unlicensed marking](#unlicensed-marking)
|
|
14
|
+
- [Escaping](#escaping)
|
|
15
|
+
- [Printing to PDF](#printing-to-pdf)
|
|
16
|
+
- [Documentation](#documentation)
|
|
17
|
+
- [License](#license)
|
|
18
|
+
|
|
7
19
|
## Install
|
|
8
20
|
|
|
9
21
|
```bash
|
|
@@ -69,7 +81,8 @@ in your own page shell and stylesheet.
|
|
|
69
81
|
|
|
70
82
|
### `html(options?)`
|
|
71
83
|
|
|
72
|
-
The target factory
|
|
84
|
+
The target factory takes this target's host options, validates them at the call, and returns
|
|
85
|
+
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
86
|
`report()` compiles once and `report.render(html(), data)` resolves the fragment. Compile at
|
|
74
87
|
startup and render per request. Definition problems throw at `report()`, at compile time.
|
|
75
88
|
|
|
@@ -77,8 +90,8 @@ Two options. `{ paths: true }` stamps `data-q-path="<schema path>"` on each elem
|
|
|
77
90
|
carries one (items, images, group containers, the table and its cells), mapping rendered output
|
|
78
91
|
back to the definition behind it. Off by default. `{ fonts }` maps a declared `family` name to the
|
|
79
92
|
CSS `font-family` value it should emit — a custom property, a font stack, a quoted name —
|
|
80
|
-
consulted before the three built-in generics
|
|
81
|
-
`;` or `}`
|
|
93
|
+
consulted before the three built-in generics. Names match case-insensitively, and the factory
|
|
94
|
+
rejects a value holding `;` or `}` when it constructs the target:
|
|
82
95
|
|
|
83
96
|
```js
|
|
84
97
|
html({ fonts: { "Instrument Sans": "var(--font-instrument-sans)", mono: "var(--font-ibm-plex-mono)" } });
|
|
@@ -95,25 +108,25 @@ report.stream(data); // the raw event generator, if you want events instead
|
|
|
95
108
|
```
|
|
96
109
|
|
|
97
110
|
Engine-level options (`query` budgets, the license key) live on the instance
|
|
98
|
-
(`quario({ query, license })`)
|
|
111
|
+
(`quario({ query, license })`). See the [engine README](https://www.npmjs.com/package/quario).
|
|
99
112
|
|
|
100
|
-
Rendering is asynchronous and
|
|
101
|
-
host. Compilation stays synchronous
|
|
113
|
+
Rendering is asynchronous and returns the loop between batches, so a large report never blocks the
|
|
114
|
+
host. Compilation stays synchronous. Render-time failures reject with the same located errors.
|
|
102
115
|
|
|
103
116
|
## Output contract
|
|
104
117
|
|
|
105
118
|
These classes are the contract host CSS targets. They are stable, and changing them is a breaking
|
|
106
119
|
change.
|
|
107
120
|
|
|
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`
|
|
113
|
-
| `<table class="q-table">` | Each table, with a real `colgroup`, `thead`, `tbody`, and a `tfoot` around the total rows the walk emits
|
|
114
|
-
| `<div class="q-item q-image q-<role>">` | Each image item, holding one `<img
|
|
115
|
-
| `<div class="q-columns" style="column-count:<n>">` | The content between a node's own bands when it declares `columns
|
|
116
|
-
| `<div class="q-split q-<role>">` | Each split, carrying inline `display:flex
|
|
121
|
+
| Emits | For |
|
|
122
|
+
| -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
123
|
+
| `<div class="q-report">` | The fragment's root, holding every band |
|
|
124
|
+
| `<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` |
|
|
125
|
+
| `<div class="q-group" data-group="name">` | Each group instance, wrapping its header, nested content, and footer. Adds `q-break` where the group's `break` turns a page before the instance, and `q-break-after` where one turns after it |
|
|
126
|
+
| `<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` |
|
|
127
|
+
| `<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` |
|
|
128
|
+
| `<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` |
|
|
129
|
+
| `<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
130
|
|
|
118
131
|
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
132
|
target. The schema never speaks in CSS. Displaying a fragment that contains images under a Content
|
|
@@ -123,16 +136,16 @@ Security Policy needs `img-src data:` (see
|
|
|
123
136
|
Style declarations map to inline CSS (`bold` → `font-weight:bold` or `font-weight:normal`, `size` → `font-size:<n>pt`,
|
|
124
137
|
`family: "mono"` → `font-family:monospace`, …). Occupy-a-line and newline-as-break are **not** inline: they hang off
|
|
125
138
|
`.q-item` and `.q-table th, td` in [`@quario/html/style.css`](#the-reference-stylesheet), the same way `.q-break`
|
|
126
|
-
honors
|
|
127
|
-
A fragment without that sheet still carries the classes and the text, including newlines
|
|
139
|
+
honors a leading page break. The class is the hook. The rule is a reference default a host overrides on source order.
|
|
140
|
+
A fragment without that sheet still carries the classes and the text, including newlines. It does not occupy or
|
|
128
141
|
break until some stylesheet says so. This target supplies no defaults in the markup: no weight or size per band
|
|
129
142
|
role, no leading, no padding, no spacing between bands, no borders. An inline `style` attribute would beat yours
|
|
130
|
-
in the cascade and force `!important` on you
|
|
143
|
+
in the cascade and force `!important` on you. The defaults and the honor rules ship as an ordinary stylesheet
|
|
131
144
|
instead. The PDF and XLSX targets carry theirs built in, because their consumers have no stylesheet. An empty
|
|
132
145
|
row set still emits the table, its header, and an empty `<tbody>`. A hidden table cell keeps its `<td>`, empty.
|
|
133
146
|
|
|
134
|
-
This target
|
|
135
|
-
and maps
|
|
147
|
+
This target paginates nothing: it ignores schema page bands (page furniture belongs to your print CSS)
|
|
148
|
+
and maps each edge a group's `break` turns to its own class. Page columns are laid out:
|
|
136
149
|
a node declaring `columns` wraps the content between its own bands in the `q-columns` container
|
|
137
150
|
above, and the browser flows it.
|
|
138
151
|
|
|
@@ -144,17 +157,17 @@ with each declaration's fate in the
|
|
|
144
157
|
|
|
145
158
|
An unlicensed render opens the fragment with `<div class="q-unlicensed">` holding the wording
|
|
146
159
|
from `report-start.marking` as escaped text. A licensed render emits no badge. The element and its
|
|
147
|
-
position are normative
|
|
160
|
+
position are normative. Visual treatment belongs to the host. The shipped stylesheet leaves the
|
|
148
161
|
badge unstyled, because a watermark would escape the fragment's box onto the host page.
|
|
149
162
|
|
|
150
163
|
## Escaping
|
|
151
164
|
|
|
152
|
-
**
|
|
153
|
-
exempt a data value from escaping.
|
|
154
|
-
and `colspan
|
|
165
|
+
**This target escapes every interpolated value.** `{{{ }}}` is a definition error, so no schema syntax
|
|
166
|
+
can exempt a data value from escaping. It escapes every generated attribute value too — inline styles,
|
|
167
|
+
`data-` attributes, `src`, `alt` and `colspan`. Class names are constants the target owns and never carry data.
|
|
155
168
|
|
|
156
169
|
Literal template text passes through verbatim as author-controlled markup. A definition is
|
|
157
|
-
trusted configuration. Its _data_ is
|
|
170
|
+
trusted configuration. Its _data_ is not, and data can never reach the document unescaped.
|
|
158
171
|
|
|
159
172
|
## Printing to PDF
|
|
160
173
|
|
|
@@ -171,10 +184,10 @@ Host CSS targets these classes:
|
|
|
171
184
|
`.q-item { min-height: 1lh; white-space: pre-line }` occupies a line and breaks on newlines,
|
|
172
185
|
`.q-group { break-inside: avoid }` keeps a group header with its rows,
|
|
173
186
|
`.q-table tr { break-inside: avoid }` keeps a row whole,
|
|
174
|
-
`.q-break { break-before: page }`
|
|
175
|
-
printed page. Yours adds the page geometry in `@page`, which only you can decide.
|
|
187
|
+
`.q-break { break-before: page }` and `.q-break-after { break-after: page }` honor the schema's own
|
|
188
|
+
break hints, and `thead` repeats per printed page. Yours adds the page geometry in `@page`, which only you can decide.
|
|
176
189
|
3. Print with a headless browser (`page.pdf()` in Playwright or Puppeteer, page numbers via the
|
|
177
|
-
footer template) or a Paged-CSS engine (WeasyPrint
|
|
190
|
+
footer template) or a Paged-CSS engine (WeasyPrint or Prince, with page numbers via `@page` margin
|
|
178
191
|
boxes).
|
|
179
192
|
|
|
180
193
|
### The reference stylesheet
|
|
@@ -194,13 +207,13 @@ const css = readFileSync(new URL(import.meta.resolve("@quario/html/style.css")),
|
|
|
194
207
|
const page = `<!doctype html><html><head><style>${css}</style></head><body>${fragment}</body></html>`;
|
|
195
208
|
```
|
|
196
209
|
|
|
197
|
-
Its **selectors** are contract
|
|
210
|
+
Its **selectors** are contract. Its **rules** are not. It is a reference default meant to be
|
|
198
211
|
overridden. Every rule is ordinary specificity, so your own stylesheet loaded after it wins
|
|
199
212
|
without `!important` anywhere. Link nothing at all and you get unstyled markup.
|
|
200
213
|
|
|
201
214
|
Two things it leaves to you: `@page` geometry and body type, which only a host can decide, and
|
|
202
215
|
any watermark treatment of `.q-unlicensed`, left as plain text here because `position: fixed`
|
|
203
|
-
would paint over your whole page. `example/print.css` in the repository is a host's half
|
|
216
|
+
would paint over your whole page. `example/print.css` in the repository is a host's half that does
|
|
204
217
|
both, and `example/print.js` is the complete pipeline.
|
|
205
218
|
|
|
206
219
|
## Documentation
|
|
@@ -212,10 +225,10 @@ specification of what a report may declare, and
|
|
|
212
225
|
|
|
213
226
|
## License
|
|
214
227
|
|
|
215
|
-
Commercial software with readable source.
|
|
216
|
-
licenses at [getquario.com](https://getquario.com). See the bundled LICENSE.
|
|
228
|
+
Commercial software with readable source. Evaluation is free, unlimited, and watermarked.
|
|
229
|
+
Per-developer licenses at [getquario.com](https://getquario.com). See the bundled LICENSE.
|
|
217
230
|
|
|
218
|
-
Pass your license key once, on the instance
|
|
231
|
+
Pass your license key once, on the instance. quario verifies it offline:
|
|
219
232
|
|
|
220
233
|
```js
|
|
221
234
|
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, splits, styledRuns, text, walk } from "quario";
|
|
13
13
|
|
|
14
14
|
/** @type {Record<string, string>} */
|
|
15
15
|
let ESC = {
|
|
@@ -24,9 +24,8 @@ let esc = (value) => display(value).replace(/[&<>"']/g, (char) => ESC[char]);
|
|
|
24
24
|
|
|
25
25
|
// Join a cell's tokens to HTML: literal template text is author-controlled
|
|
26
26
|
// markup and passes verbatim; every interpolated value is escaped, no
|
|
27
|
-
// exceptions.
|
|
28
|
-
//
|
|
29
|
-
// that one, escaped whole.
|
|
27
|
+
// exceptions. An attribute value takes the engine's `text` join instead,
|
|
28
|
+
// escaped whole.
|
|
30
29
|
/** @type {(tokens: any[], style: any, intl: any) => string} */
|
|
31
30
|
let joined = (tokens, style, intl) => {
|
|
32
31
|
let out = "";
|
|
@@ -41,10 +40,8 @@ let shown = (value, style, intl) => {
|
|
|
41
40
|
};
|
|
42
41
|
|
|
43
42
|
// One styled run: a `<span>` carrying that run's whole resolved inline style,
|
|
44
|
-
//
|
|
45
|
-
//
|
|
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.
|
|
43
|
+
// not a difference against the cell's -- the engine already composed the two.
|
|
44
|
+
// An unstyled run stays bare.
|
|
48
45
|
/** @type {(styled: { style: any, tokens: any[] }, style: any, intl: any, map: any) => string} */
|
|
49
46
|
let piece = (styled, style, intl, map) => {
|
|
50
47
|
let body = joined(styled.tokens, styled.style ?? style, intl);
|
|
@@ -54,8 +51,7 @@ let piece = (styled, style, intl, map) => {
|
|
|
54
51
|
};
|
|
55
52
|
|
|
56
53
|
// 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
|
|
58
|
-
// stream").
|
|
54
|
+
// `styledRuns`, so this target cannot drift from the others.
|
|
59
55
|
/** @type {(map: any) => (tokens: any[], style?: any, intl?: any) => string} */
|
|
60
56
|
let cellMarkup = (map) => (tokens, style, intl) => {
|
|
61
57
|
let out = "";
|
|
@@ -63,10 +59,9 @@ let cellMarkup = (map) => (tokens, style, intl) => {
|
|
|
63
59
|
return out;
|
|
64
60
|
};
|
|
65
61
|
|
|
66
|
-
// base64 for an image's `data:` URI, without Buffer:
|
|
67
|
-
// and in
|
|
68
|
-
//
|
|
69
|
-
// argument per byte, which a real image exhausts.
|
|
62
|
+
// base64 for an image's `data:` URI, without Buffer: the same `lib/` runs in
|
|
63
|
+
// Node and in browsers, and `btoa` is standard in both. Chunked because
|
|
64
|
+
// `fromCharCode(...bytes)` spreads one argument per byte.
|
|
70
65
|
let CHUNK = 0x8000;
|
|
71
66
|
/** @type {(bytes: Uint8Array) => string} */
|
|
72
67
|
let base64 = (bytes) => {
|
|
@@ -105,23 +100,36 @@ let VALIGNMENTS = Object.keys(ALIGN_CONTENT);
|
|
|
105
100
|
// content like anything else. The report's own bands are the engine's to name.
|
|
106
101
|
let OWN = new Set(["group-header", "group-footer"]);
|
|
107
102
|
// A host font mapping (`html({ fonts })`), lowercased once so lookup matches
|
|
108
|
-
// `pdf({ fonts })`'s. Validated here rather than at render
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
// too: one option, one message shape.
|
|
103
|
+
// `pdf({ fonts })`'s. Validated here rather than at render: a malformed one is
|
|
104
|
+
// host configuration and belongs where the host wrote it. The value itself is
|
|
105
|
+
// trusted and emitted verbatim. `Error` and the `options.fonts.<name>:` prefix
|
|
106
|
+
// match the pdf target's: one option, one message shape.
|
|
113
107
|
/** @type {(path: string, value: any) => string} */
|
|
114
108
|
let cssValue = (path, value) => {
|
|
115
|
-
if (typeof value !== "string" || !value)
|
|
116
|
-
|
|
109
|
+
if (typeof value !== "string" || !value)
|
|
110
|
+
throw TypeError(path + ": expected a CSS font-family value");
|
|
111
|
+
if (UNSAFE.test(value)) throw TypeError(path + ": cannot contain ';' or '}'");
|
|
117
112
|
return value;
|
|
118
113
|
};
|
|
119
114
|
|
|
115
|
+
// A flag rather than anything truthy: `html({ paths: "no" })` reads as yes
|
|
116
|
+
// under a truthiness test, which is the opposite of what the host wrote
|
|
117
|
+
// (`docs/adr/0072`).
|
|
118
|
+
/** @type {(paths: any) => boolean} */
|
|
119
|
+
let stamping = (paths) => {
|
|
120
|
+
if (paths != null && typeof paths !== "boolean")
|
|
121
|
+
throw TypeError("options.paths: expected true or false");
|
|
122
|
+
return Boolean(paths);
|
|
123
|
+
};
|
|
124
|
+
|
|
120
125
|
/** @type {(fonts: any) => Record<string, string>} */
|
|
121
126
|
let mapping = (fonts) => {
|
|
122
127
|
if (fonts == null) return {};
|
|
123
|
-
|
|
124
|
-
|
|
128
|
+
// A record of family names, so the key set is open and `hostOptions` is the
|
|
129
|
+
// wrong tool -- but an array is a `typeof "object"` that would name families
|
|
130
|
+
// 0, 1, 2, so it is refused here.
|
|
131
|
+
if (typeof fonts !== "object" || Array.isArray(fonts))
|
|
132
|
+
throw TypeError("options.fonts: expected an object of family names to CSS values");
|
|
125
133
|
return Object.fromEntries(
|
|
126
134
|
Object.entries(fonts).map(([name, value]) => [
|
|
127
135
|
name.toLowerCase(),
|
|
@@ -130,11 +138,10 @@ let mapping = (fonts) => {
|
|
|
130
138
|
);
|
|
131
139
|
};
|
|
132
140
|
|
|
133
|
-
// Resolve a declared family name against the table the factory built
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
// of naming a face that cannot exist (docs/adr/0034).
|
|
141
|
+
// Resolve a declared family name against the table the factory built, then
|
|
142
|
+
// the name itself. A name that is not nameable as it stands resolves to
|
|
143
|
+
// nothing rather than a rewritten near-miss, so the element falls through to
|
|
144
|
+
// the layer below (docs/adr/0034).
|
|
138
145
|
/** @type {(map: Record<string, string>, value: string) => string} */
|
|
139
146
|
let named = (map, value) => {
|
|
140
147
|
// Own-key lookup: `constructor` must not resolve an inherited member.
|
|
@@ -160,9 +167,8 @@ let CSS = {
|
|
|
160
167
|
background: (value) => (isHex(value) ? "background-color:" + value : ""),
|
|
161
168
|
align: (value) => (ALIGNMENTS.includes(value) ? "text-align:" + value : ""),
|
|
162
169
|
// The table reading, on a `<tr>` or a cell: the browser's own sheet gives
|
|
163
|
-
// cells `vertical-align: inherit`, so a row's reaches them like `bold`
|
|
164
|
-
//
|
|
165
|
-
// before this map sees it.
|
|
170
|
+
// cells `vertical-align: inherit`, so a row's reaches them like `bold` does.
|
|
171
|
+
// `placed` below lifts the name out before this map sees a slot item's.
|
|
166
172
|
valign: (value) => (VALIGNMENTS.includes(value) ? "vertical-align:" + value : ""),
|
|
167
173
|
// The text itself is untouched, so selection and screen readers get what
|
|
168
174
|
// the author wrote; only its rendering is capitalised.
|
|
@@ -268,17 +274,15 @@ let css = (style, map) => {
|
|
|
268
274
|
};
|
|
269
275
|
|
|
270
276
|
// Compose a resolved style into one escaped inline style attribute. `own` is
|
|
271
|
-
// this target's
|
|
277
|
+
// this target's structural CSS, which goes first and always emits: page
|
|
272
278
|
// columns and splits are placement, not the visual defaults this target
|
|
273
|
-
// refuses to supply
|
|
274
|
-
// everything else rather than a second one (CLAUDE.md, the markup edge).
|
|
279
|
+
// refuses to supply.
|
|
275
280
|
/** @type {(fonts: any) => { attr: (event: { style?: any }, own?: string) => string, map: Record<string, string> }} */
|
|
276
281
|
let styling = (fonts) => {
|
|
277
282
|
// The spread is the precedence the option promises: a host mapping is
|
|
278
|
-
// consulted before the three generics, and may replace one.
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
// `family` against the same one.
|
|
283
|
+
// consulted before the three generics, and may replace one. Handed back
|
|
284
|
+
// beside the attribute writer, because a styled run's span resolves a
|
|
285
|
+
// `family` against the same table.
|
|
282
286
|
let map = { ...FAMILY, ...mapping(fonts) };
|
|
283
287
|
return {
|
|
284
288
|
map,
|
|
@@ -290,25 +294,21 @@ let styling = (fonts) => {
|
|
|
290
294
|
};
|
|
291
295
|
|
|
292
296
|
// One slot's wrapper. An authored share is that width; a width-less slot
|
|
293
|
-
// divides what the sized ones leave,
|
|
297
|
+
// divides what the sized ones leave, as a width-less column does.
|
|
294
298
|
// `display:grid` is what gives a slot's box the split's height rather than its
|
|
295
|
-
// own content's
|
|
296
|
-
//
|
|
297
|
-
// fragment, because a slot's item is the same `q-item` div it would be
|
|
298
|
-
// anywhere else -- nothing about an item's markup depends on where it sits.
|
|
299
|
+
// own content's. It belongs here and not on the fragment: a slot's item is the
|
|
300
|
+
// same `q-item` div it would be anywhere else.
|
|
299
301
|
/** @type {(slot: { width?: number } | undefined) => string} */
|
|
300
302
|
let slotAttr = (slot) =>
|
|
301
303
|
' style="' +
|
|
302
304
|
esc("display:grid;" + (slot && slot.width != null ? "width:" + slot.width + "%" : "flex:1")) +
|
|
303
305
|
'"';
|
|
304
306
|
|
|
305
|
-
// A slot item's `valign`. The
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
// style so the cell mapping (`vertical-align`) does not also emit on a div.
|
|
311
|
-
// The result is what `styleAttr` takes: a style and the structural prefix.
|
|
307
|
+
// A slot item's `valign`. The wrapper's grid stretches the item to the split's
|
|
308
|
+
// height, so the item is the box: it becomes a grid of its own and
|
|
309
|
+
// `align-content` places the content. Not on the wrapper -- `align-items`
|
|
310
|
+
// there would shrink the item back and reopen the gap the grid closed. Lifted
|
|
311
|
+
// out of the style so the cell mapping (`vertical-align`) does not also emit.
|
|
312
312
|
/** @type {(event: { style?: any }) => { style?: any, own: string }} */
|
|
313
313
|
let placed = (event) => {
|
|
314
314
|
let valign = event.style?.valign;
|
|
@@ -325,9 +325,9 @@ let justified = (own, style) =>
|
|
|
325
325
|
? own + ";justify-items:" + JUSTIFY_ITEMS[style.align]
|
|
326
326
|
: own;
|
|
327
327
|
|
|
328
|
-
// The identity attribute
|
|
329
|
-
//
|
|
330
|
-
//
|
|
328
|
+
// The identity attribute. `mark` is the factory's verdict: a render without
|
|
329
|
+
// `paths` maps every event to "" through the same function rather than
|
|
330
|
+
// re-deciding per call site.
|
|
331
331
|
/** @type {(event: { path?: string }) => string} */
|
|
332
332
|
let pathAttr = ({ path }) => (path ? ' data-q-path="' + esc(path) + '"' : "");
|
|
333
333
|
/** @type {(list: any[], tag: string, attrs: (event: any) => string, intl: any, markup: any) => string} */
|
|
@@ -351,9 +351,7 @@ let cells = (list, tag, attrs, intl, markup) =>
|
|
|
351
351
|
*
|
|
352
352
|
* `paths` opts the fragment into identity: each element whose event carries a
|
|
353
353
|
* schema `path` gains `data-q-path`, so a consumer can map rendered output
|
|
354
|
-
* back to the definition behind it
|
|
355
|
-
* default, because the attribute is weight every plain display pays for
|
|
356
|
-
* nothing.
|
|
354
|
+
* back to the definition behind it. Off by default.
|
|
357
355
|
*
|
|
358
356
|
* `fonts` is this target's font mapping: a declared `family` name to a CSS
|
|
359
357
|
* value, so a host says what the name means on this page. Malformed values
|
|
@@ -368,7 +366,8 @@ let cells = (list, tag, attrs, intl, markup) =>
|
|
|
368
366
|
* The target (see SCHEMA.md, "Instances and targets").
|
|
369
367
|
*/
|
|
370
368
|
export function html(options) {
|
|
371
|
-
|
|
369
|
+
hostOptions(options, ["paths", "fonts"], "options");
|
|
370
|
+
let mark = stamping(options?.paths) ? pathAttr : () => "";
|
|
372
371
|
// The factory's verdict, like `mark`: the mapping is validated once, here.
|
|
373
372
|
let { attr: styleAttr, map: fontMap } = styling(options?.fonts);
|
|
374
373
|
// A cell's markup, bound to this target's font table once.
|
|
@@ -376,15 +375,12 @@ export function html(options) {
|
|
|
376
375
|
// The pair every cell carries, bound once rather than threaded as two.
|
|
377
376
|
/** @type {(cell: any) => string} */
|
|
378
377
|
// A span covers several columns; `colspan` is what that is on this surface.
|
|
379
|
-
// Escaped like every other attribute value this target writes, though the
|
|
380
|
-
// engine has already vouched the span is a positive integer.
|
|
381
378
|
/** @type {(cell: any) => string} */
|
|
382
379
|
let attrs = (cell) =>
|
|
383
380
|
(cell.span > 1 ? ' colspan="' + esc(cell.span) + '"' : "") + styleAttr(cell) + mark(cell);
|
|
384
381
|
// One table row: the row style on the `<tr>`, from which it inherits into
|
|
385
382
|
// the cells, `valign` included. The box is not among these names — a `<tr>`
|
|
386
|
-
// could not honour one, and the engine
|
|
387
|
-
// the cells, where it arrives as ordinary inline CSS.
|
|
383
|
+
// could not honour one, and the engine resolved that half onto the cells.
|
|
388
384
|
/** @type {(event: any, list: any[], tag: string, intl: any) => string} */
|
|
389
385
|
let tr = (event, list, tag, intl) =>
|
|
390
386
|
"<tr" + styleAttr(event) + ">" + cells(list, tag, attrs, intl, markup) + "</tr>";
|
|
@@ -394,8 +390,7 @@ export function html(options) {
|
|
|
394
390
|
let out = "";
|
|
395
391
|
// The finished `src` of every image this render has already encoded,
|
|
396
392
|
// keyed by the array the source expression yielded. A logo down a
|
|
397
|
-
// thousand rows is one base64 walk
|
|
398
|
-
// goes with the render — nothing here outlives it.
|
|
393
|
+
// thousand rows is one base64 walk. Nothing here outlives the render.
|
|
399
394
|
/** @type {Map<Uint8Array, string>} */
|
|
400
395
|
let sources = new Map();
|
|
401
396
|
// Whether a `total-row` already opened `<tfoot>` — markup
|
|
@@ -407,35 +402,16 @@ export function html(options) {
|
|
|
407
402
|
let rooted = false;
|
|
408
403
|
// The page columns a node declared: its `count`, the `owner` depth that
|
|
409
404
|
// declared it (-1 for the report root), and whether the container is
|
|
410
|
-
// emitted and still to be closed.
|
|
411
|
-
//
|
|
405
|
+
// emitted and still to be closed. Never more than one, because nesting a
|
|
406
|
+
// columned region is a definition error.
|
|
412
407
|
/** @type {{ count: number, owner: number, open: boolean } | null} */
|
|
413
408
|
let region = null;
|
|
414
409
|
// How many group instances are open, so the declaring node's own bands
|
|
415
|
-
// can be told from the content they bracket
|
|
416
|
-
//
|
|
417
|
-
// that may own the region. Taken from each event rather than counted, so
|
|
418
|
-
// there is one spelling of a depth the stream already carries.
|
|
410
|
+
// can be told from the content they bracket. Taken from each event
|
|
411
|
+
// rather than counted, so one spelling of a depth the stream carries.
|
|
419
412
|
let depth = 0;
|
|
420
|
-
// The split being filled, if any: the slot geometry the bracket carried
|
|
421
|
-
// and how many slots have been placed. Splits never nest, so one is
|
|
422
|
-
// enough. Null whenever the walk is not inside a bracket.
|
|
423
|
-
/** @type {{ slots: any[], at: number } | null} */
|
|
424
|
-
let split = null;
|
|
425
413
|
/** @type {{ locale?: string, currency?: string, timeZone?: string } | null} */
|
|
426
414
|
let intl = null;
|
|
427
|
-
// Place one item's markup: inside a bracket it goes in the next slot,
|
|
428
|
-
// and everywhere else it is a band item like any other.
|
|
429
|
-
/** @type {(event: any, fragment: string) => void} */
|
|
430
|
-
let emit = (event, fragment) => {
|
|
431
|
-
if (!split) {
|
|
432
|
-
content(event);
|
|
433
|
-
out += fragment;
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
out +=
|
|
437
|
-
'<div class="q-slot"' + slotAttr(split.slots[split.at++]) + ">" + fragment + "</div>";
|
|
438
|
-
};
|
|
439
415
|
// Does this event stay full-width, outside the columns? The declaring
|
|
440
416
|
// node's own bands do.
|
|
441
417
|
/** @type {(event: any) => boolean} */
|
|
@@ -468,14 +444,61 @@ export function html(options) {
|
|
|
468
444
|
if (fullWidth(event)) close();
|
|
469
445
|
else openColumns();
|
|
470
446
|
};
|
|
447
|
+
// One item's or picture's own markup, by event type: the stable
|
|
448
|
+
// `q-item q-<role>` classes are the whole class attribute (print CSS
|
|
449
|
+
// targets the band-role class for styling and breaks), and a picture is
|
|
450
|
+
// an item container of its own holding one `<img>`.
|
|
451
|
+
/** @type {Record<string, (event: any) => string>} */
|
|
452
|
+
let fragment = {
|
|
453
|
+
item: (event) => {
|
|
454
|
+
let slot = placed(event);
|
|
455
|
+
return (
|
|
456
|
+
'<div class="q-item q-' +
|
|
457
|
+
event.role +
|
|
458
|
+
'"' +
|
|
459
|
+
styleAttr(slot, slot.own) +
|
|
460
|
+
mark(event) +
|
|
461
|
+
">" +
|
|
462
|
+
markup(event.tokens, event.style, intl) +
|
|
463
|
+
"</div>"
|
|
464
|
+
);
|
|
465
|
+
},
|
|
466
|
+
image: (event) => {
|
|
467
|
+
let slot = placed(event);
|
|
468
|
+
let source = sources.get(event.bytes);
|
|
469
|
+
if (source === undefined)
|
|
470
|
+
sources.set(
|
|
471
|
+
event.bytes,
|
|
472
|
+
(source = esc("data:image/" + event.format + ";base64," + base64(event.bytes))),
|
|
473
|
+
);
|
|
474
|
+
return (
|
|
475
|
+
'<div class="q-item q-image q-' +
|
|
476
|
+
event.role +
|
|
477
|
+
'"' +
|
|
478
|
+
styleAttr(slot, justified(slot.own, event.style)) +
|
|
479
|
+
mark(event) +
|
|
480
|
+
'><img src="' +
|
|
481
|
+
source +
|
|
482
|
+
'" width="' +
|
|
483
|
+
esc(event.width) +
|
|
484
|
+
'" height="' +
|
|
485
|
+
esc(event.height) +
|
|
486
|
+
'" alt="' +
|
|
487
|
+
esc(text(event.alt || [])) +
|
|
488
|
+
'" style="' +
|
|
489
|
+
(event.fit === "width" ? "" : "max-") +
|
|
490
|
+
'width:100%;height:auto">' +
|
|
491
|
+
"</div>"
|
|
492
|
+
);
|
|
493
|
+
},
|
|
494
|
+
};
|
|
471
495
|
// Hidden items never reach the stream and a hidden cell arrives with no
|
|
472
496
|
// tokens, so no visibility checks are needed in any handler below.
|
|
473
|
-
await walk(stream(data), {
|
|
497
|
+
await walk(splits(stream(data)), {
|
|
474
498
|
// Nothing unless the render is unlicensed — then the fragment opens
|
|
475
|
-
// with the marking badge
|
|
476
|
-
//
|
|
477
|
-
//
|
|
478
|
-
// against.
|
|
499
|
+
// with the marking badge: the engine's wording in this target's
|
|
500
|
+
// element, escaped, so the badge stays childless whatever stream a
|
|
501
|
+
// host compiles against.
|
|
479
502
|
"report-start": (event) => {
|
|
480
503
|
intl = {
|
|
481
504
|
locale: event.locale,
|
|
@@ -485,12 +508,10 @@ export function html(options) {
|
|
|
485
508
|
if (event.marking) out += '<div class="q-unlicensed">' + esc(event.marking) + "</div>";
|
|
486
509
|
if (event.margin != null)
|
|
487
510
|
out += "<style>@page{margin:" + esc(event.margin) + "pt}</style>";
|
|
488
|
-
// The report root. Always emitted
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
//
|
|
492
|
-
// reach it, and a marking an author can shrink is one ADR 0002
|
|
493
|
-
// does not allow.
|
|
511
|
+
// The report root. Always emitted: the fragment otherwise has no
|
|
512
|
+
// element for a document-wide declaration to inherit from, and the
|
|
513
|
+
// shipped stylesheet's baseline hangs off it. The marking stays
|
|
514
|
+
// outside, where an author's `style` cannot reach it (ADR 0002).
|
|
494
515
|
out += '<div class="q-report"' + styleAttr(event) + ">";
|
|
495
516
|
rooted = true;
|
|
496
517
|
if (event.columns) region = { count: event.columns, owner: -1, open: false };
|
|
@@ -498,90 +519,54 @@ export function html(options) {
|
|
|
498
519
|
// The stable `q-item q-<role>` classes are the whole class attribute;
|
|
499
520
|
// print CSS targets the band-role class for styling and breaks.
|
|
500
521
|
item: (event) => {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
event,
|
|
504
|
-
'<div class="q-item q-' +
|
|
505
|
-
event.role +
|
|
506
|
-
'"' +
|
|
507
|
-
styleAttr(slot, slot.own) +
|
|
508
|
-
mark(event) +
|
|
509
|
-
">" +
|
|
510
|
-
markup(event.tokens, event.style, intl) +
|
|
511
|
-
"</div>",
|
|
512
|
-
);
|
|
522
|
+
content(event);
|
|
523
|
+
out += fragment.item(event);
|
|
513
524
|
},
|
|
514
525
|
// A split places its slots across the content width instead of down
|
|
515
|
-
// the band. The slot items
|
|
516
|
-
//
|
|
517
|
-
|
|
518
|
-
"split-start": (event) => {
|
|
526
|
+
// the band. The slot items keep their ordinary markup; only the
|
|
527
|
+
// wrapper around them is new.
|
|
528
|
+
split: (event) => {
|
|
519
529
|
content(event);
|
|
520
530
|
out +=
|
|
521
531
|
'<div class="q-split q-' + event.role + '"' + styleAttr(event, "display:flex") + ">";
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
532
|
+
event.items.forEach((/** @type {any} */ slot, /** @type {number} */ at) => {
|
|
533
|
+
out +=
|
|
534
|
+
'<div class="q-slot"' +
|
|
535
|
+
slotAttr(event.slots[at]) +
|
|
536
|
+
">" +
|
|
537
|
+
fragment[slot.type](slot) +
|
|
538
|
+
"</div>";
|
|
539
|
+
});
|
|
526
540
|
out += "</div>";
|
|
527
541
|
},
|
|
528
542
|
// An image is an item container of its own holding one `<img>`: the
|
|
529
543
|
// band-role class joins `q-item q-image`, `align`/`background` dress
|
|
530
|
-
// the container, and `fit` sizes the picture inside it.
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
// same escaped attribute path as everything else rather than a second
|
|
534
|
-
// one, and escaped when it is encoded rather than per occurrence. `alt` is report data, so it is the display-text join escaped,
|
|
535
|
-
// never the markup-passing join a cell body gets; an image without one
|
|
536
|
-
// carries `alt=""`, the decorative-image convention.
|
|
544
|
+
// the container, and `fit` sizes the picture inside it. `alt` is report
|
|
545
|
+
// data, so it takes the display-text join escaped, never the
|
|
546
|
+
// markup-passing join a cell body gets; without one it is `alt=""`.
|
|
537
547
|
//
|
|
538
|
-
// `width`/`height` are the event's natural size,
|
|
539
|
-
//
|
|
540
|
-
//
|
|
541
|
-
//
|
|
542
|
-
//
|
|
543
|
-
// used height and a picture capped narrower than its file renders
|
|
544
|
-
// squashed. SCHEMA.md, "The HTML target", states that normatively.
|
|
548
|
+
// `width`/`height` are the event's natural size, so a picture reserves
|
|
549
|
+
// its aspect ratio before it decodes. The pair is a presentational
|
|
550
|
+
// hint rather than a ratio, which is why the `fit` style carries
|
|
551
|
+
// `height:auto`: without it a picture capped narrower than its file
|
|
552
|
+
// renders squashed.
|
|
545
553
|
image: (event) => {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
if (source === undefined)
|
|
549
|
-
sources.set(
|
|
550
|
-
event.bytes,
|
|
551
|
-
(source = esc("data:image/" + event.format + ";base64," + base64(event.bytes))),
|
|
552
|
-
);
|
|
553
|
-
emit(
|
|
554
|
-
event,
|
|
555
|
-
'<div class="q-item q-image q-' +
|
|
556
|
-
event.role +
|
|
557
|
-
'"' +
|
|
558
|
-
styleAttr(slot, justified(slot.own, event.style)) +
|
|
559
|
-
mark(event) +
|
|
560
|
-
'><img src="' +
|
|
561
|
-
source +
|
|
562
|
-
'" width="' +
|
|
563
|
-
esc(event.width) +
|
|
564
|
-
'" height="' +
|
|
565
|
-
esc(event.height) +
|
|
566
|
-
'" alt="' +
|
|
567
|
-
esc(text(event.alt || [])) +
|
|
568
|
-
'" style="' +
|
|
569
|
-
(event.fit === "width" ? "" : "max-") +
|
|
570
|
-
'width:100%;height:auto">' +
|
|
571
|
-
"</div>",
|
|
572
|
-
);
|
|
554
|
+
content(event);
|
|
555
|
+
out += fragment.image(event);
|
|
573
556
|
},
|
|
574
|
-
// Each group instance is wrapped in a `q-group` container carrying
|
|
575
|
-
//
|
|
576
|
-
//
|
|
577
|
-
//
|
|
578
|
-
//
|
|
557
|
+
// Each group instance is wrapped in a `q-group` container carrying the
|
|
558
|
+
// group name, so print CSS can keep a header with its rows or force a
|
|
559
|
+
// break per group. The stream resolved the group's `break` position
|
|
560
|
+
// into this instance's two edges, each mapping to its class. Both ride
|
|
561
|
+
// the opening event: the container is written here and a class cannot
|
|
562
|
+
// be added to it later.
|
|
579
563
|
"group-start": (event) => {
|
|
580
564
|
content(event);
|
|
581
565
|
depth = event.depth + 1;
|
|
582
566
|
out +=
|
|
583
567
|
'<div class="q-group' +
|
|
584
|
-
(event.break
|
|
568
|
+
(event.break ? " q-break" : "") +
|
|
569
|
+
(event.breakAfter ? " q-break-after" : "") +
|
|
585
570
|
'"' +
|
|
586
571
|
mark(event) +
|
|
587
572
|
' data-group="' +
|
package/lib/style.css
CHANGED
|
@@ -105,7 +105,9 @@
|
|
|
105
105
|
* Fragmentation. SCHEMA.md calls these the print-CSS contract, so they ship
|
|
106
106
|
* with the look rather than being left to the host to rediscover: `thead`
|
|
107
107
|
* repeats per printed page, a table row stays whole, a group header travels
|
|
108
|
-
* with its rows, and a group
|
|
108
|
+
* with its rows, and each edge a group's `break` position turns gets one. The
|
|
109
|
+
* trailing edge needs its own rule: `break-before` cannot say what falls after
|
|
110
|
+
* the last instance of a run.
|
|
109
111
|
*
|
|
110
112
|
* Pages are not the only fragmentation context: the columns case lives in the
|
|
111
113
|
* shared block above, because a sheet with no pages can still have columns.
|
|
@@ -132,3 +134,7 @@
|
|
|
132
134
|
.q-break {
|
|
133
135
|
break-before: page;
|
|
134
136
|
}
|
|
137
|
+
|
|
138
|
+
.q-break-after {
|
|
139
|
+
break-after: page;
|
|
140
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quario/html",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "Tiny, escape-by-default HTML render target for quario. Semantic tables and a stable class contract.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"csp",
|
|
7
|
+
"escaping",
|
|
8
|
+
"html",
|
|
9
|
+
"quario",
|
|
10
|
+
"report"
|
|
11
|
+
],
|
|
5
12
|
"homepage": "https://getquario.com",
|
|
6
13
|
"license": "SEE LICENSE IN LICENSE",
|
|
7
14
|
"repository": {
|
|
@@ -39,12 +46,12 @@
|
|
|
39
46
|
"devDependencies": {
|
|
40
47
|
"@arethetypeswrong/cli": "^0.18.3",
|
|
41
48
|
"@size-limit/preset-small-lib": "^13.0.3",
|
|
42
|
-
"quario": "^0.
|
|
49
|
+
"quario": "^0.10.0",
|
|
43
50
|
"size-limit": "^13.0.3",
|
|
44
51
|
"typescript": "^7.0.2"
|
|
45
52
|
},
|
|
46
53
|
"peerDependencies": {
|
|
47
|
-
"quario": "^0.
|
|
54
|
+
"quario": "^0.10.0"
|
|
48
55
|
},
|
|
49
56
|
"size-limit": [
|
|
50
57
|
{
|