@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 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 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,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 })`); 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 and whose `width`/`height` are the picture's natural size; `fit` sets `max-width:100%` or `width:100%` on it, paired with `height:auto`, 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
@@ -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; the rule is a reference default a host overrides on source order.
127
- 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
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; 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
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 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)
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; 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
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
- **Every interpolated value is escaped.** `{{{ }}}` is a definition error, so no schema syntax can
153
- exempt a data value from escaping. Every generated attribute value is escaped too — inline styles, `data-` attributes, `src`, `alt`
154
- 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.
155
156
 
156
157
  Literal template text passes through verbatim as author-controlled markup. A definition is
157
- 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.
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, Prince; page numbers via `@page` margin
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; 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
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 showing
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. Free, unlimited, watermarked evaluation; per-developer
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; it is verified offline:
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) 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quario/html",
3
- "version": "0.8.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.8.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.8.0"
47
+ "quario": "^0.9.0"
48
48
  },
49
49
  "size-limit": [
50
50
  {