@quario/html 0.2.0 → 0.4.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 +59 -0
- package/README.md +21 -18
- package/lib/index.js +138 -29
- package/lib/style.css +20 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,65 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-09-03
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **A split slot's box now fills the split's height.** Before, a slot's
|
|
15
|
+
background and border stopped at that slot's own content, leaving a gap under
|
|
16
|
+
any slot shorter than its neighbours. The slot wrapper now carries inline
|
|
17
|
+
`display:grid`, so the item inside it fills the row; a slot's item markup is
|
|
18
|
+
unchanged. See the `@quario/layout` changelog for the rule and for what it
|
|
19
|
+
costs a report that relied on the short box.
|
|
20
|
+
|
|
21
|
+
- **A date string under `format: "date"` now presents as a date.** The
|
|
22
|
+
engine's `format()` helper revives the two read forms, so a cell that
|
|
23
|
+
rendered `2026-08-14` verbatim now renders the presented date. See the
|
|
24
|
+
`quario` changelog for the forms and the timezone rule.
|
|
25
|
+
|
|
26
|
+
## [0.3.0] - 2026-09-02
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **`format` stringifies at the markup edge.** The kind is presented through
|
|
31
|
+
the engine's `format()` helper from the instance locale, then escaped like
|
|
32
|
+
any other interpolated value.
|
|
33
|
+
|
|
34
|
+
- **`page.margin` becomes `@page{margin:<n>pt}`.** Report-header `height`
|
|
35
|
+
is unread: a CSS height would pin in the body box, not from the page top.
|
|
36
|
+
|
|
37
|
+
- **`spaceBefore` / `spaceAfter` map to `margin-top` / `margin-bottom`.**
|
|
38
|
+
Report and group containers are flex columns so adjacent authored gaps add
|
|
39
|
+
rather than collapse.
|
|
40
|
+
|
|
41
|
+
- **Padding and complete border sides map to inline CSS.**
|
|
42
|
+
`box-sizing:border-box` rides the same attribute. A side that does not
|
|
43
|
+
resolve all three names emits nothing, so a missing colour never becomes
|
|
44
|
+
the browser's solid black. Header-row, row, and total-row boxes are
|
|
45
|
+
withdrawn from `<tr>` — CSS does not box a table row — while other row
|
|
46
|
+
styles still inherit.
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- **`<tfoot>` holds every total row.** Extra rows are extra `<tr>`; the
|
|
51
|
+
section still closes at `table-end`.
|
|
52
|
+
|
|
53
|
+
- **A visible text item occupies a line, and a literal newline is a break.**
|
|
54
|
+
Before, an empty item collapsed and a newline became a space. The honor
|
|
55
|
+
rules now hang off `.q-item` and `.q-table th, td` in the reference
|
|
56
|
+
stylesheet, the same pattern as `.q-break` — class is the hook, the rule
|
|
57
|
+
is overridable. Empty table cells still collapse. Host CSS still owns
|
|
58
|
+
leading; this is not `line-height: 1.4`.
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
|
|
62
|
+
- **A declared `false` flag now emits its CSS off-value.** `bold: false` used
|
|
63
|
+
to emit nothing, so the reference stylesheet's band-role `font-weight: bold`
|
|
64
|
+
still won on screen while PDF rendered regular. Exact `false` now writes
|
|
65
|
+
`font-weight:normal` (and the matching off-value for italic, uppercase, and
|
|
66
|
+
the decorations), which beats the sheet. Omit and a non-boolean result still
|
|
67
|
+
emit nothing.
|
|
68
|
+
|
|
10
69
|
## [0.2.0] - 2026-09-01
|
|
11
70
|
|
|
12
71
|
### Added
|
package/README.md
CHANGED
|
@@ -97,27 +97,28 @@ host. Compilation stays synchronous; render-time failures reject with the same l
|
|
|
97
97
|
These classes are the contract host CSS targets. They are stable, and changing them is a breaking
|
|
98
98
|
change.
|
|
99
99
|
|
|
100
|
-
| Emits | For
|
|
101
|
-
| ----------------------------------------- |
|
|
102
|
-
| `<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`
|
|
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"`
|
|
104
|
-
| `<table class="q-table">` | Each table, with a real `colgroup`, `thead`, `tbody`, and a `tfoot` when totals are declared
|
|
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">`
|
|
100
|
+
| Emits | For |
|
|
101
|
+
| ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
102
|
+
| `<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` |
|
|
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"` |
|
|
104
|
+
| `<table class="q-table">` | Each table, with a real `colgroup`, `thead`, `tbody`, and a `tfoot` when totals are declared |
|
|
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">` carrying inline `display:grid` and their share, each holding the slot item's ordinary container |
|
|
107
107
|
|
|
108
108
|
A column `width` becomes an inline `width:<n>%` on its `<col>`. The classes above belong to this
|
|
109
109
|
target. The schema never speaks in CSS. Displaying a fragment that contains images under a Content
|
|
110
110
|
Security Policy needs `img-src data:`.
|
|
111
111
|
|
|
112
|
-
Style declarations map to inline CSS (`bold` → `font-weight:bold`, `size` → `font-size:<n>pt`,
|
|
113
|
-
`family: "mono"` → `font-family:monospace`, …).
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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.
|
|
121
122
|
|
|
122
123
|
This target is unpaginated: it ignores schema page bands (page furniture belongs to your print CSS)
|
|
123
124
|
and maps `break: "page"` and `reset: "page"` to the `q-break` class. Page columns are accepted on
|
|
@@ -151,6 +152,7 @@ Host CSS targets these classes:
|
|
|
151
152
|
1. Render the fragment and wrap it in a document with two stylesheets inlined, in order:
|
|
152
153
|
`@quario/html/style.css` below, then your own.
|
|
153
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,
|
|
154
156
|
`.q-group { break-inside: avoid }` keeps a group header with its rows,
|
|
155
157
|
`.q-break { break-before: page }` honors the schema's own break hint, and `thead` repeats per
|
|
156
158
|
printed page. Yours adds the page geometry in `@page`, which only you can decide.
|
|
@@ -161,8 +163,9 @@ Host CSS targets these classes:
|
|
|
161
163
|
### The reference stylesheet
|
|
162
164
|
|
|
163
165
|
The package ships the default look as a real stylesheet, `@quario/html/style.css`: the band-role
|
|
164
|
-
weights and sizes, the table look, and
|
|
165
|
-
|
|
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:
|
|
166
169
|
|
|
167
170
|
```js
|
|
168
171
|
import { readFileSync } from "node:fs";
|
package/lib/index.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* never carries markup-escaped text.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { display, 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 = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
@@ -21,12 +21,18 @@ let esc = (value) => display(value).replace(/[&<>"']/g, (char) => ESC[char]);
|
|
|
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
|
|
@@ -100,6 +106,10 @@ let named = (map, value) => {
|
|
|
100
106
|
if (Object.hasOwn(map, key)) return map[key];
|
|
101
107
|
return NAMEABLE.test(value) ? "'" + value + "'" : "";
|
|
102
108
|
};
|
|
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 : "");
|
|
103
113
|
/** @type {Record<string, (value: any, map: Record<string, string>) => string>} */
|
|
104
114
|
let CSS = {
|
|
105
115
|
family: (value, map) => {
|
|
@@ -108,22 +118,68 @@ let CSS = {
|
|
|
108
118
|
return name ? "font-family:" + name : "";
|
|
109
119
|
},
|
|
110
120
|
size: (value) => (Number.isFinite(value) && value > 0 ? "font-size:" + value + "pt" : ""),
|
|
111
|
-
bold: (
|
|
112
|
-
italic: (
|
|
121
|
+
bold: flag("font-weight:bold", "font-weight:normal"),
|
|
122
|
+
italic: flag("font-style:italic", "font-style:normal"),
|
|
113
123
|
color: (value) => (isHex(value) ? "color:" + value : ""),
|
|
114
124
|
background: (value) => (isHex(value) ? "background-color:" + value : ""),
|
|
115
125
|
align: (value) => (ALIGNMENTS.includes(value) ? "text-align:" + value : ""),
|
|
116
126
|
// The text itself is untouched, so selection and screen readers get what
|
|
117
127
|
// the author wrote; only its rendering is capitalised.
|
|
118
|
-
uppercase: (
|
|
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
|
+
];
|
|
157
|
+
};
|
|
158
|
+
|
|
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;
|
|
119
176
|
};
|
|
120
177
|
|
|
121
178
|
/** @type {(style: any) => string} */
|
|
122
179
|
let decoration = (style) => {
|
|
123
|
-
let parts =
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
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" : "";
|
|
127
183
|
};
|
|
128
184
|
|
|
129
185
|
/** @type {(name: string) => boolean} */
|
|
@@ -144,17 +200,40 @@ let pushDecl = (out, name, value, map) => {
|
|
|
144
200
|
if (declaration) out.push(declaration);
|
|
145
201
|
};
|
|
146
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
|
+
|
|
147
226
|
// Serialize a resolved style object to inline CSS, in declaration order.
|
|
148
227
|
// underline and strikethrough share one CSS property, so the first of either
|
|
149
|
-
// in authored order emits the composed value and the second is skipped.
|
|
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.
|
|
150
231
|
/** @type {(style: any, map: Record<string, string>) => string} */
|
|
151
232
|
let css = (style, map) => {
|
|
152
233
|
let out = /** @type {string[]} */ ([]);
|
|
153
|
-
let seen = { decoration: false };
|
|
154
|
-
for (let name in style)
|
|
155
|
-
|
|
156
|
-
else pushDecl(out, name, style[name], map);
|
|
157
|
-
}
|
|
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");
|
|
158
237
|
return out.join(";");
|
|
159
238
|
};
|
|
160
239
|
|
|
@@ -177,9 +256,16 @@ let styling = (fonts) => {
|
|
|
177
256
|
|
|
178
257
|
// One slot's wrapper. An authored share is that width; a width-less slot
|
|
179
258
|
// divides what the sized ones leave, exactly as a width-less column does.
|
|
259
|
+
// `display:grid` is what gives a slot's box the split's height rather than its
|
|
260
|
+
// own content's: the wrapper already stretches as a flex item, and a grid makes
|
|
261
|
+
// its single child fill it in both axes. It belongs here and not on the
|
|
262
|
+
// fragment, because a slot's item is the same `q-item` div it would be
|
|
263
|
+
// anywhere else -- nothing about an item's markup depends on where it sits.
|
|
180
264
|
/** @type {(slot: { width?: number } | undefined) => string} */
|
|
181
265
|
let slotAttr = (slot) =>
|
|
182
|
-
' style="' +
|
|
266
|
+
' style="' +
|
|
267
|
+
esc("display:grid;" + (slot && slot.width != null ? "width:" + slot.width + "%" : "flex:1")) +
|
|
268
|
+
'"';
|
|
183
269
|
|
|
184
270
|
// The identity attribute, escaped like every other generated value at this
|
|
185
271
|
// edge. `mark` is the factory's verdict: a render without `paths` maps every
|
|
@@ -189,10 +275,13 @@ let pathAttr = ({ path }) => (path ? ' data-q-path="' + esc(path) + '"' : "");
|
|
|
189
275
|
/** @type {() => string} */
|
|
190
276
|
let noAttr = () => "";
|
|
191
277
|
|
|
192
|
-
/** @type {(list: any[], tag: string, attrs: (event: any) => string) => string} */
|
|
193
|
-
let cells = (list, tag, attrs) =>
|
|
278
|
+
/** @type {(list: any[], tag: string, attrs: (event: any) => string, intl: any) => string} */
|
|
279
|
+
let cells = (list, tag, attrs, intl) =>
|
|
194
280
|
list
|
|
195
|
-
.map(
|
|
281
|
+
.map(
|
|
282
|
+
(cell) =>
|
|
283
|
+
"<" + tag + attrs(cell) + ">" + markup(cell.tokens, cell.style, intl) + "</" + tag + ">",
|
|
284
|
+
)
|
|
196
285
|
.join("");
|
|
197
286
|
|
|
198
287
|
/**
|
|
@@ -240,7 +329,7 @@ export function html(options) {
|
|
|
240
329
|
// goes with the render — nothing here outlives it.
|
|
241
330
|
/** @type {Map<Uint8Array, string>} */
|
|
242
331
|
let sources = new Map();
|
|
243
|
-
// Whether a `total-row` already
|
|
332
|
+
// Whether a `total-row` already opened `<tfoot>` — markup
|
|
244
333
|
// bookkeeping that stays at this edge, out of the event contract.
|
|
245
334
|
let tfoot = false;
|
|
246
335
|
// Whether `report-start` opened the report root. A stream may start
|
|
@@ -264,6 +353,8 @@ export function html(options) {
|
|
|
264
353
|
// enough. Null whenever the walk is not inside a bracket.
|
|
265
354
|
/** @type {{ slots: any[], at: number } | null} */
|
|
266
355
|
let split = null;
|
|
356
|
+
/** @type {{ locale?: string, currency?: string, timeZone?: string } | null} */
|
|
357
|
+
let intl = null;
|
|
267
358
|
// Place one item's markup: inside a bracket it goes in the next slot,
|
|
268
359
|
// and everywhere else it is a band item like any other.
|
|
269
360
|
/** @type {(event: any, fragment: string) => void} */
|
|
@@ -317,7 +408,10 @@ export function html(options) {
|
|
|
317
408
|
// edge, so the badge stays childless whatever stream a host compiles
|
|
318
409
|
// against.
|
|
319
410
|
"report-start": (event) => {
|
|
411
|
+
intl = { locale: event.locale, currency: event.currency, timeZone: event.timeZone };
|
|
320
412
|
if (event.marking) out += '<div class="q-unlicensed">' + esc(event.marking) + "</div>";
|
|
413
|
+
if (event.margin != null)
|
|
414
|
+
out += "<style>@page{margin:" + esc(event.margin) + "pt}</style>";
|
|
321
415
|
// The report root. Always emitted, because the fragment otherwise
|
|
322
416
|
// has no element for a document-wide declaration to be inherited
|
|
323
417
|
// from, and the shipped stylesheet's own baseline now hangs off it.
|
|
@@ -339,7 +433,7 @@ export function html(options) {
|
|
|
339
433
|
styleAttr(event) +
|
|
340
434
|
mark(event) +
|
|
341
435
|
">" +
|
|
342
|
-
markup(event.tokens) +
|
|
436
|
+
markup(event.tokens, event.style, intl) +
|
|
343
437
|
"</div>",
|
|
344
438
|
);
|
|
345
439
|
},
|
|
@@ -438,19 +532,34 @@ export function html(options) {
|
|
|
438
532
|
"<colgroup>" +
|
|
439
533
|
cols +
|
|
440
534
|
"</colgroup>" +
|
|
441
|
-
"<thead><tr
|
|
442
|
-
|
|
535
|
+
"<thead><tr" +
|
|
536
|
+
styleAttr({ style: unbox(event.style) }) +
|
|
537
|
+
">" +
|
|
538
|
+
cells(headers, "th", attrs, intl) +
|
|
443
539
|
"</tr></thead><tbody>";
|
|
444
540
|
},
|
|
445
541
|
row: (event) => {
|
|
446
|
-
out +=
|
|
542
|
+
out +=
|
|
543
|
+
"<tr" +
|
|
544
|
+
styleAttr({ style: unbox(event.style) }) +
|
|
545
|
+
">" +
|
|
546
|
+
cells(event.cells, "td", attrs, intl) +
|
|
547
|
+
"</tr>";
|
|
447
548
|
},
|
|
448
549
|
"total-row": (event) => {
|
|
449
|
-
tfoot
|
|
450
|
-
|
|
550
|
+
if (!tfoot) {
|
|
551
|
+
out += "</tbody><tfoot>";
|
|
552
|
+
tfoot = true;
|
|
553
|
+
}
|
|
554
|
+
out +=
|
|
555
|
+
"<tr" +
|
|
556
|
+
styleAttr({ style: unbox(event.style) }) +
|
|
557
|
+
">" +
|
|
558
|
+
cells(event.cells, "td", attrs, intl) +
|
|
559
|
+
"</tr>";
|
|
451
560
|
},
|
|
452
561
|
"table-end": () => {
|
|
453
|
-
out += tfoot ? "</table>" : "</tbody></table>";
|
|
562
|
+
out += tfoot ? "</tfoot></table>" : "</tbody></table>";
|
|
454
563
|
},
|
|
455
564
|
});
|
|
456
565
|
// A root region reaches here open when no full-width footer followed the
|
package/lib/style.css
CHANGED
|
@@ -36,15 +36,31 @@
|
|
|
36
36
|
font-family: sans-serif;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
.q-report,
|
|
40
|
+
.q-group {
|
|
41
|
+
display: flex;
|
|
42
|
+
flex-direction: column;
|
|
43
|
+
}
|
|
44
|
+
|
|
39
45
|
.q-table {
|
|
40
46
|
width: 100%;
|
|
41
47
|
border-collapse: collapse;
|
|
42
48
|
}
|
|
43
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
|
+
|
|
44
59
|
.q-table th,
|
|
45
60
|
.q-table td {
|
|
46
61
|
padding: 2pt 6pt;
|
|
47
62
|
text-align: left;
|
|
63
|
+
white-space: pre-line;
|
|
48
64
|
}
|
|
49
65
|
|
|
50
66
|
.q-table thead th {
|
|
@@ -95,6 +111,10 @@
|
|
|
95
111
|
* Pages are not the only fragmentation context: the columns case lives in the
|
|
96
112
|
* shared block above, because a sheet with no pages can still have columns.
|
|
97
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
|
+
*
|
|
98
118
|
* A host overrides any of these the ordinary way; `example/print.css` turns
|
|
99
119
|
* `break-inside` back to `auto` for one group to let it span pages.
|
|
100
120
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quario/html",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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.4.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.4.0"
|
|
48
48
|
},
|
|
49
49
|
"size-limit": [
|
|
50
50
|
{
|
|
51
51
|
"path": "lib/index.js",
|
|
52
|
-
"limit": "2 kB",
|
|
52
|
+
"limit": "2.5 kB",
|
|
53
53
|
"ignore": [
|
|
54
54
|
"quario"
|
|
55
55
|
]
|