@quario/html 0.8.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 +45 -0
- package/README.md +31 -30
- package/lib/index.d.ts +7 -1
- package/lib/index.js +21 -6
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# @quario/html
|
|
2
2
|
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
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.
|
|
14
|
+
|
|
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.
|
|
18
|
+
|
|
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
|
+
|
|
3
48
|
## 0.8.0
|
|
4
49
|
|
|
5
50
|
### Minor Changes
|
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
|
|
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
|
|
81
|
-
`;` or `}`
|
|
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,25 +96,25 @@ 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 })`)
|
|
99
|
+
(`quario({ query, license })`). See the [engine README](https://www.npmjs.com/package/quario).
|
|
99
100
|
|
|
100
|
-
Rendering is asynchronous and
|
|
101
|
-
host. Compilation stays synchronous
|
|
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
|
|
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
|
|
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
|
|
@@ -123,15 +124,15 @@ Security Policy needs `img-src data:` (see
|
|
|
123
124
|
Style declarations map to inline CSS (`bold` → `font-weight:bold` or `font-weight:normal`, `size` → `font-size:<n>pt`,
|
|
124
125
|
`family: "mono"` → `font-family:monospace`, …). Occupy-a-line and newline-as-break are **not** inline: they hang off
|
|
125
126
|
`.q-item` and `.q-table th, td` in [`@quario/html/style.css`](#the-reference-stylesheet), the same way `.q-break`
|
|
126
|
-
honors `break: "page"`. The class is the hook
|
|
127
|
-
A fragment without that sheet still carries the classes and the text, including newlines
|
|
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
|
|
128
129
|
break until some stylesheet says so. This target supplies no defaults in the markup: no weight or size per band
|
|
129
130
|
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
|
|
131
|
+
in the cascade and force `!important` on you. The defaults and the honor rules ship as an ordinary stylesheet
|
|
131
132
|
instead. The PDF and XLSX targets carry theirs built in, because their consumers have no stylesheet. An empty
|
|
132
133
|
row set still emits the table, its header, and an empty `<tbody>`. A hidden table cell keeps its `<td>`, empty.
|
|
133
134
|
|
|
134
|
-
This target
|
|
135
|
+
This target paginates nothing: it ignores schema page bands (page furniture belongs to your print CSS)
|
|
135
136
|
and maps `break: "page"` and `reset: "page"` to the `q-break` class. Page columns are laid out:
|
|
136
137
|
a node declaring `columns` wraps the content between its own bands in the `q-columns` container
|
|
137
138
|
above, and the browser flows it.
|
|
@@ -144,17 +145,17 @@ with each declaration's fate in the
|
|
|
144
145
|
|
|
145
146
|
An unlicensed render opens the fragment with `<div class="q-unlicensed">` holding the wording
|
|
146
147
|
from `report-start.marking` as escaped text. A licensed render emits no badge. The element and its
|
|
147
|
-
position are normative
|
|
148
|
+
position are normative. Visual treatment belongs to the host. The shipped stylesheet leaves the
|
|
148
149
|
badge unstyled, because a watermark would escape the fragment's box onto the host page.
|
|
149
150
|
|
|
150
151
|
## Escaping
|
|
151
152
|
|
|
152
|
-
**
|
|
153
|
-
exempt a data value from escaping.
|
|
154
|
-
and `colspan
|
|
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.
|
|
155
156
|
|
|
156
157
|
Literal template text passes through verbatim as author-controlled markup. A definition is
|
|
157
|
-
trusted configuration. Its _data_ is
|
|
158
|
+
trusted configuration. Its _data_ is not, and data can never reach the document unescaped.
|
|
158
159
|
|
|
159
160
|
## Printing to PDF
|
|
160
161
|
|
|
@@ -174,7 +175,7 @@ Host CSS targets these classes:
|
|
|
174
175
|
`.q-break { break-before: page }` honors the schema's own break hint, and `thead` repeats per
|
|
175
176
|
printed page. Yours adds the page geometry in `@page`, which only you can decide.
|
|
176
177
|
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
|
|
178
|
+
footer template) or a Paged-CSS engine (WeasyPrint or Prince, with page numbers via `@page` margin
|
|
178
179
|
boxes).
|
|
179
180
|
|
|
180
181
|
### The reference stylesheet
|
|
@@ -194,13 +195,13 @@ const css = readFileSync(new URL(import.meta.resolve("@quario/html/style.css")),
|
|
|
194
195
|
const page = `<!doctype html><html><head><style>${css}</style></head><body>${fragment}</body></html>`;
|
|
195
196
|
```
|
|
196
197
|
|
|
197
|
-
Its **selectors** are contract
|
|
198
|
+
Its **selectors** are contract. Its **rules** are not. It is a reference default meant to be
|
|
198
199
|
overridden. Every rule is ordinary specificity, so your own stylesheet loaded after it wins
|
|
199
200
|
without `!important` anywhere. Link nothing at all and you get unstyled markup.
|
|
200
201
|
|
|
201
202
|
Two things it leaves to you: `@page` geometry and body type, which only a host can decide, and
|
|
202
203
|
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
|
|
204
|
+
would paint over your whole page. `example/print.css` in the repository is a host's half that does
|
|
204
205
|
both, and `example/print.js` is the complete pipeline.
|
|
205
206
|
|
|
206
207
|
## Documentation
|
|
@@ -212,10 +213,10 @@ specification of what a report may declare, and
|
|
|
212
213
|
|
|
213
214
|
## License
|
|
214
215
|
|
|
215
|
-
Commercial software with readable source.
|
|
216
|
-
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.
|
|
217
218
|
|
|
218
|
-
Pass your license key once, on the instance
|
|
219
|
+
Pass your license key once, on the instance. quario verifies it offline:
|
|
219
220
|
|
|
220
221
|
```js
|
|
221
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)
|
|
116
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quario/html",
|
|
3
|
-
"version": "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.
|
|
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.
|
|
47
|
+
"quario": "^0.9.0"
|
|
48
48
|
},
|
|
49
49
|
"size-limit": [
|
|
50
50
|
{
|