@quario/html 0.1.0 → 0.3.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 +126 -0
- package/README.md +32 -22
- package/lib/index.d.ts +18 -1
- package/lib/index.js +307 -76
- package/lib/style.css +33 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,132 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-09-02
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`format` stringifies at the markup edge.** The kind is presented through
|
|
15
|
+
the engine's `format()` helper from the instance locale, then escaped like
|
|
16
|
+
any other interpolated value.
|
|
17
|
+
|
|
18
|
+
- **`page.margin` becomes `@page{margin:<n>pt}`.** Report-header `height`
|
|
19
|
+
is unread: a CSS height would pin in the body box, not from the page top.
|
|
20
|
+
|
|
21
|
+
- **`spaceBefore` / `spaceAfter` map to `margin-top` / `margin-bottom`.**
|
|
22
|
+
Report and group containers are flex columns so adjacent authored gaps add
|
|
23
|
+
rather than collapse.
|
|
24
|
+
|
|
25
|
+
- **Padding and complete border sides map to inline CSS.**
|
|
26
|
+
`box-sizing:border-box` rides the same attribute. A side that does not
|
|
27
|
+
resolve all three names emits nothing, so a missing colour never becomes
|
|
28
|
+
the browser's solid black. Header-row, row, and total-row boxes are
|
|
29
|
+
withdrawn from `<tr>` — CSS does not box a table row — while other row
|
|
30
|
+
styles still inherit.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- **`<tfoot>` holds every total row.** Extra rows are extra `<tr>`; the
|
|
35
|
+
section still closes at `table-end`.
|
|
36
|
+
|
|
37
|
+
- **A visible text item occupies a line, and a literal newline is a break.**
|
|
38
|
+
Before, an empty item collapsed and a newline became a space. The honor
|
|
39
|
+
rules now hang off `.q-item` and `.q-table th, td` in the reference
|
|
40
|
+
stylesheet, the same pattern as `.q-break` — class is the hook, the rule
|
|
41
|
+
is overridable. Empty table cells still collapse. Host CSS still owns
|
|
42
|
+
leading; this is not `line-height: 1.4`.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- **A declared `false` flag now emits its CSS off-value.** `bold: false` used
|
|
47
|
+
to emit nothing, so the reference stylesheet's band-role `font-weight: bold`
|
|
48
|
+
still won on screen while PDF rendered regular. Exact `false` now writes
|
|
49
|
+
`font-weight:normal` (and the matching off-value for italic, uppercase, and
|
|
50
|
+
the decorations), which beats the sheet. Omit and a non-boolean result still
|
|
51
|
+
emit nothing.
|
|
52
|
+
|
|
53
|
+
## [0.2.0] - 2026-09-01
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
|
|
57
|
+
- **`html({ fonts })` says what a family name means on your page.** A report
|
|
58
|
+
declares the typeface it wants — `family: "Instrument Sans"`, or the
|
|
59
|
+
portable `family: "mono"` — and this option resolves that name to whatever
|
|
60
|
+
CSS means on the page it is displayed in:
|
|
61
|
+
`html({ fonts: { "Instrument Sans": "var(--font-instrument-sans)" } })`. Any
|
|
62
|
+
CSS `font-family` value works, so a custom property, a font stack or a
|
|
63
|
+
quoted name are all fine; a value containing `;` or `}` throws from `html()`
|
|
64
|
+
rather than at render. Names match case-insensitively, and `sans`, `serif`
|
|
65
|
+
and `mono` can be mapped like any other, which is what lets one definition
|
|
66
|
+
say "the mono one" and each surface answer for itself. Useful wherever your
|
|
67
|
+
faces are registered under names you do not control — a build tool that
|
|
68
|
+
hashes `@font-face` names, for instance. The mapping resolves families a
|
|
69
|
+
report declares; the face of text that declares none stays your
|
|
70
|
+
stylesheet's, so it is still one CSS rule away.
|
|
71
|
+
|
|
72
|
+
- **The report default rides a report root.** Every fragment is now wrapped in
|
|
73
|
+
one `<div class="q-report">`, and a report that declares a `style` carries it
|
|
74
|
+
there as an inline `font-family`/`font-size` — written once for the document
|
|
75
|
+
rather than repeated on every item, with CSS inheritance doing the rest. The
|
|
76
|
+
unlicensed marking stays outside the root, as the fragment's first element
|
|
77
|
+
and the root's sibling, so no authored style can reach it.
|
|
78
|
+
|
|
79
|
+
- **`uppercase` maps to `text-transform:uppercase`.** The markup carries the
|
|
80
|
+
text exactly as authored and only its rendering is capitalised, so what a
|
|
81
|
+
reader selects, copies, or hears from a screen reader is your own casing.
|
|
82
|
+
|
|
83
|
+
- **Splits render as a flex row of sized slots.** A
|
|
84
|
+
[split](https://getquario.com/docs) becomes
|
|
85
|
+
`<div class="q-split q-<role>">` carrying inline `display:flex`, with each
|
|
86
|
+
slot a `<div class="q-slot">` sized inline — `width:<n>%` for an authored
|
|
87
|
+
share, `flex:1` for a width-less one. The slot item inside keeps its
|
|
88
|
+
ordinary `q-item` container, so an item's markup never depends on where it
|
|
89
|
+
sits. Placement is emitted inline for the same reason page columns are: it
|
|
90
|
+
is document structure, not one of the visual defaults this target leaves to
|
|
91
|
+
your stylesheet.
|
|
92
|
+
- **`html({ paths: true })` maps rendered output back to the schema.** Each
|
|
93
|
+
element whose event carries a schema path gains `data-q-path`: item and
|
|
94
|
+
image containers, each group instance's container, the table, and every
|
|
95
|
+
`<th>`/`<td>` (the column definition's path; total cells their own entry).
|
|
96
|
+
One definition renders many times, so one path appears on every instance of
|
|
97
|
+
it. Escaped like every generated attribute, and off by default — the
|
|
98
|
+
attribute is weight a plain display pays for nothing.
|
|
99
|
+
|
|
100
|
+
### Changed
|
|
101
|
+
|
|
102
|
+
- **A family name that could not be written as CSS now falls back instead of
|
|
103
|
+
being rewritten.** A `family` carrying characters that cannot appear in a
|
|
104
|
+
family name was stripped down to whatever survived and emitted anyway, which
|
|
105
|
+
named a face that could not exist — `family: "var(--my-font)"` became
|
|
106
|
+
`font-family:'var--my-font'`. Such a name now contributes nothing, so the
|
|
107
|
+
text renders in the face above it: the report default, or your stylesheet's
|
|
108
|
+
baseline. Ordinary names are unaffected. To resolve a name your page knows
|
|
109
|
+
under a different one, map it with `html({ fonts })`.
|
|
110
|
+
|
|
111
|
+
- **The fragment now has a root element.** `<div class="q-report">` wraps every
|
|
112
|
+
render, whether or not a report declares a default. Host CSS that assumed
|
|
113
|
+
items and tables were the fragment's top-level children needs a descendant
|
|
114
|
+
selector; nothing else about the markup moved.
|
|
115
|
+
|
|
116
|
+
- **The reference stylesheet's baseline face moved onto `.q-report`.** It was
|
|
117
|
+
on `.q-item`, `.q-table` and `.q-unlicensed` — a direct rule, which would
|
|
118
|
+
have beaten the report default an author writes on the document. The marking
|
|
119
|
+
keeps its own rule, being outside the root. Same look, one rule up.
|
|
120
|
+
|
|
121
|
+
- **A bare `Date` escapes to ISO 8601 UTC, the same on every machine.**
|
|
122
|
+
Interpolated `Date` values rendered through `String(date)`, which bakes the
|
|
123
|
+
host's timezone and locale into the markup. They now render as
|
|
124
|
+
`toISOString()` text through the engine's shared display rule — escaped
|
|
125
|
+
like every other value — so the same report produces the same HTML
|
|
126
|
+
everywhere. Formatted dates keep coming from registered functions.
|
|
127
|
+
|
|
128
|
+
### Fixed
|
|
129
|
+
|
|
130
|
+
- **The reference stylesheet sets a base font family.** A fragment dropped
|
|
131
|
+
into a page with no font of its own inherited the browser default — a serif
|
|
132
|
+
— where every other target rendered a grotesque. `@quario/html/style.css`
|
|
133
|
+
now sets `sans-serif` on report text, as an ordinary rule any host overrides
|
|
134
|
+
by source order.
|
|
135
|
+
|
|
10
136
|
## [0.1.0] - 2026-08-27
|
|
11
137
|
|
|
12
138
|
### Added
|
package/README.md
CHANGED
|
@@ -47,16 +47,18 @@ const page = await report.render(html(), data);
|
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
```html
|
|
50
|
-
<div class="q-
|
|
51
|
-
<div class="q-
|
|
52
|
-
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
50
|
+
<div class="q-report">
|
|
51
|
+
<div class="q-group" data-group="region">
|
|
52
|
+
<div class="q-item q-group-header" style="font-weight:bold">North</div>
|
|
53
|
+
<table class="q-table">
|
|
54
|
+
<colgroup><col><col></colgroup>
|
|
55
|
+
<thead><tr><th>Product</th><th>Amount</th></tr></thead>
|
|
56
|
+
<tbody><tr><td>Desk</td><td style="text-align:right">$500.00</td></tr></tbody>
|
|
57
|
+
</table>
|
|
58
|
+
<div class="q-item q-group-footer">Subtotal $500.00</div>
|
|
59
|
+
</div>
|
|
60
|
+
<div class="q-item q-report-footer">Grand total $500.00</div>
|
|
58
61
|
</div>
|
|
59
|
-
<div class="q-item q-report-footer">Grand total $500.00</div>
|
|
60
62
|
```
|
|
61
63
|
|
|
62
64
|
The output is a **fragment**: no `<html>`, no `<head>`, no styles of its own. Wrap it in your own
|
|
@@ -64,12 +66,16 @@ page shell and stylesheet.
|
|
|
64
66
|
|
|
65
67
|
## API
|
|
66
68
|
|
|
67
|
-
### `html()`
|
|
69
|
+
### `html(options?)`
|
|
68
70
|
|
|
69
|
-
The target factory
|
|
71
|
+
The target factory returns the target you pass to `render`.
|
|
70
72
|
`report()` compiles once and `report.render(html(), data)` resolves the fragment. Compile at
|
|
71
73
|
startup and render per request. Definition problems throw at `report()`, at compile time.
|
|
72
74
|
|
|
75
|
+
One option: `{ paths: true }` stamps `data-q-path="<schema path>"` on each element whose event
|
|
76
|
+
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
|
+
|
|
73
79
|
```js
|
|
74
80
|
const report = quario().report(schema, funcs);
|
|
75
81
|
const page = await report.render(html(), data);
|
|
@@ -97,20 +103,22 @@ change.
|
|
|
97
103
|
| `<div class="q-group" data-group="name">` | Each group instance, wrapping its header, nested content, and footer. Adds `q-break` when the group declares `break: "page"` or `reset: "page"` |
|
|
98
104
|
| `<table class="q-table">` | Each table, with a real `colgroup`, `thead`, `tbody`, and a `tfoot` when totals are declared |
|
|
99
105
|
| `<div class="q-item q-image q-<role>">` | Each image item, holding one `<img>` whose `src` is a base64 `data:` URI of the event's bytes; the rendered `alt` is escaped |
|
|
106
|
+
| `<div class="q-split q-<role>">` | Each split, carrying inline `display:flex`; its slots are `<div class="q-slot">` sized inline, each holding the slot item's ordinary container |
|
|
100
107
|
|
|
101
108
|
A column `width` becomes an inline `width:<n>%` on its `<col>`. The classes above belong to this
|
|
102
109
|
target. The schema never speaks in CSS. Displaying a fragment that contains images under a Content
|
|
103
110
|
Security Policy needs `img-src data:`.
|
|
104
111
|
|
|
105
|
-
Style declarations map to inline CSS (`bold` → `font-weight:bold`, `size` → `font-size:<n>pt`,
|
|
106
|
-
`family: "mono"` → `font-family:monospace`, …).
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
112
|
+
Style declarations map to inline CSS (`bold` → `font-weight:bold` or `font-weight:normal`, `size` → `font-size:<n>pt`,
|
|
113
|
+
`family: "mono"` → `font-family:monospace`, …). Occupy-a-line and newline-as-break are **not** inline: they hang off
|
|
114
|
+
`.q-item` and `.q-table th, td` in [`@quario/html/style.css`](#the-reference-stylesheet), the same way `.q-break`
|
|
115
|
+
honors `break: "page"`. The class is the hook; the rule is a reference default a host overrides on source order.
|
|
116
|
+
A fragment without that sheet still carries the classes and the text, including newlines; it does not occupy or
|
|
117
|
+
break until some stylesheet says so. This target supplies no defaults in the markup: no weight or size per band
|
|
118
|
+
role, no leading, no padding, no spacing between bands, no borders. An inline `style` attribute would beat yours
|
|
119
|
+
in the cascade and force `!important` on you; the defaults and the honor rules ship as an ordinary stylesheet
|
|
120
|
+
instead. The PDF and XLSX targets carry theirs built in, because their consumers have no stylesheet. An empty
|
|
121
|
+
row set still emits the table, its header, and an empty `<tbody>`. A hidden table cell keeps its `<td>`, empty.
|
|
114
122
|
|
|
115
123
|
This target is unpaginated: it ignores schema page bands (page furniture belongs to your print CSS)
|
|
116
124
|
and maps `break: "page"` and `reset: "page"` to the `q-break` class. Page columns are accepted on
|
|
@@ -144,6 +152,7 @@ Host CSS targets these classes:
|
|
|
144
152
|
1. Render the fragment and wrap it in a document with two stylesheets inlined, in order:
|
|
145
153
|
`@quario/html/style.css` below, then your own.
|
|
146
154
|
2. The shipped sheet already puts band behavior on the emitted classes:
|
|
155
|
+
`.q-item { min-height: 1lh; white-space: pre-line }` occupies a line and breaks on newlines,
|
|
147
156
|
`.q-group { break-inside: avoid }` keeps a group header with its rows,
|
|
148
157
|
`.q-break { break-before: page }` honors the schema's own break hint, and `thead` repeats per
|
|
149
158
|
printed page. Yours adds the page geometry in `@page`, which only you can decide.
|
|
@@ -154,8 +163,9 @@ Host CSS targets these classes:
|
|
|
154
163
|
### The reference stylesheet
|
|
155
164
|
|
|
156
165
|
The package ships the default look as a real stylesheet, `@quario/html/style.css`: the band-role
|
|
157
|
-
weights and sizes, the table look, and
|
|
158
|
-
|
|
166
|
+
weights and sizes, the table look, occupy and newline-as-break on `.q-item` and table cells, and
|
|
167
|
+
the four pagination rules above. Link it, import it through a bundler, or read and inline it.
|
|
168
|
+
Plain Node cannot `import` a `.css` file, so on the server:
|
|
159
169
|
|
|
160
170
|
```js
|
|
161
171
|
import { readFileSync } from "node:fs";
|
package/lib/index.d.ts
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
import type { Target } from "quario";
|
|
2
2
|
|
|
3
|
+
export interface HtmlOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Emit `data-q-path` on each element whose event carries a schema `path`,
|
|
6
|
+
* mapping rendered output back to the definition behind it — the editor's
|
|
7
|
+
* selection seam. Off by default.
|
|
8
|
+
*/
|
|
9
|
+
paths?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* This target's font mapping: a declared `family` name to a CSS
|
|
12
|
+
* `font-family` value — `{ "Instrument Sans": "var(--font-instrument-sans)" }`.
|
|
13
|
+
* Names match case-insensitively, and the generics `sans` / `serif` / `mono`
|
|
14
|
+
* may be mapped like any other. A value containing `;` or `}` throws from
|
|
15
|
+
* `html()`. See SCHEMA.md, "The HTML target" → Fonts.
|
|
16
|
+
*/
|
|
17
|
+
fonts?: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
|
|
3
20
|
/**
|
|
4
21
|
* The HTML render target:
|
|
5
22
|
* `quario().report(schema).render(html(), data)` resolves the fragment
|
|
6
23
|
* described in SCHEMA.md ("The HTML target").
|
|
7
24
|
*/
|
|
8
|
-
export function html(): Target<"html", Promise<string>>;
|
|
25
|
+
export function html(options?: HtmlOptions): Target<"html", Promise<string>>;
|
package/lib/index.js
CHANGED
|
@@ -9,24 +9,30 @@
|
|
|
9
9
|
* never carries markup-escaped text.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { isReportBand, text, walk } from "quario";
|
|
12
|
+
import { display, format, isReportBand, text, walk } from "quario";
|
|
13
13
|
|
|
14
14
|
/** @type {Record<string, string>} */
|
|
15
15
|
let ESC = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
16
16
|
/** @type {(value: any) => string} */
|
|
17
|
-
let esc = (value) =>
|
|
17
|
+
let esc = (value) => display(value).replace(/[&<>"']/g, (char) => ESC[char]);
|
|
18
18
|
|
|
19
19
|
// Join a cell's tokens to HTML: literal template text is author-controlled
|
|
20
20
|
// markup and passes verbatim; every interpolated value is escaped, no
|
|
21
21
|
// exceptions. The engine's own `text` join is the other one -- display text,
|
|
22
22
|
// where a literal is words rather than markup -- and an attribute value takes
|
|
23
23
|
// that one, escaped whole.
|
|
24
|
-
/** @type {(tokens: any[]) => string} */
|
|
25
|
-
let markup = (tokens) => {
|
|
24
|
+
/** @type {(tokens: any[], style?: any, intl?: any) => string} */
|
|
25
|
+
let markup = (tokens, style, intl) => {
|
|
26
26
|
let out = "";
|
|
27
|
-
for (let token of tokens)
|
|
27
|
+
for (let token of tokens)
|
|
28
|
+
out += "literal" in token ? token.literal : esc(shown(token.value, style, intl));
|
|
28
29
|
return out;
|
|
29
30
|
};
|
|
31
|
+
/** @type {(value: any, style: any, intl: any) => any} */
|
|
32
|
+
let shown = (value, style, intl) => {
|
|
33
|
+
let text = format(value, style?.format, intl);
|
|
34
|
+
return text != null ? text : value;
|
|
35
|
+
};
|
|
30
36
|
|
|
31
37
|
// base64 for an image's `data:` URI, without Buffer: this package runs in Node
|
|
32
38
|
// and in ES2024 browsers off the same `lib/`, and `btoa` is a standard global
|
|
@@ -47,9 +53,13 @@ let base64 = (bytes) => {
|
|
|
47
53
|
// smuggle CSS of its own into the attribute. Unknown names are ignored.
|
|
48
54
|
/** @type {Record<string, string>} */
|
|
49
55
|
let FAMILY = { sans: "sans-serif", serif: "serif", mono: "monospace" };
|
|
50
|
-
/** @type {(value: any) => boolean} */
|
|
51
|
-
let finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
52
56
|
let HEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
57
|
+
// A family name usable as it stands. Anything else would have to be rewritten
|
|
58
|
+
// to be emitted, and a rewritten name names a face that cannot exist.
|
|
59
|
+
let NAMEABLE = /^[A-Za-z0-9 _-]+$/;
|
|
60
|
+
// What a host's CSS value may not carry: `;` is what would turn one option
|
|
61
|
+
// into two declarations.
|
|
62
|
+
let UNSAFE = /[;}]/;
|
|
53
63
|
/** @type {(value: any) => boolean} */
|
|
54
64
|
let isHex = (value) => typeof value === "string" && HEX.test(value);
|
|
55
65
|
let ALIGNMENTS = ["left", "center", "right"];
|
|
@@ -58,41 +68,118 @@ let ALIGNMENTS = ["left", "center", "right"];
|
|
|
58
68
|
// that instance is the node which declared the count — an inner instance's are
|
|
59
69
|
// content like anything else. The report's own bands are the engine's to name.
|
|
60
70
|
let OWN = new Set(["group-header", "group-footer"]);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
// A host font mapping (`html({ fonts })`), lowercased once so lookup matches
|
|
72
|
+
// `pdf({ fonts })`'s. Validated here rather than at render, because a
|
|
73
|
+
// malformed one is host configuration and belongs where the host wrote it --
|
|
74
|
+
// the value itself is trusted and emitted verbatim, as that target's font
|
|
75
|
+
// bytes are. `Error` and the `options.fonts.<name>:` prefix are that target's
|
|
76
|
+
// too: one option, one message shape.
|
|
77
|
+
/** @type {(path: string, value: any) => string} */
|
|
78
|
+
let cssValue = (path, value) => {
|
|
79
|
+
if (typeof value !== "string" || !value) throw Error(path + ": expected a CSS font-family value");
|
|
80
|
+
if (UNSAFE.test(value)) throw Error(path + ": cannot contain ';' or '}'");
|
|
81
|
+
return value;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** @type {(fonts: any) => Record<string, string>} */
|
|
85
|
+
let mapping = (fonts) => {
|
|
86
|
+
if (fonts == null) return {};
|
|
87
|
+
if (typeof fonts !== "object")
|
|
88
|
+
throw Error("options.fonts: expected an object of family names to CSS values");
|
|
89
|
+
return Object.fromEntries(
|
|
90
|
+
Object.entries(fonts).map(([name, value]) => [
|
|
91
|
+
name.toLowerCase(),
|
|
92
|
+
cssValue("options.fonts." + name, value),
|
|
93
|
+
]),
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Resolve a declared family name against the table the factory built -- the
|
|
98
|
+
// host's mapping laid over the three generics -- then the name itself. A name
|
|
99
|
+
// that is not nameable as it stands resolves to nothing rather than to a
|
|
100
|
+
// rewritten near-miss, so the element falls through to the layer below instead
|
|
101
|
+
// of naming a face that cannot exist (docs/adr/0034).
|
|
102
|
+
/** @type {(map: Record<string, string>, value: string) => string} */
|
|
103
|
+
let named = (map, value) => {
|
|
64
104
|
// Own-key lookup: `constructor` must not resolve an inherited member.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return
|
|
105
|
+
let key = value.toLowerCase();
|
|
106
|
+
if (Object.hasOwn(map, key)) return map[key];
|
|
107
|
+
return NAMEABLE.test(value) ? "'" + value + "'" : "";
|
|
68
108
|
};
|
|
69
|
-
|
|
109
|
+
// Exact true/false only: a computed value of the wrong shape contributes
|
|
110
|
+
// nothing, same as omit (SCHEMA.md, Style declarations).
|
|
111
|
+
/** @type {(on: string, off: string) => (value: any) => string} */
|
|
112
|
+
let flag = (on, off) => (value) => (value === true ? on : value === false ? off : "");
|
|
113
|
+
/** @type {Record<string, (value: any, map: Record<string, string>) => string>} */
|
|
70
114
|
let CSS = {
|
|
71
|
-
family: (value) => {
|
|
115
|
+
family: (value, map) => {
|
|
72
116
|
if (typeof value !== "string" || !value) return "";
|
|
73
|
-
let name = named(value);
|
|
117
|
+
let name = named(map, value);
|
|
74
118
|
return name ? "font-family:" + name : "";
|
|
75
119
|
},
|
|
76
|
-
size: (value) => (
|
|
77
|
-
bold: (
|
|
78
|
-
italic: (
|
|
120
|
+
size: (value) => (Number.isFinite(value) && value > 0 ? "font-size:" + value + "pt" : ""),
|
|
121
|
+
bold: flag("font-weight:bold", "font-weight:normal"),
|
|
122
|
+
italic: flag("font-style:italic", "font-style:normal"),
|
|
79
123
|
color: (value) => (isHex(value) ? "color:" + value : ""),
|
|
80
124
|
background: (value) => (isHex(value) ? "background-color:" + value : ""),
|
|
81
125
|
align: (value) => (ALIGNMENTS.includes(value) ? "text-align:" + value : ""),
|
|
126
|
+
// The text itself is untouched, so selection and screen readers get what
|
|
127
|
+
// the author wrote; only its rendering is capitalised.
|
|
128
|
+
uppercase: flag("text-transform:uppercase", "text-transform:none"),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
let SIDES = ["Top", "Right", "Bottom", "Left"];
|
|
132
|
+
let LINES = ["solid", "dashed", "dotted"];
|
|
133
|
+
/** @type {(value: any) => boolean} */
|
|
134
|
+
let isPad = (value) => Number.isFinite(value) && value >= 0;
|
|
135
|
+
for (let side of SIDES) {
|
|
136
|
+
let edge = side.toLowerCase();
|
|
137
|
+
CSS["padding" + side] = (value) => (isPad(value) ? "padding-" + edge + ":" + value + "pt" : "");
|
|
138
|
+
}
|
|
139
|
+
CSS.spaceBefore = (value) => (isPad(value) ? "margin-top:" + value + "pt" : "");
|
|
140
|
+
CSS.spaceAfter = (value) => (isPad(value) ? "margin-bottom:" + value + "pt" : "");
|
|
141
|
+
|
|
142
|
+
/** @type {(width: any) => boolean} */
|
|
143
|
+
let isStroke = (width) => Number.isFinite(width) && width > 0;
|
|
144
|
+
|
|
145
|
+
/** @type {(style: any, side: string) => string[]} */
|
|
146
|
+
let borderSide = (style, side) => {
|
|
147
|
+
let width = style["border" + side + "Width"];
|
|
148
|
+
let line = style["border" + side + "Style"];
|
|
149
|
+
let color = style["border" + side + "Color"];
|
|
150
|
+
if (!isStroke(width) || !LINES.includes(line) || !isHex(color)) return [];
|
|
151
|
+
let edge = side.toLowerCase();
|
|
152
|
+
return [
|
|
153
|
+
"border-" + edge + "-width:" + width + "pt",
|
|
154
|
+
"border-" + edge + "-style:" + line,
|
|
155
|
+
"border-" + edge + "-color:" + color,
|
|
156
|
+
];
|
|
82
157
|
};
|
|
83
158
|
|
|
84
|
-
/** @type {(name: string
|
|
85
|
-
let
|
|
86
|
-
|
|
87
|
-
|
|
159
|
+
/** @type {(name: string) => boolean} */
|
|
160
|
+
let isBox = (name) => name.startsWith("padding") || name.startsWith("border");
|
|
161
|
+
|
|
162
|
+
/** @type {(style: any) => any} */
|
|
163
|
+
let unbox = (style) => {
|
|
164
|
+
if (!style) return;
|
|
165
|
+
let names = Object.keys(style).filter((name) => !isBox(name));
|
|
166
|
+
if (!names.length) return;
|
|
167
|
+
return Object.fromEntries(names.map((name) => [name, style[name]]));
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/** @type {(style: any) => string[]} */
|
|
171
|
+
let decoLine = (style) => {
|
|
172
|
+
let parts = [];
|
|
173
|
+
if (style.underline === true) parts.push("underline");
|
|
174
|
+
if (style.strikethrough === true) parts.push("line-through");
|
|
175
|
+
return parts;
|
|
88
176
|
};
|
|
89
177
|
|
|
90
178
|
/** @type {(style: any) => string} */
|
|
91
179
|
let decoration = (style) => {
|
|
92
|
-
let parts =
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
return parts.length ? "text-decoration:" + parts.join(" ") : "";
|
|
180
|
+
let parts = decoLine(style);
|
|
181
|
+
if (parts.length) return "text-decoration:" + parts.join(" ");
|
|
182
|
+
return style.underline === false || style.strikethrough === false ? "text-decoration:none" : "";
|
|
96
183
|
};
|
|
97
184
|
|
|
98
185
|
/** @type {(name: string) => boolean} */
|
|
@@ -106,37 +193,88 @@ let pushDecoration = (out, style, seen) => {
|
|
|
106
193
|
if (deco) out.push(deco);
|
|
107
194
|
};
|
|
108
195
|
|
|
109
|
-
/** @type {(out: string[], name: string, value: any) => void} */
|
|
110
|
-
let pushDecl = (out, name, value) => {
|
|
111
|
-
|
|
196
|
+
/** @type {(out: string[], name: string, value: any, map: Record<string, string>) => void} */
|
|
197
|
+
let pushDecl = (out, name, value, map) => {
|
|
198
|
+
if (value == null || !Object.hasOwn(CSS, name)) return;
|
|
199
|
+
let declaration = CSS[name](value, map);
|
|
112
200
|
if (declaration) out.push(declaration);
|
|
113
201
|
};
|
|
114
202
|
|
|
203
|
+
/** @type {(out: string[], style: any, seen: { box: boolean, sizing: boolean }) => void} */
|
|
204
|
+
let pushBox = (out, style, seen) => {
|
|
205
|
+
if (seen.box) return;
|
|
206
|
+
seen.box = true;
|
|
207
|
+
let start = out.length;
|
|
208
|
+
for (let side of SIDES) out.push(...borderSide(style, side));
|
|
209
|
+
if (out.length > start) seen.sizing = true;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/** @type {(seen: { sizing: boolean }, name: string, added: boolean) => void} */
|
|
213
|
+
let notePad = (seen, name, added) => {
|
|
214
|
+
if (added && name.startsWith("padding")) seen.sizing = true;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/** @type {(out: string[], name: string, style: any, seen: { decoration: boolean, box: boolean, sizing: boolean }, map: Record<string, string>) => void} */
|
|
218
|
+
let emitDecl = (out, name, style, seen, map) => {
|
|
219
|
+
if (isDecoration(name)) return pushDecoration(out, style, seen);
|
|
220
|
+
if (name.startsWith("border")) return pushBox(out, style, seen);
|
|
221
|
+
let start = out.length;
|
|
222
|
+
pushDecl(out, name, style[name], map);
|
|
223
|
+
notePad(seen, name, out.length > start);
|
|
224
|
+
};
|
|
225
|
+
|
|
115
226
|
// Serialize a resolved style object to inline CSS, in declaration order.
|
|
116
227
|
// underline and strikethrough share one CSS property, so the first of either
|
|
117
|
-
// in authored order emits the composed value and the second is skipped.
|
|
118
|
-
|
|
119
|
-
|
|
228
|
+
// in authored order emits the composed value and the second is skipped. A
|
|
229
|
+
// border side emits only when width, style and colour all resolve, so a
|
|
230
|
+
// missing name never becomes a browser-default solid black stroke.
|
|
231
|
+
/** @type {(style: any, map: Record<string, string>) => string} */
|
|
232
|
+
let css = (style, map) => {
|
|
120
233
|
let out = /** @type {string[]} */ ([]);
|
|
121
|
-
let seen = { decoration: false };
|
|
122
|
-
for (let name in style)
|
|
123
|
-
|
|
124
|
-
else pushDecl(out, name, style[name]);
|
|
125
|
-
}
|
|
234
|
+
let seen = { decoration: false, box: false, sizing: false };
|
|
235
|
+
for (let name in style) emitDecl(out, name, style, seen, map);
|
|
236
|
+
if (seen.sizing) out.push("box-sizing:border-box");
|
|
126
237
|
return out.join(";");
|
|
127
238
|
};
|
|
128
239
|
|
|
129
|
-
// Compose a resolved style into one escaped inline style attribute.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
240
|
+
// Compose a resolved style into one escaped inline style attribute. `own` is
|
|
241
|
+
// this target's own structural CSS, which goes first and always emits: page
|
|
242
|
+
// columns and splits are placement, not the visual defaults this target
|
|
243
|
+
// refuses to supply, so they ride the same escaped attribute path as
|
|
244
|
+
// everything else rather than a second one (CLAUDE.md, the markup edge).
|
|
245
|
+
/** @type {(fonts: any) => (event: { style?: any }, own?: string) => string} */
|
|
246
|
+
let styling = (fonts) => {
|
|
247
|
+
// The spread is the precedence the option promises: a host mapping is
|
|
248
|
+
// consulted before the three generics, and may replace one. `mapping`
|
|
249
|
+
// lowercased its keys to match the ones already here.
|
|
250
|
+
let map = { ...FAMILY, ...mapping(fonts) };
|
|
251
|
+
return ({ style }, own = "") => {
|
|
252
|
+
let out = [own, style && css(style, map)].filter(Boolean).join(";");
|
|
253
|
+
return out ? ' style="' + esc(out) + '"' : "";
|
|
254
|
+
};
|
|
134
255
|
};
|
|
135
256
|
|
|
136
|
-
|
|
137
|
-
|
|
257
|
+
// One slot's wrapper. An authored share is that width; a width-less slot
|
|
258
|
+
// divides what the sized ones leave, exactly as a width-less column does.
|
|
259
|
+
/** @type {(slot: { width?: number } | undefined) => string} */
|
|
260
|
+
let slotAttr = (slot) =>
|
|
261
|
+
' style="' + esc(slot && slot.width != null ? "width:" + slot.width + "%" : "flex:1") + '"';
|
|
262
|
+
|
|
263
|
+
// The identity attribute, escaped like every other generated value at this
|
|
264
|
+
// edge. `mark` is the factory's verdict: a render without `paths` maps every
|
|
265
|
+
// event to "" through the same function rather than re-deciding per call site.
|
|
266
|
+
/** @type {(event: { path?: string }) => string} */
|
|
267
|
+
let pathAttr = ({ path }) => (path ? ' data-q-path="' + esc(path) + '"' : "");
|
|
268
|
+
/** @type {() => string} */
|
|
269
|
+
let noAttr = () => "";
|
|
270
|
+
|
|
271
|
+
/** @type {(list: any[], tag: string, attrs: (event: any) => string, intl: any) => string} */
|
|
272
|
+
let cells = (list, tag, attrs, intl) =>
|
|
138
273
|
list
|
|
139
|
-
.map(
|
|
274
|
+
.map(
|
|
275
|
+
(cell) =>
|
|
276
|
+
"<" + tag + attrs(cell) + ">" + markup(cell.tokens, cell.style, intl) + "</" + tag + ">",
|
|
277
|
+
)
|
|
140
278
|
.join("");
|
|
141
279
|
|
|
142
280
|
/**
|
|
@@ -149,10 +287,31 @@ let cells = (list, tag) =>
|
|
|
149
287
|
* fragment, so `report-start.page` band closures are ignored — page furniture
|
|
150
288
|
* belongs to the host's print CSS.
|
|
151
289
|
*
|
|
290
|
+
* `paths` opts the fragment into identity: each element whose event carries a
|
|
291
|
+
* schema `path` gains `data-q-path`, so a consumer can map rendered output
|
|
292
|
+
* back to the definition behind it — the editor's selection seam. Off by
|
|
293
|
+
* default, because the attribute is weight every plain display pays for
|
|
294
|
+
* nothing.
|
|
295
|
+
*
|
|
296
|
+
* `fonts` is this target's font mapping: a declared `family` name to a CSS
|
|
297
|
+
* value, so a host says what the name means on this page. Malformed values
|
|
298
|
+
* throw from here (docs/adr/0034).
|
|
299
|
+
*
|
|
300
|
+
* The options are described once, in the hand-written public declarations,
|
|
301
|
+
* and read back here -- a second copy in JSDoc is a copy that drifts.
|
|
302
|
+
* @import { HtmlOptions } from './index.d.ts'
|
|
303
|
+
*
|
|
304
|
+
* @param {HtmlOptions} [options] Host options for this target.
|
|
152
305
|
* @returns {{name: "html", compile: (stream: any) => (data?: any) => Promise<string>}}
|
|
153
306
|
* The target (see SCHEMA.md, "Instances and targets").
|
|
154
307
|
*/
|
|
155
|
-
export function html() {
|
|
308
|
+
export function html(options) {
|
|
309
|
+
let mark = options?.paths ? pathAttr : noAttr;
|
|
310
|
+
// The factory's verdict, like `mark`: the mapping is validated once, here.
|
|
311
|
+
let styleAttr = styling(options?.fonts);
|
|
312
|
+
// The pair every cell carries, bound once rather than threaded as two.
|
|
313
|
+
/** @type {(cell: any) => string} */
|
|
314
|
+
let attrs = (cell) => styleAttr(cell) + mark(cell);
|
|
156
315
|
return {
|
|
157
316
|
name: "html",
|
|
158
317
|
compile: (stream) => async (data) => {
|
|
@@ -163,9 +322,13 @@ export function html() {
|
|
|
163
322
|
// goes with the render — nothing here outlives it.
|
|
164
323
|
/** @type {Map<Uint8Array, string>} */
|
|
165
324
|
let sources = new Map();
|
|
166
|
-
// Whether a `total-row` already
|
|
325
|
+
// Whether a `total-row` already opened `<tfoot>` — markup
|
|
167
326
|
// bookkeeping that stays at this edge, out of the event contract.
|
|
168
327
|
let tfoot = false;
|
|
328
|
+
// Whether `report-start` opened the report root. A stream may start
|
|
329
|
+
// part-way through (the walk driver reads an event's type and nothing
|
|
330
|
+
// else), and an unbalanced closing tag would be this target's own bug.
|
|
331
|
+
let rooted = false;
|
|
169
332
|
// The page columns a node declared: its `count`, the `owner` depth that
|
|
170
333
|
// declared it (-1 for the report root), and whether the container is
|
|
171
334
|
// emitted and still to be closed. Null when no node owes one. Never more
|
|
@@ -178,6 +341,25 @@ export function html() {
|
|
|
178
341
|
// that may own the region. Taken from each event rather than counted, so
|
|
179
342
|
// there is one spelling of a depth the stream already carries.
|
|
180
343
|
let depth = 0;
|
|
344
|
+
// The split being filled, if any: the slot geometry the bracket carried
|
|
345
|
+
// and how many slots have been placed. Splits never nest, so one is
|
|
346
|
+
// enough. Null whenever the walk is not inside a bracket.
|
|
347
|
+
/** @type {{ slots: any[], at: number } | null} */
|
|
348
|
+
let split = null;
|
|
349
|
+
/** @type {{ locale?: string, currency?: string, timeZone?: string } | null} */
|
|
350
|
+
let intl = null;
|
|
351
|
+
// Place one item's markup: inside a bracket it goes in the next slot,
|
|
352
|
+
// and everywhere else it is a band item like any other.
|
|
353
|
+
/** @type {(event: any, fragment: string) => void} */
|
|
354
|
+
let emit = (event, fragment) => {
|
|
355
|
+
if (!split) {
|
|
356
|
+
content(event);
|
|
357
|
+
out += fragment;
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
out +=
|
|
361
|
+
'<div class="q-slot"' + slotAttr(split.slots[split.at++]) + ">" + fragment + "</div>";
|
|
362
|
+
};
|
|
181
363
|
// Does this event stay full-width, outside the columns? The declaring
|
|
182
364
|
// node's own bands do.
|
|
183
365
|
/** @type {(event: any) => boolean} */
|
|
@@ -219,21 +401,48 @@ export function html() {
|
|
|
219
401
|
// edge, so the badge stays childless whatever stream a host compiles
|
|
220
402
|
// against.
|
|
221
403
|
"report-start": (event) => {
|
|
404
|
+
intl = { locale: event.locale, currency: event.currency, timeZone: event.timeZone };
|
|
222
405
|
if (event.marking) out += '<div class="q-unlicensed">' + esc(event.marking) + "</div>";
|
|
406
|
+
if (event.margin != null)
|
|
407
|
+
out += "<style>@page{margin:" + esc(event.margin) + "pt}</style>";
|
|
408
|
+
// The report root. Always emitted, because the fragment otherwise
|
|
409
|
+
// has no element for a document-wide declaration to be inherited
|
|
410
|
+
// from, and the shipped stylesheet's own baseline now hangs off it.
|
|
411
|
+
// The marking stays outside: inside, an author's `style` would
|
|
412
|
+
// reach it, and a marking an author can shrink is one ADR 0002
|
|
413
|
+
// does not allow.
|
|
414
|
+
out += '<div class="q-report"' + styleAttr(event) + ">";
|
|
415
|
+
rooted = true;
|
|
223
416
|
if (event.columns) region = { count: event.columns, owner: -1, open: false };
|
|
224
417
|
},
|
|
225
418
|
// The stable `q-item q-<role>` classes are the whole class attribute;
|
|
226
419
|
// print CSS targets the band-role class for styling and breaks.
|
|
227
420
|
item: (event) => {
|
|
421
|
+
emit(
|
|
422
|
+
event,
|
|
423
|
+
'<div class="q-item q-' +
|
|
424
|
+
event.role +
|
|
425
|
+
'"' +
|
|
426
|
+
styleAttr(event) +
|
|
427
|
+
mark(event) +
|
|
428
|
+
">" +
|
|
429
|
+
markup(event.tokens, event.style, intl) +
|
|
430
|
+
"</div>",
|
|
431
|
+
);
|
|
432
|
+
},
|
|
433
|
+
// A split places its slots across the content width instead of down
|
|
434
|
+
// the band. The slot items inside keep their ordinary markup, so a
|
|
435
|
+
// consumer of this fragment reads the same item container wherever it
|
|
436
|
+
// sits; only the wrapper around it is new.
|
|
437
|
+
"split-start": (event) => {
|
|
228
438
|
content(event);
|
|
229
439
|
out +=
|
|
230
|
-
'<div class="q-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
"</div>";
|
|
440
|
+
'<div class="q-split q-' + event.role + '"' + styleAttr(event, "display:flex") + ">";
|
|
441
|
+
split = { slots: event.slots, at: 0 };
|
|
442
|
+
},
|
|
443
|
+
"split-end": () => {
|
|
444
|
+
split = null;
|
|
445
|
+
out += "</div>";
|
|
237
446
|
},
|
|
238
447
|
// An image is an item container of its own holding one `<img>`: the
|
|
239
448
|
// band-role class joins `q-item q-image`, `align`/`background` dress
|
|
@@ -245,25 +454,27 @@ export function html() {
|
|
|
245
454
|
// never the markup-passing join a cell body gets; an image without one
|
|
246
455
|
// carries `alt=""`, the decorative-image convention.
|
|
247
456
|
image: (event) => {
|
|
248
|
-
content(event);
|
|
249
457
|
let source = sources.get(event.bytes);
|
|
250
458
|
if (source === undefined)
|
|
251
459
|
sources.set(
|
|
252
460
|
event.bytes,
|
|
253
461
|
(source = esc("data:image/" + event.format + ";base64," + base64(event.bytes))),
|
|
254
462
|
);
|
|
255
|
-
|
|
463
|
+
emit(
|
|
464
|
+
event,
|
|
256
465
|
'<div class="q-item q-image q-' +
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
466
|
+
event.role +
|
|
467
|
+
'"' +
|
|
468
|
+
styleAttr(event) +
|
|
469
|
+
mark(event) +
|
|
470
|
+
'><img src="' +
|
|
471
|
+
source +
|
|
472
|
+
'" alt="' +
|
|
473
|
+
esc(text(event.alt || [])) +
|
|
474
|
+
// Both keep the aspect ratio, since neither names a height.
|
|
475
|
+
(event.fit === "width" ? '" style="width:100%">' : '" style="max-width:100%">') +
|
|
476
|
+
"</div>",
|
|
477
|
+
);
|
|
267
478
|
},
|
|
268
479
|
// Each group instance is wrapped in a `q-group` container carrying
|
|
269
480
|
// the group name, so print CSS can keep a header with its rows
|
|
@@ -277,6 +488,7 @@ export function html() {
|
|
|
277
488
|
'<div class="q-group' +
|
|
278
489
|
(event.break || event.reset ? " q-break" : "") +
|
|
279
490
|
'"' +
|
|
491
|
+
mark(event) +
|
|
280
492
|
' data-group="' +
|
|
281
493
|
esc(event.name) +
|
|
282
494
|
'">';
|
|
@@ -303,31 +515,50 @@ export function html() {
|
|
|
303
515
|
for (let column of /** @type {any[]} */ (event.columns))
|
|
304
516
|
cols +=
|
|
305
517
|
"<col" + (column.width ? ' style="width:' + esc(column.width) + '%"' : "") + ">";
|
|
306
|
-
let headers = event.columns.map((/** @type {any} */ column) =>
|
|
518
|
+
let headers = event.columns.map((/** @type {any} */ column) =>
|
|
519
|
+
Object.assign({}, column.header, { path: column.path }),
|
|
520
|
+
);
|
|
307
521
|
out +=
|
|
308
|
-
'<table class="q-table"
|
|
522
|
+
'<table class="q-table"' +
|
|
523
|
+
mark(event) +
|
|
524
|
+
">" +
|
|
309
525
|
"<colgroup>" +
|
|
310
526
|
cols +
|
|
311
527
|
"</colgroup>" +
|
|
312
|
-
"<thead><tr
|
|
313
|
-
|
|
528
|
+
"<thead><tr" +
|
|
529
|
+
styleAttr({ style: unbox(event.style) }) +
|
|
530
|
+
">" +
|
|
531
|
+
cells(headers, "th", attrs, intl) +
|
|
314
532
|
"</tr></thead><tbody>";
|
|
315
533
|
},
|
|
316
534
|
row: (event) => {
|
|
317
|
-
out +=
|
|
535
|
+
out +=
|
|
536
|
+
"<tr" +
|
|
537
|
+
styleAttr({ style: unbox(event.style) }) +
|
|
538
|
+
">" +
|
|
539
|
+
cells(event.cells, "td", attrs, intl) +
|
|
540
|
+
"</tr>";
|
|
318
541
|
},
|
|
319
542
|
"total-row": (event) => {
|
|
320
|
-
tfoot
|
|
321
|
-
|
|
543
|
+
if (!tfoot) {
|
|
544
|
+
out += "</tbody><tfoot>";
|
|
545
|
+
tfoot = true;
|
|
546
|
+
}
|
|
547
|
+
out +=
|
|
548
|
+
"<tr" +
|
|
549
|
+
styleAttr({ style: unbox(event.style) }) +
|
|
550
|
+
">" +
|
|
551
|
+
cells(event.cells, "td", attrs, intl) +
|
|
552
|
+
"</tr>";
|
|
322
553
|
},
|
|
323
554
|
"table-end": () => {
|
|
324
|
-
out += tfoot ? "</table>" : "</tbody></table>";
|
|
555
|
+
out += tfoot ? "</tfoot></table>" : "</tbody></table>";
|
|
325
556
|
},
|
|
326
557
|
});
|
|
327
558
|
// A root region reaches here open when no full-width footer followed the
|
|
328
559
|
// body it columns.
|
|
329
560
|
close();
|
|
330
|
-
return out;
|
|
561
|
+
return rooted ? out + "</div>" : out;
|
|
331
562
|
},
|
|
332
563
|
};
|
|
333
564
|
}
|
package/lib/style.css
CHANGED
|
@@ -23,15 +23,44 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
/* shared:start */
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* The baseline face hangs off the report root rather than off .q-item and
|
|
29
|
+
* .q-table: a direct rule beats an inherited one, so a rule on the items
|
|
30
|
+
* themselves would override the report default an author writes on the
|
|
31
|
+
* document, which the root carries as an inline font-family. The marking is
|
|
32
|
+
* outside the root and keeps its own rule for that reason.
|
|
33
|
+
*/
|
|
34
|
+
.q-report,
|
|
35
|
+
.q-unlicensed {
|
|
36
|
+
font-family: sans-serif;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.q-report,
|
|
40
|
+
.q-group {
|
|
41
|
+
display: flex;
|
|
42
|
+
flex-direction: column;
|
|
43
|
+
}
|
|
44
|
+
|
|
26
45
|
.q-table {
|
|
27
46
|
width: 100%;
|
|
28
47
|
border-collapse: collapse;
|
|
29
48
|
}
|
|
30
49
|
|
|
50
|
+
/* A visible text item occupies a line, and a literal newline is a break.
|
|
51
|
+
The class is the hook; a host overrides these the ordinary way, as with
|
|
52
|
+
.q-break. Empty table cells stay contentless, so they take only the
|
|
53
|
+
newline mapping. */
|
|
54
|
+
.q-item {
|
|
55
|
+
min-height: 1lh;
|
|
56
|
+
white-space: pre-line;
|
|
57
|
+
}
|
|
58
|
+
|
|
31
59
|
.q-table th,
|
|
32
60
|
.q-table td {
|
|
33
61
|
padding: 2pt 6pt;
|
|
34
62
|
text-align: left;
|
|
63
|
+
white-space: pre-line;
|
|
35
64
|
}
|
|
36
65
|
|
|
37
66
|
.q-table thead th {
|
|
@@ -82,6 +111,10 @@
|
|
|
82
111
|
* Pages are not the only fragmentation context: the columns case lives in the
|
|
83
112
|
* shared block above, because a sheet with no pages can still have columns.
|
|
84
113
|
*
|
|
114
|
+
* Occupy and newline-as-break live in the shared block above: a screen needs
|
|
115
|
+
* them, and they follow the same pattern as `.q-break` -- class is the hook,
|
|
116
|
+
* the rule is overridable.
|
|
117
|
+
*
|
|
85
118
|
* A host overrides any of these the ordinary way; `example/print.css` turns
|
|
86
119
|
* `break-inside` back to `auto` for one group to let it span pages.
|
|
87
120
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quario/html",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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,17 +39,17 @@
|
|
|
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.3.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.3.0"
|
|
48
48
|
},
|
|
49
49
|
"size-limit": [
|
|
50
50
|
{
|
|
51
51
|
"path": "lib/index.js",
|
|
52
|
-
"limit": "
|
|
52
|
+
"limit": "2.5 kB",
|
|
53
53
|
"ignore": [
|
|
54
54
|
"quario"
|
|
55
55
|
]
|