@quario/html 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,89 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] - 2026-09-01
11
+
12
+ ### Added
13
+
14
+ - **`html({ fonts })` says what a family name means on your page.** A report
15
+ declares the typeface it wants — `family: "Instrument Sans"`, or the
16
+ portable `family: "mono"` — and this option resolves that name to whatever
17
+ CSS means on the page it is displayed in:
18
+ `html({ fonts: { "Instrument Sans": "var(--font-instrument-sans)" } })`. Any
19
+ CSS `font-family` value works, so a custom property, a font stack or a
20
+ quoted name are all fine; a value containing `;` or `}` throws from `html()`
21
+ rather than at render. Names match case-insensitively, and `sans`, `serif`
22
+ and `mono` can be mapped like any other, which is what lets one definition
23
+ say "the mono one" and each surface answer for itself. Useful wherever your
24
+ faces are registered under names you do not control — a build tool that
25
+ hashes `@font-face` names, for instance. The mapping resolves families a
26
+ report declares; the face of text that declares none stays your
27
+ stylesheet's, so it is still one CSS rule away.
28
+
29
+ - **The report default rides a report root.** Every fragment is now wrapped in
30
+ one `<div class="q-report">`, and a report that declares a `style` carries it
31
+ there as an inline `font-family`/`font-size` — written once for the document
32
+ rather than repeated on every item, with CSS inheritance doing the rest. The
33
+ unlicensed marking stays outside the root, as the fragment's first element
34
+ and the root's sibling, so no authored style can reach it.
35
+
36
+ - **`uppercase` maps to `text-transform:uppercase`.** The markup carries the
37
+ text exactly as authored and only its rendering is capitalised, so what a
38
+ reader selects, copies, or hears from a screen reader is your own casing.
39
+
40
+ - **Splits render as a flex row of sized slots.** A
41
+ [split](https://getquario.com/docs) becomes
42
+ `<div class="q-split q-<role>">` carrying inline `display:flex`, with each
43
+ slot a `<div class="q-slot">` sized inline — `width:<n>%` for an authored
44
+ share, `flex:1` for a width-less one. The slot item inside keeps its
45
+ ordinary `q-item` container, so an item's markup never depends on where it
46
+ sits. Placement is emitted inline for the same reason page columns are: it
47
+ is document structure, not one of the visual defaults this target leaves to
48
+ your stylesheet.
49
+ - **`html({ paths: true })` maps rendered output back to the schema.** Each
50
+ element whose event carries a schema path gains `data-q-path`: item and
51
+ image containers, each group instance's container, the table, and every
52
+ `<th>`/`<td>` (the column definition's path; total cells their own entry).
53
+ One definition renders many times, so one path appears on every instance of
54
+ it. Escaped like every generated attribute, and off by default — the
55
+ attribute is weight a plain display pays for nothing.
56
+
57
+ ### Changed
58
+
59
+ - **A family name that could not be written as CSS now falls back instead of
60
+ being rewritten.** A `family` carrying characters that cannot appear in a
61
+ family name was stripped down to whatever survived and emitted anyway, which
62
+ named a face that could not exist — `family: "var(--my-font)"` became
63
+ `font-family:'var--my-font'`. Such a name now contributes nothing, so the
64
+ text renders in the face above it: the report default, or your stylesheet's
65
+ baseline. Ordinary names are unaffected. To resolve a name your page knows
66
+ under a different one, map it with `html({ fonts })`.
67
+
68
+ - **The fragment now has a root element.** `<div class="q-report">` wraps every
69
+ render, whether or not a report declares a default. Host CSS that assumed
70
+ items and tables were the fragment's top-level children needs a descendant
71
+ selector; nothing else about the markup moved.
72
+
73
+ - **The reference stylesheet's baseline face moved onto `.q-report`.** It was
74
+ on `.q-item`, `.q-table` and `.q-unlicensed` — a direct rule, which would
75
+ have beaten the report default an author writes on the document. The marking
76
+ keeps its own rule, being outside the root. Same look, one rule up.
77
+
78
+ - **A bare `Date` escapes to ISO 8601 UTC, the same on every machine.**
79
+ Interpolated `Date` values rendered through `String(date)`, which bakes the
80
+ host's timezone and locale into the markup. They now render as
81
+ `toISOString()` text through the engine's shared display rule — escaped
82
+ like every other value — so the same report produces the same HTML
83
+ everywhere. Formatted dates keep coming from registered functions.
84
+
85
+ ### Fixed
86
+
87
+ - **The reference stylesheet sets a base font family.** A fragment dropped
88
+ into a page with no font of its own inherited the browser default — a serif
89
+ — where every other target rendered a grotesque. `@quario/html/style.css`
90
+ now sets `sans-serif` on report text, as an ordinary rule any host overrides
91
+ by source order.
92
+
10
93
  ## [0.1.0] - 2026-08-27
11
94
 
12
95
  ### 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-group" data-group="region">
51
- <div class="q-item q-group-header" style="font-weight:bold">North</div>
52
- <table class="q-table">
53
- <colgroup><col><col></colgroup>
54
- <thead><tr><th>Product</th><th>Amount</th></tr></thead>
55
- <tbody><tr><td>Desk</td><td style="text-align:right">$500.00</td></tr></tbody>
56
- </table>
57
- <div class="q-item q-group-footer">Subtotal $500.00</div>
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 takes no options and returns the target you pass to `render`.
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,6 +103,7 @@ 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
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,12 +9,12 @@
9
9
  * never carries markup-escaped text.
10
10
  */
11
11
 
12
- import { isReportBand, text, walk } from "quario";
12
+ import { display, isReportBand, text, walk } from "quario";
13
13
 
14
14
  /** @type {Record<string, string>} */
15
15
  let ESC = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
16
16
  /** @type {(value: any) => string} */
17
- let esc = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ESC[char]);
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
@@ -47,9 +47,13 @@ let base64 = (bytes) => {
47
47
  // smuggle CSS of its own into the attribute. Unknown names are ignored.
48
48
  /** @type {Record<string, string>} */
49
49
  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
50
  let HEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
51
+ // A family name usable as it stands. Anything else would have to be rewritten
52
+ // to be emitted, and a rewritten name names a face that cannot exist.
53
+ let NAMEABLE = /^[A-Za-z0-9 _-]+$/;
54
+ // What a host's CSS value may not carry: `;` is what would turn one option
55
+ // into two declarations.
56
+ let UNSAFE = /[;}]/;
53
57
  /** @type {(value: any) => boolean} */
54
58
  let isHex = (value) => typeof value === "string" && HEX.test(value);
55
59
  let ALIGNMENTS = ["left", "center", "right"];
@@ -58,33 +62,60 @@ let ALIGNMENTS = ["left", "center", "right"];
58
62
  // that instance is the node which declared the count — an inner instance's are
59
63
  // content like anything else. The report's own bands are the engine's to name.
60
64
  let OWN = new Set(["group-header", "group-footer"]);
61
- /** @type {(value: string) => string} */
62
- let named = (value) => {
63
- let key = value.toLowerCase();
65
+ // A host font mapping (`html({ fonts })`), lowercased once so lookup matches
66
+ // `pdf({ fonts })`'s. Validated here rather than at render, because a
67
+ // malformed one is host configuration and belongs where the host wrote it --
68
+ // the value itself is trusted and emitted verbatim, as that target's font
69
+ // bytes are. `Error` and the `options.fonts.<name>:` prefix are that target's
70
+ // too: one option, one message shape.
71
+ /** @type {(path: string, value: any) => string} */
72
+ let cssValue = (path, value) => {
73
+ if (typeof value !== "string" || !value) throw Error(path + ": expected a CSS font-family value");
74
+ if (UNSAFE.test(value)) throw Error(path + ": cannot contain ';' or '}'");
75
+ return value;
76
+ };
77
+
78
+ /** @type {(fonts: any) => Record<string, string>} */
79
+ let mapping = (fonts) => {
80
+ if (fonts == null) return {};
81
+ if (typeof fonts !== "object")
82
+ throw Error("options.fonts: expected an object of family names to CSS values");
83
+ return Object.fromEntries(
84
+ Object.entries(fonts).map(([name, value]) => [
85
+ name.toLowerCase(),
86
+ cssValue("options.fonts." + name, value),
87
+ ]),
88
+ );
89
+ };
90
+
91
+ // Resolve a declared family name against the table the factory built -- the
92
+ // host's mapping laid over the three generics -- then the name itself. A name
93
+ // that is not nameable as it stands resolves to nothing rather than to a
94
+ // rewritten near-miss, so the element falls through to the layer below instead
95
+ // of naming a face that cannot exist (docs/adr/0034).
96
+ /** @type {(map: Record<string, string>, value: string) => string} */
97
+ let named = (map, value) => {
64
98
  // Own-key lookup: `constructor` must not resolve an inherited member.
65
- if (Object.hasOwn(FAMILY, key)) return FAMILY[key];
66
- let name = value.replace(/[^A-Za-z0-9 _-]/g, "");
67
- return name ? "'" + name + "'" : "";
99
+ let key = value.toLowerCase();
100
+ if (Object.hasOwn(map, key)) return map[key];
101
+ return NAMEABLE.test(value) ? "'" + value + "'" : "";
68
102
  };
69
- /** @type {Record<string, (value: any) => string>} */
103
+ /** @type {Record<string, (value: any, map: Record<string, string>) => string>} */
70
104
  let CSS = {
71
- family: (value) => {
105
+ family: (value, map) => {
72
106
  if (typeof value !== "string" || !value) return "";
73
- let name = named(value);
107
+ let name = named(map, value);
74
108
  return name ? "font-family:" + name : "";
75
109
  },
76
- size: (value) => (finite(value) && value > 0 ? "font-size:" + value + "pt" : ""),
110
+ size: (value) => (Number.isFinite(value) && value > 0 ? "font-size:" + value + "pt" : ""),
77
111
  bold: (value) => (value ? "font-weight:bold" : ""),
78
112
  italic: (value) => (value ? "font-style:italic" : ""),
79
113
  color: (value) => (isHex(value) ? "color:" + value : ""),
80
114
  background: (value) => (isHex(value) ? "background-color:" + value : ""),
81
115
  align: (value) => (ALIGNMENTS.includes(value) ? "text-align:" + value : ""),
82
- };
83
-
84
- /** @type {(name: string, value: any) => string} */
85
- let decl = (name, value) => {
86
- if (value == null || !Object.hasOwn(CSS, name)) return "";
87
- return CSS[name](value);
116
+ // The text itself is untouched, so selection and screen readers get what
117
+ // the author wrote; only its rendering is capitalised.
118
+ uppercase: (value) => (value ? "text-transform:uppercase" : ""),
88
119
  };
89
120
 
90
121
  /** @type {(style: any) => string} */
@@ -106,37 +137,62 @@ let pushDecoration = (out, style, seen) => {
106
137
  if (deco) out.push(deco);
107
138
  };
108
139
 
109
- /** @type {(out: string[], name: string, value: any) => void} */
110
- let pushDecl = (out, name, value) => {
111
- let declaration = decl(name, value);
140
+ /** @type {(out: string[], name: string, value: any, map: Record<string, string>) => void} */
141
+ let pushDecl = (out, name, value, map) => {
142
+ if (value == null || !Object.hasOwn(CSS, name)) return;
143
+ let declaration = CSS[name](value, map);
112
144
  if (declaration) out.push(declaration);
113
145
  };
114
146
 
115
147
  // Serialize a resolved style object to inline CSS, in declaration order.
116
148
  // underline and strikethrough share one CSS property, so the first of either
117
149
  // in authored order emits the composed value and the second is skipped.
118
- /** @type {(style: any) => string} */
119
- let css = (style) => {
150
+ /** @type {(style: any, map: Record<string, string>) => string} */
151
+ let css = (style, map) => {
120
152
  let out = /** @type {string[]} */ ([]);
121
153
  let seen = { decoration: false };
122
154
  for (let name in style) {
123
155
  if (isDecoration(name)) pushDecoration(out, style, seen);
124
- else pushDecl(out, name, style[name]);
156
+ else pushDecl(out, name, style[name], map);
125
157
  }
126
158
  return out.join(";");
127
159
  };
128
160
 
129
- // Compose a resolved style into one escaped inline style attribute.
130
- /** @type {(event: { style?: any }) => string} */
131
- let styleAttr = ({ style }) => {
132
- let out = style ? css(style) : "";
133
- return out ? ' style="' + esc(out) + '"' : "";
161
+ // Compose a resolved style into one escaped inline style attribute. `own` is
162
+ // this target's own structural CSS, which goes first and always emits: page
163
+ // columns and splits are placement, not the visual defaults this target
164
+ // refuses to supply, so they ride the same escaped attribute path as
165
+ // everything else rather than a second one (CLAUDE.md, the markup edge).
166
+ /** @type {(fonts: any) => (event: { style?: any }, own?: string) => string} */
167
+ let styling = (fonts) => {
168
+ // The spread is the precedence the option promises: a host mapping is
169
+ // consulted before the three generics, and may replace one. `mapping`
170
+ // lowercased its keys to match the ones already here.
171
+ let map = { ...FAMILY, ...mapping(fonts) };
172
+ return ({ style }, own = "") => {
173
+ let out = [own, style && css(style, map)].filter(Boolean).join(";");
174
+ return out ? ' style="' + esc(out) + '"' : "";
175
+ };
134
176
  };
135
177
 
136
- /** @type {(list: any[], tag: string) => string} */
137
- let cells = (list, tag) =>
178
+ // One slot's wrapper. An authored share is that width; a width-less slot
179
+ // divides what the sized ones leave, exactly as a width-less column does.
180
+ /** @type {(slot: { width?: number } | undefined) => string} */
181
+ let slotAttr = (slot) =>
182
+ ' style="' + esc(slot && slot.width != null ? "width:" + slot.width + "%" : "flex:1") + '"';
183
+
184
+ // The identity attribute, escaped like every other generated value at this
185
+ // edge. `mark` is the factory's verdict: a render without `paths` maps every
186
+ // event to "" through the same function rather than re-deciding per call site.
187
+ /** @type {(event: { path?: string }) => string} */
188
+ let pathAttr = ({ path }) => (path ? ' data-q-path="' + esc(path) + '"' : "");
189
+ /** @type {() => string} */
190
+ let noAttr = () => "";
191
+
192
+ /** @type {(list: any[], tag: string, attrs: (event: any) => string) => string} */
193
+ let cells = (list, tag, attrs) =>
138
194
  list
139
- .map((cell) => "<" + tag + styleAttr(cell) + ">" + markup(cell.tokens) + "</" + tag + ">")
195
+ .map((cell) => "<" + tag + attrs(cell) + ">" + markup(cell.tokens) + "</" + tag + ">")
140
196
  .join("");
141
197
 
142
198
  /**
@@ -149,10 +205,31 @@ let cells = (list, tag) =>
149
205
  * fragment, so `report-start.page` band closures are ignored — page furniture
150
206
  * belongs to the host's print CSS.
151
207
  *
208
+ * `paths` opts the fragment into identity: each element whose event carries a
209
+ * schema `path` gains `data-q-path`, so a consumer can map rendered output
210
+ * back to the definition behind it — the editor's selection seam. Off by
211
+ * default, because the attribute is weight every plain display pays for
212
+ * nothing.
213
+ *
214
+ * `fonts` is this target's font mapping: a declared `family` name to a CSS
215
+ * value, so a host says what the name means on this page. Malformed values
216
+ * throw from here (docs/adr/0034).
217
+ *
218
+ * The options are described once, in the hand-written public declarations,
219
+ * and read back here -- a second copy in JSDoc is a copy that drifts.
220
+ * @import { HtmlOptions } from './index.d.ts'
221
+ *
222
+ * @param {HtmlOptions} [options] Host options for this target.
152
223
  * @returns {{name: "html", compile: (stream: any) => (data?: any) => Promise<string>}}
153
224
  * The target (see SCHEMA.md, "Instances and targets").
154
225
  */
155
- export function html() {
226
+ export function html(options) {
227
+ let mark = options?.paths ? pathAttr : noAttr;
228
+ // The factory's verdict, like `mark`: the mapping is validated once, here.
229
+ let styleAttr = styling(options?.fonts);
230
+ // The pair every cell carries, bound once rather than threaded as two.
231
+ /** @type {(cell: any) => string} */
232
+ let attrs = (cell) => styleAttr(cell) + mark(cell);
156
233
  return {
157
234
  name: "html",
158
235
  compile: (stream) => async (data) => {
@@ -166,6 +243,10 @@ export function html() {
166
243
  // Whether a `total-row` already closed the `<tbody>` — markup
167
244
  // bookkeeping that stays at this edge, out of the event contract.
168
245
  let tfoot = false;
246
+ // Whether `report-start` opened the report root. A stream may start
247
+ // part-way through (the walk driver reads an event's type and nothing
248
+ // else), and an unbalanced closing tag would be this target's own bug.
249
+ let rooted = false;
169
250
  // The page columns a node declared: its `count`, the `owner` depth that
170
251
  // declared it (-1 for the report root), and whether the container is
171
252
  // emitted and still to be closed. Null when no node owes one. Never more
@@ -178,6 +259,23 @@ export function html() {
178
259
  // that may own the region. Taken from each event rather than counted, so
179
260
  // there is one spelling of a depth the stream already carries.
180
261
  let depth = 0;
262
+ // The split being filled, if any: the slot geometry the bracket carried
263
+ // and how many slots have been placed. Splits never nest, so one is
264
+ // enough. Null whenever the walk is not inside a bracket.
265
+ /** @type {{ slots: any[], at: number } | null} */
266
+ let split = null;
267
+ // Place one item's markup: inside a bracket it goes in the next slot,
268
+ // and everywhere else it is a band item like any other.
269
+ /** @type {(event: any, fragment: string) => void} */
270
+ let emit = (event, fragment) => {
271
+ if (!split) {
272
+ content(event);
273
+ out += fragment;
274
+ return;
275
+ }
276
+ out +=
277
+ '<div class="q-slot"' + slotAttr(split.slots[split.at++]) + ">" + fragment + "</div>";
278
+ };
181
279
  // Does this event stay full-width, outside the columns? The declaring
182
280
  // node's own bands do.
183
281
  /** @type {(event: any) => boolean} */
@@ -220,20 +318,44 @@ export function html() {
220
318
  // against.
221
319
  "report-start": (event) => {
222
320
  if (event.marking) out += '<div class="q-unlicensed">' + esc(event.marking) + "</div>";
321
+ // The report root. Always emitted, because the fragment otherwise
322
+ // has no element for a document-wide declaration to be inherited
323
+ // from, and the shipped stylesheet's own baseline now hangs off it.
324
+ // The marking stays outside: inside, an author's `style` would
325
+ // reach it, and a marking an author can shrink is one ADR 0002
326
+ // does not allow.
327
+ out += '<div class="q-report"' + styleAttr(event) + ">";
328
+ rooted = true;
223
329
  if (event.columns) region = { count: event.columns, owner: -1, open: false };
224
330
  },
225
331
  // The stable `q-item q-<role>` classes are the whole class attribute;
226
332
  // print CSS targets the band-role class for styling and breaks.
227
333
  item: (event) => {
334
+ emit(
335
+ event,
336
+ '<div class="q-item q-' +
337
+ event.role +
338
+ '"' +
339
+ styleAttr(event) +
340
+ mark(event) +
341
+ ">" +
342
+ markup(event.tokens) +
343
+ "</div>",
344
+ );
345
+ },
346
+ // A split places its slots across the content width instead of down
347
+ // the band. The slot items inside keep their ordinary markup, so a
348
+ // consumer of this fragment reads the same item container wherever it
349
+ // sits; only the wrapper around it is new.
350
+ "split-start": (event) => {
228
351
  content(event);
229
352
  out +=
230
- '<div class="q-item q-' +
231
- event.role +
232
- '"' +
233
- styleAttr(event) +
234
- ">" +
235
- markup(event.tokens) +
236
- "</div>";
353
+ '<div class="q-split q-' + event.role + '"' + styleAttr(event, "display:flex") + ">";
354
+ split = { slots: event.slots, at: 0 };
355
+ },
356
+ "split-end": () => {
357
+ split = null;
358
+ out += "</div>";
237
359
  },
238
360
  // An image is an item container of its own holding one `<img>`: the
239
361
  // band-role class joins `q-item q-image`, `align`/`background` dress
@@ -245,25 +367,27 @@ export function html() {
245
367
  // never the markup-passing join a cell body gets; an image without one
246
368
  // carries `alt=""`, the decorative-image convention.
247
369
  image: (event) => {
248
- content(event);
249
370
  let source = sources.get(event.bytes);
250
371
  if (source === undefined)
251
372
  sources.set(
252
373
  event.bytes,
253
374
  (source = esc("data:image/" + event.format + ";base64," + base64(event.bytes))),
254
375
  );
255
- out +=
376
+ emit(
377
+ event,
256
378
  '<div class="q-item q-image q-' +
257
- event.role +
258
- '"' +
259
- styleAttr(event) +
260
- '><img src="' +
261
- source +
262
- '" alt="' +
263
- esc(text(event.alt || [])) +
264
- // Both keep the aspect ratio, since neither names a height.
265
- (event.fit === "width" ? '" style="width:100%">' : '" style="max-width:100%">') +
266
- "</div>";
379
+ event.role +
380
+ '"' +
381
+ styleAttr(event) +
382
+ mark(event) +
383
+ '><img src="' +
384
+ source +
385
+ '" alt="' +
386
+ esc(text(event.alt || [])) +
387
+ // Both keep the aspect ratio, since neither names a height.
388
+ (event.fit === "width" ? '" style="width:100%">' : '" style="max-width:100%">') +
389
+ "</div>",
390
+ );
267
391
  },
268
392
  // Each group instance is wrapped in a `q-group` container carrying
269
393
  // the group name, so print CSS can keep a header with its rows
@@ -277,6 +401,7 @@ export function html() {
277
401
  '<div class="q-group' +
278
402
  (event.break || event.reset ? " q-break" : "") +
279
403
  '"' +
404
+ mark(event) +
280
405
  ' data-group="' +
281
406
  esc(event.name) +
282
407
  '">';
@@ -303,22 +428,26 @@ export function html() {
303
428
  for (let column of /** @type {any[]} */ (event.columns))
304
429
  cols +=
305
430
  "<col" + (column.width ? ' style="width:' + esc(column.width) + '%"' : "") + ">";
306
- let headers = event.columns.map((/** @type {any} */ column) => column.header);
431
+ let headers = event.columns.map((/** @type {any} */ column) =>
432
+ Object.assign({}, column.header, { path: column.path }),
433
+ );
307
434
  out +=
308
- '<table class="q-table">' +
435
+ '<table class="q-table"' +
436
+ mark(event) +
437
+ ">" +
309
438
  "<colgroup>" +
310
439
  cols +
311
440
  "</colgroup>" +
312
441
  "<thead><tr>" +
313
- cells(headers, "th") +
442
+ cells(headers, "th", attrs) +
314
443
  "</tr></thead><tbody>";
315
444
  },
316
445
  row: (event) => {
317
- out += "<tr" + styleAttr(event) + ">" + cells(event.cells, "td") + "</tr>";
446
+ out += "<tr" + styleAttr(event) + ">" + cells(event.cells, "td", attrs) + "</tr>";
318
447
  },
319
448
  "total-row": (event) => {
320
449
  tfoot = true;
321
- out += "</tbody><tfoot><tr>" + cells(event.cells, "td") + "</tr></tfoot>";
450
+ out += "</tbody><tfoot><tr>" + cells(event.cells, "td", attrs) + "</tr></tfoot>";
322
451
  },
323
452
  "table-end": () => {
324
453
  out += tfoot ? "</table>" : "</tbody></table>";
@@ -327,7 +456,7 @@ export function html() {
327
456
  // A root region reaches here open when no full-width footer followed the
328
457
  // body it columns.
329
458
  close();
330
- return out;
459
+ return rooted ? out + "</div>" : out;
331
460
  },
332
461
  };
333
462
  }
package/lib/style.css CHANGED
@@ -23,6 +23,19 @@
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
+
26
39
  .q-table {
27
40
  width: 100%;
28
41
  border-collapse: collapse;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quario/html",
3
- "version": "0.1.0",
3
+ "version": "0.2.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.1.0",
42
+ "quario": "^0.2.0",
43
43
  "size-limit": "^13.0.3",
44
44
  "typescript": "^7.0.2"
45
45
  },
46
46
  "peerDependencies": {
47
- "quario": "^0.1.0"
47
+ "quario": "^0.2.0"
48
48
  },
49
49
  "size-limit": [
50
50
  {
51
51
  "path": "lib/index.js",
52
- "limit": "1.7 kB",
52
+ "limit": "2 kB",
53
53
  "ignore": [
54
54
  "quario"
55
55
  ]