@quario/html 0.9.0 → 0.11.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 +37 -0
- package/README.md +18 -5
- package/lib/index.d.ts +24 -0
- package/lib/index.js +223 -180
- package/lib/style.css +10 -2
- package/package.json +15 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# @quario/html
|
|
2
2
|
|
|
3
|
+
## 0.11.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- **A `capabilities` descriptor.** The target exports what it makes of each declaration the spec's support matrix names — `resolved`, `approximated`, `withdrawn` or `unread` — beside its factory, so a host can ask before it renders rather than reading the prose. It is keyed by the schema name an author writes (`valign`, `break`, `page.margin`, and `column.width` / `slot.width` where one word carries two declarations), hand-written and gated against that matrix. It carries no version of its own: the package that ships it is the version.
|
|
8
|
+
- **`negative` and `zero` reach the markup edge.** The target stringifies through the engine's `format()`, so a cell declaring either presents there too: a negative in parentheses, a zero as a dash.
|
|
9
|
+
- **A collapsed group does not draw its content.** An instance whose group declares `collapsed` renders as its header and footer, with the rows between them left out — a page has nowhere to put a row a reader could open. Aggregates are unchanged, so a footer still totals them.
|
|
10
|
+
- **A run that links becomes an anchor.** A run carrying `href` is wrapped in an `<a>` inside its own span, so the style the run declared paints the link, and the URL is escaped at the one markup edge like every other interpolated value. A real anchor rather than a styled span, because keyboard reachability comes with it. The engine admitted the URL before it crossed, so this target decides nothing about it.
|
|
11
|
+
- **The report header scales with the size it sits in.** The shipped stylesheet's `.q-item.q-report-header` rule is `font-size: 1.4em` rather than `14pt`. A document declaring `"size": 20` now renders its report header at 28pt where it rendered at 14pt, smaller than its own body. This is visible to every host page: an unstyled report's header is now 1.4× the host's own body type rather than a fixed 14pt, so a host whose body is not 10pt sees the headline move. A host rule on the class still wins, as before.
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- **A cell's own `href` now links its text.** `href` sits on a cell as well as in a run's inline subset, but this target read it only off a run that declared a style of its own, so `{ style: { href } }` on the cell rendered the text with no anchor at all. It now asks the style the run wears rather than the one it declares, which is the cell's wherever the run is silent. A run declaring its own `href` still replaces the cell's rather than nesting inside it.
|
|
16
|
+
- Updated dependencies
|
|
17
|
+
- quario@0.11.0
|
|
18
|
+
|
|
19
|
+
## 0.10.0
|
|
20
|
+
|
|
21
|
+
### Minor Changes
|
|
22
|
+
|
|
23
|
+
- **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.
|
|
24
|
+
|
|
25
|
+
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.
|
|
26
|
+
|
|
27
|
+
**`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.
|
|
28
|
+
|
|
29
|
+
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`.
|
|
30
|
+
|
|
31
|
+
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.
|
|
32
|
+
|
|
33
|
+
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.
|
|
34
|
+
|
|
35
|
+
### Patch Changes
|
|
36
|
+
|
|
37
|
+
- Updated dependencies
|
|
38
|
+
- quario@0.10.0
|
|
39
|
+
|
|
3
40
|
## 0.9.0
|
|
4
41
|
|
|
5
42
|
### 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
|
|
@@ -109,8 +121,9 @@ change.
|
|
|
109
121
|
| Emits | For |
|
|
110
122
|
| -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
111
123
|
| `<div class="q-report">` | The fragment's root, holding every band |
|
|
124
|
+
| `<a href>` | A styled run that declares `href`, inside that run's own span. The URL is escaped like every other interpolated value, and the engine admitted it against the host's scheme allowlist before this target saw it |
|
|
112
125
|
| `<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`
|
|
126
|
+
| `<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 |
|
|
114
127
|
| `<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
128
|
| `<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
129
|
| `<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` |
|
|
@@ -124,7 +137,7 @@ Security Policy needs `img-src data:` (see
|
|
|
124
137
|
Style declarations map to inline CSS (`bold` → `font-weight:bold` or `font-weight:normal`, `size` → `font-size:<n>pt`,
|
|
125
138
|
`family: "mono"` → `font-family:monospace`, …). Occupy-a-line and newline-as-break are **not** inline: they hang off
|
|
126
139
|
`.q-item` and `.q-table th, td` in [`@quario/html/style.css`](#the-reference-stylesheet), the same way `.q-break`
|
|
127
|
-
honors
|
|
140
|
+
honors a leading page break. The class is the hook. The rule is a reference default a host overrides on source order.
|
|
128
141
|
A fragment without that sheet still carries the classes and the text, including newlines. It does not occupy or
|
|
129
142
|
break until some stylesheet says so. This target supplies no defaults in the markup: no weight or size per band
|
|
130
143
|
role, no leading, no padding, no spacing between bands, no borders. An inline `style` attribute would beat yours
|
|
@@ -133,7 +146,7 @@ instead. The PDF and XLSX targets carry theirs built in, because their consumers
|
|
|
133
146
|
row set still emits the table, its header, and an empty `<tbody>`. A hidden table cell keeps its `<td>`, empty.
|
|
134
147
|
|
|
135
148
|
This target paginates nothing: it ignores schema page bands (page furniture belongs to your print CSS)
|
|
136
|
-
and maps
|
|
149
|
+
and maps each edge a group's `break` turns to its own class. Page columns are laid out:
|
|
137
150
|
a node declaring `columns` wraps the content between its own bands in the `q-columns` container
|
|
138
151
|
above, and the browser flows it.
|
|
139
152
|
|
|
@@ -172,8 +185,8 @@ Host CSS targets these classes:
|
|
|
172
185
|
`.q-item { min-height: 1lh; white-space: pre-line }` occupies a line and breaks on newlines,
|
|
173
186
|
`.q-group { break-inside: avoid }` keeps a group header with its rows,
|
|
174
187
|
`.q-table tr { break-inside: avoid }` keeps a row whole,
|
|
175
|
-
`.q-break { break-before: page }`
|
|
176
|
-
printed page. Yours adds the page geometry in `@page`, which only you can decide.
|
|
188
|
+
`.q-break { break-before: page }` and `.q-break-after { break-after: page }` honor the schema's own
|
|
189
|
+
break hints, and `thead` repeats per printed page. Yours adds the page geometry in `@page`, which only you can decide.
|
|
177
190
|
3. Print with a headless browser (`page.pdf()` in Playwright or Puppeteer, page numbers via the
|
|
178
191
|
footer template) or a Paged-CSS engine (WeasyPrint or Prince, with page numbers via `@page` margin
|
|
179
192
|
boxes).
|
package/lib/index.d.ts
CHANGED
|
@@ -29,3 +29,27 @@ export interface HtmlOptions {
|
|
|
29
29
|
* described in SCHEMA.md ("The HTML target").
|
|
30
30
|
*/
|
|
31
31
|
export function html(options?: HtmlOptions): Target<"html", Promise<string>>;
|
|
32
|
+
|
|
33
|
+
/** What a target makes of a declaration: CONTEXT.md's fate vocabulary. */
|
|
34
|
+
export type Fate = "resolved" | "approximated" | "withdrawn" | "unread";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* What this target makes of each declaration SCHEMA.md's support matrix
|
|
38
|
+
* names. Hand-written and gated against that matrix rather than generated
|
|
39
|
+
* from it. It carries no version of its own: the package that ships it is the
|
|
40
|
+
* version.
|
|
41
|
+
*
|
|
42
|
+
* Keyed by the schema name an author writes — `valign`, `break`, `page.margin`
|
|
43
|
+
* — so a host, a `required` marking and a problem message share one
|
|
44
|
+
* vocabulary. Two matrix rows share the bare word `width`, so each is
|
|
45
|
+
* qualified by what carries it: `column.width` and `slot.width`.
|
|
46
|
+
*
|
|
47
|
+
* It covers the declarations every official target states one fate for. An
|
|
48
|
+
* absent key means the matrix states no single fate for that row — a row that
|
|
49
|
+
* summarises more than one declaration, or that describes a behaviour — never
|
|
50
|
+
* that this target has no answer.
|
|
51
|
+
*/
|
|
52
|
+
export const capabilities: {
|
|
53
|
+
readonly target: "html";
|
|
54
|
+
readonly declarations: Readonly<Record<string, Fate>>;
|
|
55
|
+
};
|
package/lib/index.js
CHANGED
|
@@ -9,7 +9,17 @@
|
|
|
9
9
|
* never carries markup-escaped text.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
collapse,
|
|
14
|
+
display,
|
|
15
|
+
format,
|
|
16
|
+
hostOptions,
|
|
17
|
+
isReportBand,
|
|
18
|
+
splits,
|
|
19
|
+
styledRuns,
|
|
20
|
+
text,
|
|
21
|
+
walk,
|
|
22
|
+
} from "quario";
|
|
13
23
|
|
|
14
24
|
/** @type {Record<string, string>} */
|
|
15
25
|
let ESC = {
|
|
@@ -24,9 +34,8 @@ let esc = (value) => display(value).replace(/[&<>"']/g, (char) => ESC[char]);
|
|
|
24
34
|
|
|
25
35
|
// Join a cell's tokens to HTML: literal template text is author-controlled
|
|
26
36
|
// markup and passes verbatim; every interpolated value is escaped, no
|
|
27
|
-
// exceptions.
|
|
28
|
-
//
|
|
29
|
-
// that one, escaped whole.
|
|
37
|
+
// exceptions. An attribute value takes the engine's `text` join instead,
|
|
38
|
+
// escaped whole.
|
|
30
39
|
/** @type {(tokens: any[], style: any, intl: any) => string} */
|
|
31
40
|
let joined = (tokens, style, intl) => {
|
|
32
41
|
let out = "";
|
|
@@ -41,32 +50,42 @@ let shown = (value, style, intl) => {
|
|
|
41
50
|
};
|
|
42
51
|
|
|
43
52
|
// 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.
|
|
53
|
+
// not a difference against the cell's -- the engine already composed the two.
|
|
54
|
+
// An unstyled run stays bare.
|
|
48
55
|
/** @type {(styled: { style: any, tokens: any[] }, style: any, intl: any, map: any) => string} */
|
|
49
|
-
let
|
|
50
|
-
let
|
|
56
|
+
let styledRun = (styled, style, intl, map) => {
|
|
57
|
+
let worn = styled.style ?? style;
|
|
58
|
+
let body = linked(joined(styled.tokens, worn, intl), worn);
|
|
51
59
|
if (!styled.style) return body;
|
|
52
60
|
let inline = css(styled.style, map);
|
|
53
61
|
return inline ? '<span style="' + esc(inline) + '">' + body + "</span>" : body;
|
|
54
62
|
};
|
|
55
63
|
|
|
64
|
+
// A run that carries an `href` is wrapped in an anchor, inside its own span so
|
|
65
|
+
// the style the run declared paints the link as well. The style asked is the
|
|
66
|
+
// one the run wears rather than the one it declares, because `href` sits on a
|
|
67
|
+
// cell as well as on a run (SCHEMA.md, "Style declarations") and a cell's own
|
|
68
|
+
// links every run that declared no other. The engine admitted the
|
|
69
|
+
// URL against the host's scheme allowlist before it crossed (docs/adr/0085);
|
|
70
|
+
// this target escapes it like every other interpolated value and decides
|
|
71
|
+
// nothing about it. An `<a>` is keyboard-reachable for nothing, which is why
|
|
72
|
+
// this is a real anchor rather than a styled span with a click handler.
|
|
73
|
+
/** @type {(body: string, style: any) => string} */
|
|
74
|
+
let linked = (body, style) =>
|
|
75
|
+
style?.href ? '<a href="' + esc(style.href) + '">' + body + "</a>" : body;
|
|
76
|
+
|
|
56
77
|
// 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").
|
|
78
|
+
// `styledRuns`, so this target cannot drift from the others.
|
|
59
79
|
/** @type {(map: any) => (tokens: any[], style?: any, intl?: any) => string} */
|
|
60
80
|
let cellMarkup = (map) => (tokens, style, intl) => {
|
|
61
81
|
let out = "";
|
|
62
|
-
for (let styled of styledRuns(tokens)) out +=
|
|
82
|
+
for (let styled of styledRuns(tokens)) out += styledRun(styled, style, intl, map);
|
|
63
83
|
return out;
|
|
64
84
|
};
|
|
65
85
|
|
|
66
|
-
// base64 for an image's `data:` URI, without Buffer:
|
|
67
|
-
// and in
|
|
68
|
-
//
|
|
69
|
-
// argument per byte, which a real image exhausts.
|
|
86
|
+
// base64 for an image's `data:` URI, without Buffer: the same `lib/` runs in
|
|
87
|
+
// Node and in browsers, and `btoa` is standard in both. Chunked because
|
|
88
|
+
// `fromCharCode(...bytes)` spreads one argument per byte.
|
|
70
89
|
let CHUNK = 0x8000;
|
|
71
90
|
/** @type {(bytes: Uint8Array) => string} */
|
|
72
91
|
let base64 = (bytes) => {
|
|
@@ -91,25 +110,22 @@ let NAMEABLE = /^[A-Za-z0-9 _-]+$/;
|
|
|
91
110
|
let UNSAFE = /[;}]/;
|
|
92
111
|
/** @type {(value: any) => boolean} */
|
|
93
112
|
let isHex = (value) => typeof value === "string" && HEX.test(value);
|
|
94
|
-
let ALIGNMENTS = ["left", "center", "right"];
|
|
95
113
|
// A slot item's `valign` and `align` as grid placement (see `placed`); the
|
|
96
114
|
// vertical vocabulary is these keys, so the three readers of it agree.
|
|
97
115
|
/** @type {Record<string, string>} */
|
|
98
116
|
let ALIGN_CONTENT = { top: "start", middle: "center", bottom: "end" };
|
|
99
117
|
/** @type {Record<string, string>} */
|
|
100
118
|
let JUSTIFY_ITEMS = { left: "start", center: "center", right: "end" };
|
|
101
|
-
let VALIGNMENTS = Object.keys(ALIGN_CONTENT);
|
|
102
119
|
|
|
103
120
|
// A group instance's own bands. They sit outside a columned container only when
|
|
104
121
|
// that instance is the node which declared the count — an inner instance's are
|
|
105
122
|
// content like anything else. The report's own bands are the engine's to name.
|
|
106
123
|
let OWN = new Set(["group-header", "group-footer"]);
|
|
107
124
|
// 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.
|
|
125
|
+
// `pdf({ fonts })`'s. Validated here rather than at render: a malformed one is
|
|
126
|
+
// host configuration and belongs where the host wrote it. The value itself is
|
|
127
|
+
// trusted and emitted verbatim. `Error` and the `options.fonts.<name>:` prefix
|
|
128
|
+
// match the pdf target's: one option, one message shape.
|
|
113
129
|
/** @type {(path: string, value: any) => string} */
|
|
114
130
|
let cssValue = (path, value) => {
|
|
115
131
|
if (typeof value !== "string" || !value)
|
|
@@ -144,11 +160,10 @@ let mapping = (fonts) => {
|
|
|
144
160
|
);
|
|
145
161
|
};
|
|
146
162
|
|
|
147
|
-
// Resolve a declared family name against the table the factory built
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
// of naming a face that cannot exist (docs/adr/0034).
|
|
163
|
+
// Resolve a declared family name against the table the factory built, then
|
|
164
|
+
// the name itself. A name that is not nameable as it stands resolves to
|
|
165
|
+
// nothing rather than a rewritten near-miss, so the element falls through to
|
|
166
|
+
// the layer below (docs/adr/0034).
|
|
152
167
|
/** @type {(map: Record<string, string>, value: string) => string} */
|
|
153
168
|
let named = (map, value) => {
|
|
154
169
|
// Own-key lookup: `constructor` must not resolve an inherited member.
|
|
@@ -156,28 +171,29 @@ let named = (map, value) => {
|
|
|
156
171
|
if (Object.hasOwn(map, key)) return map[key];
|
|
157
172
|
return NAMEABLE.test(value) ? "'" + value + "'" : "";
|
|
158
173
|
};
|
|
159
|
-
//
|
|
160
|
-
//
|
|
174
|
+
// A declared flag is `true` or `false`: the engine drops every other shape
|
|
175
|
+
// before a target sees it (SCHEMA.md, "Style declarations"), so a target maps
|
|
176
|
+
// the vocabulary rather than checking it a second time.
|
|
161
177
|
/** @type {(on: string, off: string) => (value: any) => string} */
|
|
162
|
-
let flag = (on, off) => (value) => (value
|
|
178
|
+
let flag = (on, off) => (value) => (value ? on : off);
|
|
163
179
|
/** @type {Record<string, (value: any, map: Record<string, string>) => string>} */
|
|
164
180
|
let CSS = {
|
|
181
|
+
// A family is the one declaration whose value a host may name, so the map
|
|
182
|
+
// may still answer with nothing; the value itself is a non-empty string.
|
|
165
183
|
family: (value, map) => {
|
|
166
|
-
if (typeof value !== "string" || !value) return "";
|
|
167
184
|
let name = named(map, value);
|
|
168
185
|
return name ? "font-family:" + name : "";
|
|
169
186
|
},
|
|
170
|
-
size: (value) =>
|
|
187
|
+
size: (value) => "font-size:" + value + "pt",
|
|
171
188
|
bold: flag("font-weight:bold", "font-weight:normal"),
|
|
172
189
|
italic: flag("font-style:italic", "font-style:normal"),
|
|
173
|
-
color: (value) =>
|
|
174
|
-
background: (value) =>
|
|
175
|
-
align: (value) =>
|
|
190
|
+
color: (value) => "color:" + value,
|
|
191
|
+
background: (value) => "background-color:" + value,
|
|
192
|
+
align: (value) => "text-align:" + value,
|
|
176
193
|
// The table reading, on a `<tr>` or a cell: the browser's own sheet gives
|
|
177
|
-
// cells `vertical-align: inherit`, so a row's reaches them like `bold`
|
|
178
|
-
//
|
|
179
|
-
|
|
180
|
-
valign: (value) => (VALIGNMENTS.includes(value) ? "vertical-align:" + value : ""),
|
|
194
|
+
// cells `vertical-align: inherit`, so a row's reaches them like `bold` does.
|
|
195
|
+
// `placed` below lifts the name out before this map sees a slot item's.
|
|
196
|
+
valign: (value) => "vertical-align:" + value,
|
|
181
197
|
// The text itself is untouched, so selection and screen readers get what
|
|
182
198
|
// the author wrote; only its rendering is capitalised.
|
|
183
199
|
uppercase: flag("text-transform:uppercase", "text-transform:none"),
|
|
@@ -185,14 +201,12 @@ let CSS = {
|
|
|
185
201
|
|
|
186
202
|
let SIDES = ["Top", "Right", "Bottom", "Left"];
|
|
187
203
|
let LINES = ["solid", "dashed", "dotted"];
|
|
188
|
-
/** @type {(value: any) => boolean} */
|
|
189
|
-
let isPad = (value) => Number.isFinite(value) && value >= 0;
|
|
190
204
|
for (let side of SIDES) {
|
|
191
205
|
let edge = side.toLowerCase();
|
|
192
|
-
CSS["padding" + side] = (value) =>
|
|
206
|
+
CSS["padding" + side] = (value) => "padding-" + edge + ":" + value + "pt";
|
|
193
207
|
}
|
|
194
|
-
CSS.spaceBefore = (value) =>
|
|
195
|
-
CSS.spaceAfter = (value) =>
|
|
208
|
+
CSS.spaceBefore = (value) => "margin-top:" + value + "pt";
|
|
209
|
+
CSS.spaceAfter = (value) => "margin-bottom:" + value + "pt";
|
|
196
210
|
|
|
197
211
|
/** @type {(width: any) => boolean} */
|
|
198
212
|
let isStroke = (width) => Number.isFinite(width) && width > 0;
|
|
@@ -219,11 +233,12 @@ let decoLine = (style) => {
|
|
|
219
233
|
return parts;
|
|
220
234
|
};
|
|
221
235
|
|
|
236
|
+
// Only called where one of the two names is declared, so a style that draws
|
|
237
|
+
// neither line is a style that turned one off.
|
|
222
238
|
/** @type {(style: any) => string} */
|
|
223
239
|
let decoration = (style) => {
|
|
224
240
|
let parts = decoLine(style);
|
|
225
|
-
|
|
226
|
-
return style.underline === false || style.strikethrough === false ? "text-decoration:none" : "";
|
|
241
|
+
return parts.length ? "text-decoration:" + parts.join(" ") : "text-decoration:none";
|
|
227
242
|
};
|
|
228
243
|
|
|
229
244
|
/** @type {(name: string) => boolean} */
|
|
@@ -282,17 +297,15 @@ let css = (style, map) => {
|
|
|
282
297
|
};
|
|
283
298
|
|
|
284
299
|
// Compose a resolved style into one escaped inline style attribute. `own` is
|
|
285
|
-
// this target's
|
|
300
|
+
// this target's structural CSS, which goes first and always emits: page
|
|
286
301
|
// columns and splits are placement, not the visual defaults this target
|
|
287
|
-
// refuses to supply
|
|
288
|
-
// everything else rather than a second one (CLAUDE.md, the markup edge).
|
|
302
|
+
// refuses to supply.
|
|
289
303
|
/** @type {(fonts: any) => { attr: (event: { style?: any }, own?: string) => string, map: Record<string, string> }} */
|
|
290
304
|
let styling = (fonts) => {
|
|
291
305
|
// The spread is the precedence the option promises: a host mapping is
|
|
292
|
-
// consulted before the three generics, and may replace one.
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
// `family` against the same one.
|
|
306
|
+
// consulted before the three generics, and may replace one. Handed back
|
|
307
|
+
// beside the attribute writer, because a styled run's span resolves a
|
|
308
|
+
// `family` against the same table.
|
|
296
309
|
let map = { ...FAMILY, ...mapping(fonts) };
|
|
297
310
|
return {
|
|
298
311
|
map,
|
|
@@ -304,25 +317,21 @@ let styling = (fonts) => {
|
|
|
304
317
|
};
|
|
305
318
|
|
|
306
319
|
// One slot's wrapper. An authored share is that width; a width-less slot
|
|
307
|
-
// divides what the sized ones leave,
|
|
320
|
+
// divides what the sized ones leave, as a width-less column does.
|
|
308
321
|
// `display:grid` is what gives a slot's box the split's height rather than its
|
|
309
|
-
// own content's
|
|
310
|
-
//
|
|
311
|
-
// fragment, because a slot's item is the same `q-item` div it would be
|
|
312
|
-
// anywhere else -- nothing about an item's markup depends on where it sits.
|
|
322
|
+
// own content's. It belongs here and not on the fragment: a slot's item is the
|
|
323
|
+
// same `q-item` div it would be anywhere else.
|
|
313
324
|
/** @type {(slot: { width?: number } | undefined) => string} */
|
|
314
325
|
let slotAttr = (slot) =>
|
|
315
326
|
' style="' +
|
|
316
327
|
esc("display:grid;" + (slot && slot.width != null ? "width:" + slot.width + "%" : "flex:1")) +
|
|
317
328
|
'"';
|
|
318
329
|
|
|
319
|
-
// A slot item's `valign`. The
|
|
320
|
-
//
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
// style so the cell mapping (`vertical-align`) does not also emit on a div.
|
|
325
|
-
// The result is what `styleAttr` takes: a style and the structural prefix.
|
|
330
|
+
// A slot item's `valign`. The wrapper's grid stretches the item to the split's
|
|
331
|
+
// height, so the item is the box: it becomes a grid of its own and
|
|
332
|
+
// `align-content` places the content. Not on the wrapper -- `align-items`
|
|
333
|
+
// there would shrink the item back and reopen the gap the grid closed. Lifted
|
|
334
|
+
// out of the style so the cell mapping (`vertical-align`) does not also emit.
|
|
326
335
|
/** @type {(event: { style?: any }) => { style?: any, own: string }} */
|
|
327
336
|
let placed = (event) => {
|
|
328
337
|
let valign = event.style?.valign;
|
|
@@ -339,9 +348,9 @@ let justified = (own, style) =>
|
|
|
339
348
|
? own + ";justify-items:" + JUSTIFY_ITEMS[style.align]
|
|
340
349
|
: own;
|
|
341
350
|
|
|
342
|
-
// The identity attribute
|
|
343
|
-
//
|
|
344
|
-
//
|
|
351
|
+
// The identity attribute. `mark` is the factory's verdict: a render without
|
|
352
|
+
// `paths` maps every event to "" through the same function rather than
|
|
353
|
+
// re-deciding per call site.
|
|
345
354
|
/** @type {(event: { path?: string }) => string} */
|
|
346
355
|
let pathAttr = ({ path }) => (path ? ' data-q-path="' + esc(path) + '"' : "");
|
|
347
356
|
/** @type {(list: any[], tag: string, attrs: (event: any) => string, intl: any, markup: any) => string} */
|
|
@@ -365,9 +374,7 @@ let cells = (list, tag, attrs, intl, markup) =>
|
|
|
365
374
|
*
|
|
366
375
|
* `paths` opts the fragment into identity: each element whose event carries a
|
|
367
376
|
* schema `path` gains `data-q-path`, so a consumer can map rendered output
|
|
368
|
-
* back to the definition behind it
|
|
369
|
-
* default, because the attribute is weight every plain display pays for
|
|
370
|
-
* nothing.
|
|
377
|
+
* back to the definition behind it. Off by default.
|
|
371
378
|
*
|
|
372
379
|
* `fonts` is this target's font mapping: a declared `family` name to a CSS
|
|
373
380
|
* value, so a host says what the name means on this page. Malformed values
|
|
@@ -391,15 +398,12 @@ export function html(options) {
|
|
|
391
398
|
// The pair every cell carries, bound once rather than threaded as two.
|
|
392
399
|
/** @type {(cell: any) => string} */
|
|
393
400
|
// A span covers several columns; `colspan` is what that is on this surface.
|
|
394
|
-
// Escaped like every other attribute value this target writes, though the
|
|
395
|
-
// engine has already vouched the span is a positive integer.
|
|
396
401
|
/** @type {(cell: any) => string} */
|
|
397
402
|
let attrs = (cell) =>
|
|
398
403
|
(cell.span > 1 ? ' colspan="' + esc(cell.span) + '"' : "") + styleAttr(cell) + mark(cell);
|
|
399
404
|
// One table row: the row style on the `<tr>`, from which it inherits into
|
|
400
405
|
// the cells, `valign` included. The box is not among these names — a `<tr>`
|
|
401
|
-
// could not honour one, and the engine
|
|
402
|
-
// the cells, where it arrives as ordinary inline CSS.
|
|
406
|
+
// could not honour one, and the engine resolved that half onto the cells.
|
|
403
407
|
/** @type {(event: any, list: any[], tag: string, intl: any) => string} */
|
|
404
408
|
let tr = (event, list, tag, intl) =>
|
|
405
409
|
"<tr" + styleAttr(event) + ">" + cells(list, tag, attrs, intl, markup) + "</tr>";
|
|
@@ -409,8 +413,7 @@ export function html(options) {
|
|
|
409
413
|
let out = "";
|
|
410
414
|
// The finished `src` of every image this render has already encoded,
|
|
411
415
|
// keyed by the array the source expression yielded. A logo down a
|
|
412
|
-
// thousand rows is one base64 walk
|
|
413
|
-
// goes with the render — nothing here outlives it.
|
|
416
|
+
// thousand rows is one base64 walk. Nothing here outlives the render.
|
|
414
417
|
/** @type {Map<Uint8Array, string>} */
|
|
415
418
|
let sources = new Map();
|
|
416
419
|
// Whether a `total-row` already opened `<tfoot>` — markup
|
|
@@ -422,35 +425,16 @@ export function html(options) {
|
|
|
422
425
|
let rooted = false;
|
|
423
426
|
// The page columns a node declared: its `count`, the `owner` depth that
|
|
424
427
|
// declared it (-1 for the report root), and whether the container is
|
|
425
|
-
// emitted and still to be closed.
|
|
426
|
-
//
|
|
428
|
+
// emitted and still to be closed. Never more than one, because nesting a
|
|
429
|
+
// columned region is a definition error.
|
|
427
430
|
/** @type {{ count: number, owner: number, open: boolean } | null} */
|
|
428
431
|
let region = null;
|
|
429
432
|
// How many group instances are open, so the declaring node's own bands
|
|
430
|
-
// can be told from the content they bracket
|
|
431
|
-
//
|
|
432
|
-
// that may own the region. Taken from each event rather than counted, so
|
|
433
|
-
// there is one spelling of a depth the stream already carries.
|
|
433
|
+
// can be told from the content they bracket. Taken from each event
|
|
434
|
+
// rather than counted, so one spelling of a depth the stream carries.
|
|
434
435
|
let depth = 0;
|
|
435
|
-
// The split being filled, if any: the slot geometry the bracket carried
|
|
436
|
-
// and how many slots have been placed. Splits never nest, so one is
|
|
437
|
-
// enough. Null whenever the walk is not inside a bracket.
|
|
438
|
-
/** @type {{ slots: any[], at: number } | null} */
|
|
439
|
-
let split = null;
|
|
440
436
|
/** @type {{ locale?: string, currency?: string, timeZone?: string } | null} */
|
|
441
437
|
let intl = null;
|
|
442
|
-
// Place one item's markup: inside a bracket it goes in the next slot,
|
|
443
|
-
// and everywhere else it is a band item like any other.
|
|
444
|
-
/** @type {(event: any, fragment: string) => void} */
|
|
445
|
-
let emit = (event, fragment) => {
|
|
446
|
-
if (!split) {
|
|
447
|
-
content(event);
|
|
448
|
-
out += fragment;
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
out +=
|
|
452
|
-
'<div class="q-slot"' + slotAttr(split.slots[split.at++]) + ">" + fragment + "</div>";
|
|
453
|
-
};
|
|
454
438
|
// Does this event stay full-width, outside the columns? The declaring
|
|
455
439
|
// node's own bands do.
|
|
456
440
|
/** @type {(event: any) => boolean} */
|
|
@@ -483,14 +467,61 @@ export function html(options) {
|
|
|
483
467
|
if (fullWidth(event)) close();
|
|
484
468
|
else openColumns();
|
|
485
469
|
};
|
|
470
|
+
// One item's or picture's own markup, by event type: the stable
|
|
471
|
+
// `q-item q-<role>` classes are the whole class attribute (print CSS
|
|
472
|
+
// targets the band-role class for styling and breaks), and a picture is
|
|
473
|
+
// an item container of its own holding one `<img>`.
|
|
474
|
+
/** @type {Record<string, (event: any) => string>} */
|
|
475
|
+
let fragment = {
|
|
476
|
+
item: (event) => {
|
|
477
|
+
let slot = placed(event);
|
|
478
|
+
return (
|
|
479
|
+
'<div class="q-item q-' +
|
|
480
|
+
event.role +
|
|
481
|
+
'"' +
|
|
482
|
+
styleAttr(slot, slot.own) +
|
|
483
|
+
mark(event) +
|
|
484
|
+
">" +
|
|
485
|
+
markup(event.tokens, event.style, intl) +
|
|
486
|
+
"</div>"
|
|
487
|
+
);
|
|
488
|
+
},
|
|
489
|
+
image: (event) => {
|
|
490
|
+
let slot = placed(event);
|
|
491
|
+
let source = sources.get(event.bytes);
|
|
492
|
+
if (source === undefined)
|
|
493
|
+
sources.set(
|
|
494
|
+
event.bytes,
|
|
495
|
+
(source = esc("data:image/" + event.format + ";base64," + base64(event.bytes))),
|
|
496
|
+
);
|
|
497
|
+
return (
|
|
498
|
+
'<div class="q-item q-image q-' +
|
|
499
|
+
event.role +
|
|
500
|
+
'"' +
|
|
501
|
+
styleAttr(slot, justified(slot.own, event.style)) +
|
|
502
|
+
mark(event) +
|
|
503
|
+
'><img src="' +
|
|
504
|
+
source +
|
|
505
|
+
'" width="' +
|
|
506
|
+
esc(event.width) +
|
|
507
|
+
'" height="' +
|
|
508
|
+
esc(event.height) +
|
|
509
|
+
'" alt="' +
|
|
510
|
+
esc(text(event.alt || [])) +
|
|
511
|
+
'" style="' +
|
|
512
|
+
(event.fit === "width" ? "" : "max-") +
|
|
513
|
+
'width:100%;height:auto">' +
|
|
514
|
+
"</div>"
|
|
515
|
+
);
|
|
516
|
+
},
|
|
517
|
+
};
|
|
486
518
|
// Hidden items never reach the stream and a hidden cell arrives with no
|
|
487
519
|
// tokens, so no visibility checks are needed in any handler below.
|
|
488
|
-
await walk(stream(data), {
|
|
520
|
+
await walk(collapse(splits(stream(data))), {
|
|
489
521
|
// Nothing unless the render is unlicensed — then the fragment opens
|
|
490
|
-
// with the marking badge
|
|
491
|
-
//
|
|
492
|
-
//
|
|
493
|
-
// against.
|
|
522
|
+
// with the marking badge: the engine's wording in this target's
|
|
523
|
+
// element, escaped, so the badge stays childless whatever stream a
|
|
524
|
+
// host compiles against.
|
|
494
525
|
"report-start": (event) => {
|
|
495
526
|
intl = {
|
|
496
527
|
locale: event.locale,
|
|
@@ -500,12 +531,10 @@ export function html(options) {
|
|
|
500
531
|
if (event.marking) out += '<div class="q-unlicensed">' + esc(event.marking) + "</div>";
|
|
501
532
|
if (event.margin != null)
|
|
502
533
|
out += "<style>@page{margin:" + esc(event.margin) + "pt}</style>";
|
|
503
|
-
// The report root. Always emitted
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
//
|
|
507
|
-
// reach it, and a marking an author can shrink is one ADR 0002
|
|
508
|
-
// does not allow.
|
|
534
|
+
// The report root. Always emitted: the fragment otherwise has no
|
|
535
|
+
// element for a document-wide declaration to inherit from, and the
|
|
536
|
+
// shipped stylesheet's baseline hangs off it. The marking stays
|
|
537
|
+
// outside, where an author's `style` cannot reach it (ADR 0002).
|
|
509
538
|
out += '<div class="q-report"' + styleAttr(event) + ">";
|
|
510
539
|
rooted = true;
|
|
511
540
|
if (event.columns) region = { count: event.columns, owner: -1, open: false };
|
|
@@ -513,90 +542,54 @@ export function html(options) {
|
|
|
513
542
|
// The stable `q-item q-<role>` classes are the whole class attribute;
|
|
514
543
|
// print CSS targets the band-role class for styling and breaks.
|
|
515
544
|
item: (event) => {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
event,
|
|
519
|
-
'<div class="q-item q-' +
|
|
520
|
-
event.role +
|
|
521
|
-
'"' +
|
|
522
|
-
styleAttr(slot, slot.own) +
|
|
523
|
-
mark(event) +
|
|
524
|
-
">" +
|
|
525
|
-
markup(event.tokens, event.style, intl) +
|
|
526
|
-
"</div>",
|
|
527
|
-
);
|
|
545
|
+
content(event);
|
|
546
|
+
out += fragment.item(event);
|
|
528
547
|
},
|
|
529
548
|
// A split places its slots across the content width instead of down
|
|
530
|
-
// the band. The slot items
|
|
531
|
-
//
|
|
532
|
-
|
|
533
|
-
"split-start": (event) => {
|
|
549
|
+
// the band. The slot items keep their ordinary markup; only the
|
|
550
|
+
// wrapper around them is new.
|
|
551
|
+
split: (event) => {
|
|
534
552
|
content(event);
|
|
535
553
|
out +=
|
|
536
554
|
'<div class="q-split q-' + event.role + '"' + styleAttr(event, "display:flex") + ">";
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
555
|
+
event.items.forEach((/** @type {any} */ slot, /** @type {number} */ at) => {
|
|
556
|
+
out +=
|
|
557
|
+
'<div class="q-slot"' +
|
|
558
|
+
slotAttr(event.slots[at]) +
|
|
559
|
+
">" +
|
|
560
|
+
fragment[slot.type](slot) +
|
|
561
|
+
"</div>";
|
|
562
|
+
});
|
|
541
563
|
out += "</div>";
|
|
542
564
|
},
|
|
543
565
|
// An image is an item container of its own holding one `<img>`: the
|
|
544
566
|
// band-role class joins `q-item q-image`, `align`/`background` dress
|
|
545
|
-
// the container, and `fit` sizes the picture inside it.
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
// same escaped attribute path as everything else rather than a second
|
|
549
|
-
// one, and escaped when it is encoded rather than per occurrence. `alt` is report data, so it is the display-text join escaped,
|
|
550
|
-
// never the markup-passing join a cell body gets; an image without one
|
|
551
|
-
// carries `alt=""`, the decorative-image convention.
|
|
567
|
+
// the container, and `fit` sizes the picture inside it. `alt` is report
|
|
568
|
+
// data, so it takes the display-text join escaped, never the
|
|
569
|
+
// markup-passing join a cell body gets; without one it is `alt=""`.
|
|
552
570
|
//
|
|
553
|
-
// `width`/`height` are the event's natural size,
|
|
554
|
-
//
|
|
555
|
-
//
|
|
556
|
-
//
|
|
557
|
-
//
|
|
558
|
-
// used height and a picture capped narrower than its file renders
|
|
559
|
-
// squashed. SCHEMA.md, "The HTML target", states that normatively.
|
|
571
|
+
// `width`/`height` are the event's natural size, so a picture reserves
|
|
572
|
+
// its aspect ratio before it decodes. The pair is a presentational
|
|
573
|
+
// hint rather than a ratio, which is why the `fit` style carries
|
|
574
|
+
// `height:auto`: without it a picture capped narrower than its file
|
|
575
|
+
// renders squashed.
|
|
560
576
|
image: (event) => {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
if (source === undefined)
|
|
564
|
-
sources.set(
|
|
565
|
-
event.bytes,
|
|
566
|
-
(source = esc("data:image/" + event.format + ";base64," + base64(event.bytes))),
|
|
567
|
-
);
|
|
568
|
-
emit(
|
|
569
|
-
event,
|
|
570
|
-
'<div class="q-item q-image q-' +
|
|
571
|
-
event.role +
|
|
572
|
-
'"' +
|
|
573
|
-
styleAttr(slot, justified(slot.own, event.style)) +
|
|
574
|
-
mark(event) +
|
|
575
|
-
'><img src="' +
|
|
576
|
-
source +
|
|
577
|
-
'" width="' +
|
|
578
|
-
esc(event.width) +
|
|
579
|
-
'" height="' +
|
|
580
|
-
esc(event.height) +
|
|
581
|
-
'" alt="' +
|
|
582
|
-
esc(text(event.alt || [])) +
|
|
583
|
-
'" style="' +
|
|
584
|
-
(event.fit === "width" ? "" : "max-") +
|
|
585
|
-
'width:100%;height:auto">' +
|
|
586
|
-
"</div>",
|
|
587
|
-
);
|
|
577
|
+
content(event);
|
|
578
|
+
out += fragment.image(event);
|
|
588
579
|
},
|
|
589
|
-
// Each group instance is wrapped in a `q-group` container carrying
|
|
590
|
-
//
|
|
591
|
-
//
|
|
592
|
-
//
|
|
593
|
-
//
|
|
580
|
+
// Each group instance is wrapped in a `q-group` container carrying the
|
|
581
|
+
// group name, so print CSS can keep a header with its rows or force a
|
|
582
|
+
// break per group. The stream resolved the group's `break` position
|
|
583
|
+
// into this instance's two edges, each mapping to its class. Both ride
|
|
584
|
+
// the opening event: the container is written here and a class cannot
|
|
585
|
+
// be added to it later.
|
|
594
586
|
"group-start": (event) => {
|
|
595
587
|
content(event);
|
|
596
588
|
depth = event.depth + 1;
|
|
597
589
|
out +=
|
|
598
590
|
'<div class="q-group' +
|
|
599
|
-
(event.break
|
|
591
|
+
(event.break ? " q-break" : "") +
|
|
592
|
+
(event.breakAfter ? " q-break-after" : "") +
|
|
600
593
|
'"' +
|
|
601
594
|
mark(event) +
|
|
602
595
|
' data-group="' +
|
|
@@ -659,3 +652,53 @@ export function html(options) {
|
|
|
659
652
|
},
|
|
660
653
|
};
|
|
661
654
|
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* What this target makes of each declaration the support matrix names: one of
|
|
658
|
+
* the [fates](../../../CONTEXT.md#declaration-and-resolution) — `resolved`,
|
|
659
|
+
* `approximated`, `withdrawn` or `unread` — so a host can ask before it
|
|
660
|
+
* renders rather than reading the prose.
|
|
661
|
+
*
|
|
662
|
+
* **Hand-written, and gated rather than generated.** SCHEMA.md's matrix is the
|
|
663
|
+
* description a person maintains and this is the data a host reads; the repo's
|
|
664
|
+
* `test/capabilities.test.js` holds the two equal, which is the same bargain
|
|
665
|
+
* ADR 0014 makes for the band-role table. It carries no version of its own:
|
|
666
|
+
* the package that ships it is the version, and a second copy could only drift
|
|
667
|
+
* from it.
|
|
668
|
+
*
|
|
669
|
+
* Keyed by the **schema name** an author writes, so a host, a `required`
|
|
670
|
+
* marking and a problem message all index by one vocabulary. Two rows share
|
|
671
|
+
* the bare word `width`, so each is qualified by what carries it.
|
|
672
|
+
*
|
|
673
|
+
* It covers the declarations every official target states one fate for. A
|
|
674
|
+
* matrix row that summarises more than one declaration, or that describes a
|
|
675
|
+
* behaviour rather than a declaration, states no single fate and is absent
|
|
676
|
+
* here — an absent key means the spec does not answer, never that this target
|
|
677
|
+
* has no answer.
|
|
678
|
+
*/
|
|
679
|
+
export const capabilities = Object.freeze({
|
|
680
|
+
target: "html",
|
|
681
|
+
declarations: Object.freeze({
|
|
682
|
+
"page.header": "unread",
|
|
683
|
+
"page.footer": "unread",
|
|
684
|
+
break: "approximated",
|
|
685
|
+
reset: "withdrawn",
|
|
686
|
+
"page.margin": "resolved",
|
|
687
|
+
"header.height": "withdrawn",
|
|
688
|
+
groups: "resolved",
|
|
689
|
+
label: "withdrawn",
|
|
690
|
+
collapsed: "resolved",
|
|
691
|
+
"column.width": "approximated",
|
|
692
|
+
"slot.width": "resolved",
|
|
693
|
+
span: "resolved",
|
|
694
|
+
uppercase: "resolved",
|
|
695
|
+
href: "resolved",
|
|
696
|
+
spaceBefore: "resolved",
|
|
697
|
+
spaceAfter: "resolved",
|
|
698
|
+
format: "resolved",
|
|
699
|
+
family: "resolved",
|
|
700
|
+
valign: "resolved",
|
|
701
|
+
runs: "resolved",
|
|
702
|
+
alt: "resolved",
|
|
703
|
+
}),
|
|
704
|
+
});
|
package/lib/style.css
CHANGED
|
@@ -73,8 +73,10 @@
|
|
|
73
73
|
white-space: pre-line;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/* 1.4x the size it sits in, so a report default scales its headline with
|
|
77
|
+
its body. Unstyled, that is the host page's own body size. */
|
|
76
78
|
.q-item.q-report-header {
|
|
77
|
-
font-size:
|
|
79
|
+
font-size: 1.4em;
|
|
78
80
|
font-weight: bold;
|
|
79
81
|
}
|
|
80
82
|
|
|
@@ -105,7 +107,9 @@
|
|
|
105
107
|
* Fragmentation. SCHEMA.md calls these the print-CSS contract, so they ship
|
|
106
108
|
* with the look rather than being left to the host to rediscover: `thead`
|
|
107
109
|
* repeats per printed page, a table row stays whole, a group header travels
|
|
108
|
-
* with its rows, and a group
|
|
110
|
+
* with its rows, and each edge a group's `break` position turns gets one. The
|
|
111
|
+
* trailing edge needs its own rule: `break-before` cannot say what falls after
|
|
112
|
+
* the last instance of a run.
|
|
109
113
|
*
|
|
110
114
|
* Pages are not the only fragmentation context: the columns case lives in the
|
|
111
115
|
* shared block above, because a sheet with no pages can still have columns.
|
|
@@ -132,3 +136,7 @@
|
|
|
132
136
|
.q-break {
|
|
133
137
|
break-before: page;
|
|
134
138
|
}
|
|
139
|
+
|
|
140
|
+
.q-break-after {
|
|
141
|
+
break-after: page;
|
|
142
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quario/html",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.11.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": {
|
|
@@ -27,24 +34,25 @@
|
|
|
27
34
|
"access": "public"
|
|
28
35
|
},
|
|
29
36
|
"scripts": {
|
|
30
|
-
"check": "npm run size && npm test
|
|
37
|
+
"check": "npm run size && npm test",
|
|
38
|
+
"coverage:check": "c8 report --src lib/ --temp-directory=../../coverage/tmp --reporter=text --check-coverage --100",
|
|
31
39
|
"size": "size-limit",
|
|
32
|
-
"test": "npm run test:unit && npm run test:types",
|
|
40
|
+
"test": "npm run test:unit && npm run test:browser && npm run test:types && npm run coverage:check",
|
|
33
41
|
"test:browser": "node test/browser/setup.js",
|
|
34
42
|
"test:types": "tsc && attw --pack . --profile esm-only --exclude-entrypoints style.css",
|
|
35
|
-
"test:unit": "node --disallow-code-generation-from-strings --test --test-concurrency=1 test/*.test.js",
|
|
43
|
+
"test:unit": "c8 --clean=false --src lib/ --reporter=none --temp-directory=../../coverage/tmp node --disallow-code-generation-from-strings --test --test-concurrency=1 test/*.test.js",
|
|
36
44
|
"prepack": "node -e \"require('fs').copyFileSync('../../LICENSE','LICENSE')\"",
|
|
37
45
|
"postpack": "node -e \"require('fs').rmSync('LICENSE',{force:true})\""
|
|
38
46
|
},
|
|
39
47
|
"devDependencies": {
|
|
40
48
|
"@arethetypeswrong/cli": "^0.18.3",
|
|
41
49
|
"@size-limit/preset-small-lib": "^13.0.3",
|
|
42
|
-
"quario": "^0.
|
|
50
|
+
"quario": "^0.11.0",
|
|
43
51
|
"size-limit": "^13.0.3",
|
|
44
52
|
"typescript": "^7.0.2"
|
|
45
53
|
},
|
|
46
54
|
"peerDependencies": {
|
|
47
|
-
"quario": "^0.
|
|
55
|
+
"quario": "^0.11.0"
|
|
48
56
|
},
|
|
49
57
|
"size-limit": [
|
|
50
58
|
{
|