@quario/html 0.10.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 +16 -0
- package/README.md +1 -0
- package/lib/index.d.ts +24 -0
- package/lib/index.js +96 -23
- package/lib/style.css +3 -1
- package/package.json +7 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
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
|
+
|
|
3
19
|
## 0.10.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -121,6 +121,7 @@ change.
|
|
|
121
121
|
| Emits | For |
|
|
122
122
|
| -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
123
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 |
|
|
124
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` |
|
|
125
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 |
|
|
126
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` |
|
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 = {
|
|
@@ -43,19 +53,33 @@ let shown = (value, style, intl) => {
|
|
|
43
53
|
// not a difference against the cell's -- the engine already composed the two.
|
|
44
54
|
// An unstyled run stays bare.
|
|
45
55
|
/** @type {(styled: { style: any, tokens: any[] }, style: any, intl: any, map: any) => string} */
|
|
46
|
-
let
|
|
47
|
-
let
|
|
56
|
+
let styledRun = (styled, style, intl, map) => {
|
|
57
|
+
let worn = styled.style ?? style;
|
|
58
|
+
let body = linked(joined(styled.tokens, worn, intl), worn);
|
|
48
59
|
if (!styled.style) return body;
|
|
49
60
|
let inline = css(styled.style, map);
|
|
50
61
|
return inline ? '<span style="' + esc(inline) + '">' + body + "</span>" : body;
|
|
51
62
|
};
|
|
52
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
|
+
|
|
53
77
|
// A cell's markup: its styled runs in order. The grouping is the engine's
|
|
54
78
|
// `styledRuns`, so this target cannot drift from the others.
|
|
55
79
|
/** @type {(map: any) => (tokens: any[], style?: any, intl?: any) => string} */
|
|
56
80
|
let cellMarkup = (map) => (tokens, style, intl) => {
|
|
57
81
|
let out = "";
|
|
58
|
-
for (let styled of styledRuns(tokens)) out +=
|
|
82
|
+
for (let styled of styledRuns(tokens)) out += styledRun(styled, style, intl, map);
|
|
59
83
|
return out;
|
|
60
84
|
};
|
|
61
85
|
|
|
@@ -86,14 +110,12 @@ let NAMEABLE = /^[A-Za-z0-9 _-]+$/;
|
|
|
86
110
|
let UNSAFE = /[;}]/;
|
|
87
111
|
/** @type {(value: any) => boolean} */
|
|
88
112
|
let isHex = (value) => typeof value === "string" && HEX.test(value);
|
|
89
|
-
let ALIGNMENTS = ["left", "center", "right"];
|
|
90
113
|
// A slot item's `valign` and `align` as grid placement (see `placed`); the
|
|
91
114
|
// vertical vocabulary is these keys, so the three readers of it agree.
|
|
92
115
|
/** @type {Record<string, string>} */
|
|
93
116
|
let ALIGN_CONTENT = { top: "start", middle: "center", bottom: "end" };
|
|
94
117
|
/** @type {Record<string, string>} */
|
|
95
118
|
let JUSTIFY_ITEMS = { left: "start", center: "center", right: "end" };
|
|
96
|
-
let VALIGNMENTS = Object.keys(ALIGN_CONTENT);
|
|
97
119
|
|
|
98
120
|
// A group instance's own bands. They sit outside a columned container only when
|
|
99
121
|
// that instance is the node which declared the count — an inner instance's are
|
|
@@ -149,27 +171,29 @@ let named = (map, value) => {
|
|
|
149
171
|
if (Object.hasOwn(map, key)) return map[key];
|
|
150
172
|
return NAMEABLE.test(value) ? "'" + value + "'" : "";
|
|
151
173
|
};
|
|
152
|
-
//
|
|
153
|
-
//
|
|
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.
|
|
154
177
|
/** @type {(on: string, off: string) => (value: any) => string} */
|
|
155
|
-
let flag = (on, off) => (value) => (value
|
|
178
|
+
let flag = (on, off) => (value) => (value ? on : off);
|
|
156
179
|
/** @type {Record<string, (value: any, map: Record<string, string>) => string>} */
|
|
157
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.
|
|
158
183
|
family: (value, map) => {
|
|
159
|
-
if (typeof value !== "string" || !value) return "";
|
|
160
184
|
let name = named(map, value);
|
|
161
185
|
return name ? "font-family:" + name : "";
|
|
162
186
|
},
|
|
163
|
-
size: (value) =>
|
|
187
|
+
size: (value) => "font-size:" + value + "pt",
|
|
164
188
|
bold: flag("font-weight:bold", "font-weight:normal"),
|
|
165
189
|
italic: flag("font-style:italic", "font-style:normal"),
|
|
166
|
-
color: (value) =>
|
|
167
|
-
background: (value) =>
|
|
168
|
-
align: (value) =>
|
|
190
|
+
color: (value) => "color:" + value,
|
|
191
|
+
background: (value) => "background-color:" + value,
|
|
192
|
+
align: (value) => "text-align:" + value,
|
|
169
193
|
// The table reading, on a `<tr>` or a cell: the browser's own sheet gives
|
|
170
194
|
// cells `vertical-align: inherit`, so a row's reaches them like `bold` does.
|
|
171
195
|
// `placed` below lifts the name out before this map sees a slot item's.
|
|
172
|
-
valign: (value) =>
|
|
196
|
+
valign: (value) => "vertical-align:" + value,
|
|
173
197
|
// The text itself is untouched, so selection and screen readers get what
|
|
174
198
|
// the author wrote; only its rendering is capitalised.
|
|
175
199
|
uppercase: flag("text-transform:uppercase", "text-transform:none"),
|
|
@@ -177,14 +201,12 @@ let CSS = {
|
|
|
177
201
|
|
|
178
202
|
let SIDES = ["Top", "Right", "Bottom", "Left"];
|
|
179
203
|
let LINES = ["solid", "dashed", "dotted"];
|
|
180
|
-
/** @type {(value: any) => boolean} */
|
|
181
|
-
let isPad = (value) => Number.isFinite(value) && value >= 0;
|
|
182
204
|
for (let side of SIDES) {
|
|
183
205
|
let edge = side.toLowerCase();
|
|
184
|
-
CSS["padding" + side] = (value) =>
|
|
206
|
+
CSS["padding" + side] = (value) => "padding-" + edge + ":" + value + "pt";
|
|
185
207
|
}
|
|
186
|
-
CSS.spaceBefore = (value) =>
|
|
187
|
-
CSS.spaceAfter = (value) =>
|
|
208
|
+
CSS.spaceBefore = (value) => "margin-top:" + value + "pt";
|
|
209
|
+
CSS.spaceAfter = (value) => "margin-bottom:" + value + "pt";
|
|
188
210
|
|
|
189
211
|
/** @type {(width: any) => boolean} */
|
|
190
212
|
let isStroke = (width) => Number.isFinite(width) && width > 0;
|
|
@@ -211,11 +233,12 @@ let decoLine = (style) => {
|
|
|
211
233
|
return parts;
|
|
212
234
|
};
|
|
213
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.
|
|
214
238
|
/** @type {(style: any) => string} */
|
|
215
239
|
let decoration = (style) => {
|
|
216
240
|
let parts = decoLine(style);
|
|
217
|
-
|
|
218
|
-
return style.underline === false || style.strikethrough === false ? "text-decoration:none" : "";
|
|
241
|
+
return parts.length ? "text-decoration:" + parts.join(" ") : "text-decoration:none";
|
|
219
242
|
};
|
|
220
243
|
|
|
221
244
|
/** @type {(name: string) => boolean} */
|
|
@@ -494,7 +517,7 @@ export function html(options) {
|
|
|
494
517
|
};
|
|
495
518
|
// Hidden items never reach the stream and a hidden cell arrives with no
|
|
496
519
|
// tokens, so no visibility checks are needed in any handler below.
|
|
497
|
-
await walk(splits(stream(data)), {
|
|
520
|
+
await walk(collapse(splits(stream(data))), {
|
|
498
521
|
// Nothing unless the render is unlicensed — then the fragment opens
|
|
499
522
|
// with the marking badge: the engine's wording in this target's
|
|
500
523
|
// element, escaped, so the badge stays childless whatever stream a
|
|
@@ -629,3 +652,53 @@ export function html(options) {
|
|
|
629
652
|
},
|
|
630
653
|
};
|
|
631
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
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quario/html",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Tiny, escape-by-default HTML render target for quario. Semantic tables and a stable class contract.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"csp",
|
|
@@ -34,24 +34,25 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
|
-
"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",
|
|
38
39
|
"size": "size-limit",
|
|
39
|
-
"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",
|
|
40
41
|
"test:browser": "node test/browser/setup.js",
|
|
41
42
|
"test:types": "tsc && attw --pack . --profile esm-only --exclude-entrypoints style.css",
|
|
42
|
-
"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",
|
|
43
44
|
"prepack": "node -e \"require('fs').copyFileSync('../../LICENSE','LICENSE')\"",
|
|
44
45
|
"postpack": "node -e \"require('fs').rmSync('LICENSE',{force:true})\""
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
48
|
"@arethetypeswrong/cli": "^0.18.3",
|
|
48
49
|
"@size-limit/preset-small-lib": "^13.0.3",
|
|
49
|
-
"quario": "^0.
|
|
50
|
+
"quario": "^0.11.0",
|
|
50
51
|
"size-limit": "^13.0.3",
|
|
51
52
|
"typescript": "^7.0.2"
|
|
52
53
|
},
|
|
53
54
|
"peerDependencies": {
|
|
54
|
-
"quario": "^0.
|
|
55
|
+
"quario": "^0.11.0"
|
|
55
56
|
},
|
|
56
57
|
"size-limit": [
|
|
57
58
|
{
|