quario 0.4.0 → 0.5.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 +49 -0
- package/lib/index.d.ts +44 -3
- package/lib/index.js +1 -0
- package/lib/license.js +1 -1
- package/lib/plan.js +224 -66
- package/lib/stream.js +1 -3
- package/lib/style.js +112 -8
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2026-09-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`STYLE_NAMES`**, every name in the style vocabulary as a read-only list,
|
|
15
|
+
in the order the specification's table lists them. A tool that offers the
|
|
16
|
+
vocabulary — the editor's style rail — reads it from here instead of
|
|
17
|
+
carrying a copy that has to learn each new name.
|
|
18
|
+
- **`span` lets a cell cover several table columns**, so a label reaches across
|
|
19
|
+
them instead of being pushed sideways by empty cells. A literal positive
|
|
20
|
+
integer; absent is 1. It is legal on the two cells a document writes: a
|
|
21
|
+
column's `header`, where the columns it covers omit their own `header`, and a
|
|
22
|
+
total row's cells, whose spans must sum to the column count exactly. A data
|
|
23
|
+
row's cells are the columns' own, so there is no cell there to carry one — a
|
|
24
|
+
line that runs across the table is a `split` item below it. A span covers and
|
|
25
|
+
does not vote: a cell over more than one column has no say in their widths,
|
|
26
|
+
and a column nothing votes on takes the cell-padding floor.
|
|
27
|
+
- **`valign`** joins the style vocabulary: `"top"`, `"middle"` or `"bottom"`,
|
|
28
|
+
a literal or an `=` expression like `align`. It is legal exactly where a box
|
|
29
|
+
is taller than its content asked for — table cells, headers, totals, `row`,
|
|
30
|
+
and split slots, text or image — and a definition error on a stacked item and
|
|
31
|
+
on a band image, whose boxes have no such slack. A row's or a split's layers
|
|
32
|
+
under its cells' or slots' own. Undeclared is not a declaration: each target
|
|
33
|
+
keeps its own default.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **A table row's `style` now resolves onto that row's cells**, the box
|
|
38
|
+
included, instead of meaning something different on every output. Before,
|
|
39
|
+
padding and border on `detail.header`, `detail.row` or a total row's `style`
|
|
40
|
+
reached no cell at all: a fragment dropped them, a worksheet turned them into
|
|
41
|
+
an edge on each cell, and a page drew one box around the row. Now they layer
|
|
42
|
+
under each cell's own, the way `bold` and `valign` on a row already did, so
|
|
43
|
+
one declaration means one thing everywhere.
|
|
44
|
+
What this makes possible is declining the cell padding a row at a time:
|
|
45
|
+
`"paddingTop": 0, "paddingBottom": 0` on `detail.row` sets the pitch for
|
|
46
|
+
every data row, where before it had to be written on each cell.
|
|
47
|
+
Two things move for documents that already declared a box on a row.
|
|
48
|
+
A row's `borderLeft` is now an edge on each of the row's cells rather than
|
|
49
|
+
one at the row's outer left — write it on the first column's cells to get the
|
|
50
|
+
single edge back. And a row's border now occupies height, as a cell's border
|
|
51
|
+
always has, so a bordered row is taller by its border's width.
|
|
52
|
+
A border side is won whole by the cell: a cell naming any of that side's
|
|
53
|
+
three keys takes the side entirely, so a cell that failed soft on one name
|
|
54
|
+
does not inherit the row's other two.
|
|
55
|
+
On the event stream, a `row`, `total-row` or `table-start` `style` now
|
|
56
|
+
carries only what layers by ordinary means, and a row whose whole block was
|
|
57
|
+
box carries no `style` at all.
|
|
58
|
+
|
|
10
59
|
## [0.4.0] - 2026-09-03
|
|
11
60
|
|
|
12
61
|
### Changed
|
package/lib/index.d.ts
CHANGED
|
@@ -87,6 +87,12 @@ export interface StyleDeclarations extends BoxDeclarations {
|
|
|
87
87
|
background?: ExpressionValue<string>;
|
|
88
88
|
/** Horizontal alignment within the cell. */
|
|
89
89
|
align?: ExpressionValue<Align>;
|
|
90
|
+
/**
|
|
91
|
+
* Vertical alignment within a box taller than its content: a table cell or
|
|
92
|
+
* row, a split slot (text or image). A definition error on a stacked item
|
|
93
|
+
* and on a band image, whose boxes have no such slack.
|
|
94
|
+
*/
|
|
95
|
+
valign?: ExpressionValue<VAlign>;
|
|
90
96
|
/** How a number or date is presented. Locale stays on the instance. */
|
|
91
97
|
format?: ExpressionValue<FormatKind>;
|
|
92
98
|
/** Blank space before this item, in points. */
|
|
@@ -104,6 +110,7 @@ export interface SortKey {
|
|
|
104
110
|
export type CellValue = string;
|
|
105
111
|
|
|
106
112
|
export type Align = "left" | "center" | "right";
|
|
113
|
+
export type VAlign = "top" | "middle" | "bottom";
|
|
107
114
|
export type FormatKind = "number" | "currency" | "percent" | "date";
|
|
108
115
|
|
|
109
116
|
export interface Cell {
|
|
@@ -185,10 +192,19 @@ export interface TableHeader {
|
|
|
185
192
|
value: CellValue;
|
|
186
193
|
/** Literal only — never an expression. */
|
|
187
194
|
style?: StyleDeclarations;
|
|
195
|
+
/**
|
|
196
|
+
* How many columns this header covers — a literal integer >= 1, never an
|
|
197
|
+
* expression; absent is 1. The columns it covers omit their own `header`.
|
|
198
|
+
*/
|
|
199
|
+
span?: number;
|
|
188
200
|
}
|
|
189
201
|
|
|
190
202
|
export interface TableColumn extends Cell {
|
|
191
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Absent exactly when a neighbour's header `span` covers this column: every
|
|
205
|
+
* column's header slot is filled once, by its own or by a span.
|
|
206
|
+
*/
|
|
207
|
+
header?: string | TableHeader;
|
|
192
208
|
/** Column width as a percentage of the table width (0 < width <= 100). */
|
|
193
209
|
width?: number;
|
|
194
210
|
}
|
|
@@ -198,8 +214,20 @@ export interface TableRow {
|
|
|
198
214
|
style?: StyleDeclarations;
|
|
199
215
|
}
|
|
200
216
|
|
|
217
|
+
/**
|
|
218
|
+
* One cell of a total row: an ordinary cell, plus the columns it covers. The
|
|
219
|
+
* spans in a row must sum to the column count exactly.
|
|
220
|
+
*/
|
|
221
|
+
export type TotalCell = Cell & {
|
|
222
|
+
/**
|
|
223
|
+
* How many columns this cell covers — a literal integer >= 1, never an
|
|
224
|
+
* expression; absent is 1.
|
|
225
|
+
*/
|
|
226
|
+
span?: number;
|
|
227
|
+
};
|
|
228
|
+
|
|
201
229
|
export interface TotalRow {
|
|
202
|
-
cells: [
|
|
230
|
+
cells: [TotalCell, ...TotalCell[]];
|
|
203
231
|
visible?: ExpressionValue<boolean>;
|
|
204
232
|
style?: StyleDeclarations;
|
|
205
233
|
}
|
|
@@ -357,6 +385,11 @@ export type Token = LiteralToken | ValueToken;
|
|
|
357
385
|
export interface EventCell {
|
|
358
386
|
tokens: Token[];
|
|
359
387
|
style?: Record<string, unknown>;
|
|
388
|
+
/**
|
|
389
|
+
* How many columns this cell covers, present only when more than one: a
|
|
390
|
+
* span of 1 is the absence of one and never reaches the stream.
|
|
391
|
+
*/
|
|
392
|
+
span?: number;
|
|
360
393
|
/**
|
|
361
394
|
* The definition's schema path, on cells that belong to one: a `row` cell
|
|
362
395
|
* carries its column's (`detail.columns[i]`), a `total-row` cell its
|
|
@@ -490,7 +523,8 @@ export interface TableStartEvent {
|
|
|
490
523
|
type: "table-start";
|
|
491
524
|
/** Always `detail` — the table definition's schema path. */
|
|
492
525
|
path: string;
|
|
493
|
-
|
|
526
|
+
/** One entry per column. `header` is absent when a neighbour's span covers it. */
|
|
527
|
+
columns: { header?: EventCell; path: string; width?: number }[];
|
|
494
528
|
/** The header-row box from `detail.header`, when declared. */
|
|
495
529
|
style?: Record<string, unknown>;
|
|
496
530
|
}
|
|
@@ -721,3 +755,10 @@ export function typed(
|
|
|
721
755
|
* one: the question is whose band a role names, not what a walk can hand over.
|
|
722
756
|
*/
|
|
723
757
|
export function isReportBand(role: string | undefined): boolean;
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Every name in the closed style vocabulary, in the order SCHEMA.md's table
|
|
761
|
+
* lists them. A tool that offers the vocabulary imports this rather than
|
|
762
|
+
* keeping a copy.
|
|
763
|
+
*/
|
|
764
|
+
export const STYLE_NAMES: readonly string[];
|
package/lib/index.js
CHANGED
|
@@ -25,6 +25,7 @@ import { opt } from "./stream.js";
|
|
|
25
25
|
// imports them from the package, not from a file inside it.
|
|
26
26
|
export { breathe, display, isReportBand, text, typed, walk } from "./stream.js";
|
|
27
27
|
export { format } from "./format.js";
|
|
28
|
+
export { STYLE_NAMES } from "./style.js";
|
|
28
29
|
export { isDiagnostic } from "./locate.js";
|
|
29
30
|
|
|
30
31
|
/** @typedef {import("./scope.js").Scope} Scope */
|
package/lib/license.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// reassigns them. A key is valid for every version released inside its window
|
|
12
12
|
// (LICENSE section 7), so validity compares ISO date strings and never reads
|
|
13
13
|
// a clock — accepted output stays accepted.
|
|
14
|
-
let RELEASE = "2026-09-
|
|
14
|
+
let RELEASE = "2026-09-05"; // x-release-please-date
|
|
15
15
|
// The verifying half of the signing pair: the 65-byte uncompressed P-256
|
|
16
16
|
// point, base64url. The private half never enters the repo;
|
|
17
17
|
// scripts/license/sign.mjs mints keys against it.
|
package/lib/plan.js
CHANGED
|
@@ -54,8 +54,11 @@ import {
|
|
|
54
54
|
checkReportStyle,
|
|
55
55
|
checkRowStyle,
|
|
56
56
|
checkSlotImageStyle,
|
|
57
|
+
checkItemStyle,
|
|
57
58
|
checkSlotStyle,
|
|
58
59
|
checkStyle,
|
|
60
|
+
fanBox,
|
|
61
|
+
isBoxName,
|
|
59
62
|
} from "./style.js";
|
|
60
63
|
|
|
61
64
|
// Tolerance on the column-width sum. Widths are literal numbers an author may
|
|
@@ -67,11 +70,29 @@ let WIDTH_EPS = 1e-9;
|
|
|
67
70
|
// plan rather than riding the state a plan accumulates.
|
|
68
71
|
/** @type {any} */
|
|
69
72
|
let NIL = () => null;
|
|
73
|
+
// The two halves a table row's `style` compiles to, and the pair a row that
|
|
74
|
+
// declares none wears.
|
|
75
|
+
/** @typedef {{ box: (scope: Scope) => any, rest: (scope: Scope) => any }} RowStyles */
|
|
76
|
+
/** @type {RowStyles} */
|
|
77
|
+
let NO_ROW_STYLE = { box: NIL, rest: NIL };
|
|
70
78
|
/** @type {any} */
|
|
71
79
|
let EMPTY = () => [];
|
|
72
80
|
// What `$.params` reads when a report declares none.
|
|
73
81
|
let NONE = Object.freeze({});
|
|
74
82
|
|
|
83
|
+
// How many table columns one cell covers. A literal, for the same reason a
|
|
84
|
+
// column `width` is: the column geometry is allocated before any row is laid
|
|
85
|
+
// out, so it cannot depend on one. Absent is 1, and an unreadable span reads
|
|
86
|
+
// as 1 so the coverage walk above it reports where the author actually went
|
|
87
|
+
// wrong instead of cascading. `spanned` is the emit side: a span of 1 is the
|
|
88
|
+
// absence of one, and never reaches the stream.
|
|
89
|
+
/** @type {(span: any) => number} */
|
|
90
|
+
let spanValue = (span) => (Number.isInteger(span) && span >= 1 ? span : 1);
|
|
91
|
+
/** @type {(span: any) => boolean} */
|
|
92
|
+
let validSpan = (span) => span == null || spanValue(span) === span;
|
|
93
|
+
/** @type {(span: number) => number} */
|
|
94
|
+
let spanned = (span) => (span > 1 ? span : 0);
|
|
95
|
+
|
|
75
96
|
/** @type {(value: any) => boolean} */
|
|
76
97
|
let isExpr = (value) => typeof value === "string" && value[0] === "=";
|
|
77
98
|
/** @type {(from: Iterable<string>, into: Set<string>) => void} */
|
|
@@ -564,21 +585,56 @@ let readers = ({ bad, attempt }, { prop, cell, parseFold }) => {
|
|
|
564
585
|
for (let [name, value] of entries) style[name] = value(scope);
|
|
565
586
|
return style;
|
|
566
587
|
};
|
|
567
|
-
/** @type {(block: any) => boolean} */
|
|
568
|
-
let isComputed = (block) => Object.values(block).some(isExpr);
|
|
569
588
|
/**
|
|
570
589
|
* @type {(block: any, path: string, check: (name: string, value: any) => string | null)
|
|
571
590
|
* => (scope: Scope) => any}
|
|
572
591
|
*/
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
592
|
+
// A compiled block, constant-folded when nothing in it defers to render, so
|
|
593
|
+
// a literal style is resolved once and every node that wears it is handed
|
|
594
|
+
// the same frozen object.
|
|
595
|
+
/** @type {(block: any, entries: [string, Eval][]) => (scope: Scope) => any} */
|
|
596
|
+
let folded = (block, entries) => {
|
|
576
597
|
if (!entries.length) return NIL;
|
|
577
598
|
let resolve = styleFn(entries);
|
|
578
|
-
if (
|
|
599
|
+
if (entries.some(([name]) => isExpr(block[name]))) return resolve;
|
|
579
600
|
let constant = Object.freeze(resolve({}));
|
|
580
601
|
return () => constant;
|
|
581
602
|
};
|
|
603
|
+
/**
|
|
604
|
+
* @type {(block: any, path: string, check: (name: string, value: any) => string | null)
|
|
605
|
+
* => [string, Eval][]}
|
|
606
|
+
*/
|
|
607
|
+
let styleEntries = (block, path, check) => {
|
|
608
|
+
let entries = readStyles(block, path, check);
|
|
609
|
+
for (let [name, msg] of checkBorderSides(block, check)) bad(path + "." + name, msg);
|
|
610
|
+
return entries;
|
|
611
|
+
};
|
|
612
|
+
/**
|
|
613
|
+
* @type {(block: any, path: string, check: (name: string, value: any) => string | null)
|
|
614
|
+
* => (scope: Scope) => any}
|
|
615
|
+
*/
|
|
616
|
+
let compileStyles = (block, path, check) => folded(block, styleEntries(block, path, check));
|
|
617
|
+
// A table row's block is split where it is compiled rather than at every
|
|
618
|
+
// row: its box half resolves onto the row's cells and the rest stays on the
|
|
619
|
+
// row (SCHEMA.md, "Style declarations"). Both halves fold like any other
|
|
620
|
+
// block, so a literal row style is partitioned once per table.
|
|
621
|
+
/**
|
|
622
|
+
* @type {(block: any, path: string, check: (name: string, value: any) => string | null)
|
|
623
|
+
* => RowStyles}
|
|
624
|
+
*/
|
|
625
|
+
let compileRowStyles = (block, path, check) => {
|
|
626
|
+
let entries = styleEntries(block, path, check);
|
|
627
|
+
return {
|
|
628
|
+
box: folded(
|
|
629
|
+
block,
|
|
630
|
+
entries.filter(([name]) => isBoxName(name)),
|
|
631
|
+
),
|
|
632
|
+
rest: folded(
|
|
633
|
+
block,
|
|
634
|
+
entries.filter(([name]) => !isBoxName(name)),
|
|
635
|
+
),
|
|
636
|
+
};
|
|
637
|
+
};
|
|
582
638
|
/**
|
|
583
639
|
* @type {(def: any, path: string, check?: (name: string, value: any) => string | null)
|
|
584
640
|
* => (scope: Scope) => any}
|
|
@@ -589,6 +645,14 @@ let readers = ({ bad, attempt }, { prop, cell, parseFold }) => {
|
|
|
589
645
|
if (block == null || !obj(block, stylePath)) return NIL;
|
|
590
646
|
return compileStyles(block, stylePath, check);
|
|
591
647
|
};
|
|
648
|
+
/** The same, for the three blocks that sit on a table row. */
|
|
649
|
+
/** @type {(def: any, path: string) => RowStyles} */
|
|
650
|
+
let rowStylesOf = (def, path) => {
|
|
651
|
+
let block = def.style;
|
|
652
|
+
let stylePath = pathTo(path, "style");
|
|
653
|
+
if (block == null || !obj(block, stylePath)) return NO_ROW_STYLE;
|
|
654
|
+
return compileRowStyles(block, stylePath, checkRowStyle);
|
|
655
|
+
};
|
|
592
656
|
// A cell value: one sjabloon template. Emphasis is the cell's own `style`;
|
|
593
657
|
// markup in literal text is meaningful only to the HTML target.
|
|
594
658
|
/** @type {(value: any, path: string) => (scope: Scope, opts?: any) => any} */
|
|
@@ -703,6 +767,7 @@ let readers = ({ bad, attempt }, { prop, cell, parseFold }) => {
|
|
|
703
767
|
columnsOf,
|
|
704
768
|
takeOf,
|
|
705
769
|
stylesOf,
|
|
770
|
+
rowStylesOf,
|
|
706
771
|
cellValue,
|
|
707
772
|
foldsOf,
|
|
708
773
|
sortOf,
|
|
@@ -738,17 +803,53 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
738
803
|
return cellShape(tpl, visible, stylesOf(def, path, checkCellStyle));
|
|
739
804
|
};
|
|
740
805
|
|
|
806
|
+
/** @type {(def: any, path: string) => number} */
|
|
807
|
+
let spanOf = (def, path) => {
|
|
808
|
+
if (validSpan(def.span)) return spanValue(def.span);
|
|
809
|
+
bad(path + ".span", "expected a positive integer (span >= 1)");
|
|
810
|
+
return 1;
|
|
811
|
+
};
|
|
812
|
+
|
|
741
813
|
// A cell without a row, so `@` stays unbound. A bare string is shorthand for
|
|
742
814
|
// `{ value }` and keeps its authored path, so problems locate where written.
|
|
743
|
-
|
|
815
|
+
// A span needs the object form -- a bare string has nowhere to carry one.
|
|
816
|
+
// Absent is a problem here: a column that legitimately declares no header is
|
|
817
|
+
// one a neighbour's span covers, and `headerSlot` answers that before this
|
|
818
|
+
// is reached.
|
|
819
|
+
/**
|
|
820
|
+
* @type {(def: any, path: string) => { cell: (scope: Scope) => any, span: number }}
|
|
821
|
+
*/
|
|
744
822
|
let headerOf = (def, path) => {
|
|
745
|
-
if (typeof def === "string")
|
|
823
|
+
if (typeof def === "string")
|
|
824
|
+
return { cell: cellShape(cellValue(def, path), null, NIL), span: 1 };
|
|
746
825
|
if (!record(def)) {
|
|
747
826
|
bad(path, "expected a template string or object");
|
|
748
|
-
return cellShape(EMPTY, null, NIL);
|
|
827
|
+
return { cell: cellShape(EMPTY, null, NIL), span: 1 };
|
|
749
828
|
}
|
|
750
|
-
keys(def, ["value", "style"], path);
|
|
751
|
-
return cellOf(def, path);
|
|
829
|
+
keys(def, ["value", "style", "span"], path);
|
|
830
|
+
return { cell: cellOf(def, path), span: spanOf(def, path) };
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
// A column a neighbour's span covers declares no header of its own. One that
|
|
834
|
+
// declares one anyway is the overlap half of the coverage rule.
|
|
835
|
+
/** @type {(def: any, path: string) => null} */
|
|
836
|
+
let covered = (def, path) => {
|
|
837
|
+
if (def != null) bad(path, "covered by an earlier header's span");
|
|
838
|
+
return null;
|
|
839
|
+
};
|
|
840
|
+
// The per-column half of the coverage rule. `headerCover` decides which slot
|
|
841
|
+
// a column is in; this reports it, in the column's own turn, so header
|
|
842
|
+
// problems still come out in the documented key order.
|
|
843
|
+
/**
|
|
844
|
+
* @type {(def: any, path: string, slot: string) =>
|
|
845
|
+
* { cell: (scope: Scope) => any, span: number } | null}
|
|
846
|
+
*/
|
|
847
|
+
let headerSlot = (def, path, slot) => {
|
|
848
|
+
if (slot === "unreached") return null;
|
|
849
|
+
if (slot === "covered") return covered(def, path);
|
|
850
|
+
let header = headerOf(def, path);
|
|
851
|
+
if (slot === "over") bad(path + ".span", "reaches past the last column");
|
|
852
|
+
return header;
|
|
752
853
|
};
|
|
753
854
|
|
|
754
855
|
/** @type {(width: any) => boolean} */
|
|
@@ -759,11 +860,11 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
759
860
|
bad(path + ".width", "expected a percentage number (0 < width <= 100)");
|
|
760
861
|
return NaN;
|
|
761
862
|
};
|
|
762
|
-
/** @type {(def: any, path: string) => any} */
|
|
763
|
-
let columnOf = (def, path) => {
|
|
863
|
+
/** @type {(def: any, path: string, slot: string) => any} */
|
|
864
|
+
let columnOf = (def, path, slot) => {
|
|
764
865
|
if (!obj(def, path)) return null;
|
|
765
866
|
keys(def, ["header", "value", "visible", "style", "width"], path);
|
|
766
|
-
let header =
|
|
867
|
+
let header = headerSlot(def.header, path + ".header", slot);
|
|
767
868
|
let tpl = cellValue(def.value, path + ".value");
|
|
768
869
|
let visible = visibleOf(def, path);
|
|
769
870
|
let styles = stylesOf(def, path, checkCellStyle);
|
|
@@ -773,11 +874,14 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
773
874
|
return { header, cell: cellShape(tpl, visible, styles), width: widthOf(def.width, path), path };
|
|
774
875
|
};
|
|
775
876
|
|
|
776
|
-
/**
|
|
877
|
+
/**
|
|
878
|
+
* @type {(def: any, path: string) =>
|
|
879
|
+
* { cell: (scope: Scope) => any, span: number } | null}
|
|
880
|
+
*/
|
|
777
881
|
let totalOf = (def, path) => {
|
|
778
882
|
if (!obj(def, path)) return null;
|
|
779
|
-
keys(def, ["value", "visible", "style"], path);
|
|
780
|
-
return cellOf(def, path);
|
|
883
|
+
keys(def, ["value", "visible", "style", "span"], path);
|
|
884
|
+
return { cell: cellOf(def, path), span: spanOf(def, path) };
|
|
781
885
|
};
|
|
782
886
|
|
|
783
887
|
// A text item: a cell with no slot to keep, so a hidden one drops itself
|
|
@@ -793,7 +897,7 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
793
897
|
let shape = cellShape(
|
|
794
898
|
tpl,
|
|
795
899
|
inSlot ? visible : null,
|
|
796
|
-
stylesOf(def, path, inSlot ? checkSlotStyle :
|
|
900
|
+
stylesOf(def, path, inSlot ? checkSlotStyle : checkItemStyle),
|
|
797
901
|
);
|
|
798
902
|
// The cell shape first, then the item's own fields, so a cell reads
|
|
799
903
|
// the same wherever it appears in the stream.
|
|
@@ -998,7 +1102,7 @@ let nodes = ({ bad }, { arr, obj, keys, expression, cellValue, visibleOf, styles
|
|
|
998
1102
|
*/
|
|
999
1103
|
let bands = (
|
|
1000
1104
|
{ bad, runNames },
|
|
1001
|
-
{ arr, obj, keys, named, expression, visibleOf, columnsOf, takeOf,
|
|
1105
|
+
{ arr, obj, keys, named, expression, visibleOf, columnsOf, takeOf, rowStylesOf, foldsOf, sortOf },
|
|
1002
1106
|
{ columnOf, itemsOf, totalOf },
|
|
1003
1107
|
) => {
|
|
1004
1108
|
// Stacked items, one pass per row.
|
|
@@ -1014,30 +1118,71 @@ let bands = (
|
|
|
1014
1118
|
}
|
|
1015
1119
|
};
|
|
1016
1120
|
|
|
1017
|
-
/** @type {(value: any) => { rowVisible: Eval | null, rowStyles:
|
|
1121
|
+
/** @type {(value: any) => { rowVisible: Eval | null, rowStyles: RowStyles }} */
|
|
1018
1122
|
let rowLook = (value) => {
|
|
1019
1123
|
if (value.row == null || !obj(value.row, "detail.row"))
|
|
1020
|
-
return { rowVisible: null, rowStyles:
|
|
1124
|
+
return { rowVisible: null, rowStyles: NO_ROW_STYLE };
|
|
1021
1125
|
keys(value.row, ["visible", "style"], "detail.row");
|
|
1022
1126
|
return {
|
|
1023
1127
|
rowVisible: visibleOf(value.row, "detail.row"),
|
|
1024
|
-
rowStyles:
|
|
1128
|
+
rowStyles: rowStylesOf(value.row, "detail.row"),
|
|
1025
1129
|
};
|
|
1026
1130
|
};
|
|
1027
1131
|
// The header-row box: `style` only, a different path from each column's
|
|
1028
1132
|
// `header` cell. Absent when the table declares none.
|
|
1029
|
-
/** @type {(value: any) =>
|
|
1133
|
+
/** @type {(value: any) => RowStyles} */
|
|
1030
1134
|
let headerLook = (value) => {
|
|
1031
|
-
if (value.header == null || !obj(value.header, "detail.header")) return
|
|
1135
|
+
if (value.header == null || !obj(value.header, "detail.header")) return NO_ROW_STYLE;
|
|
1032
1136
|
keys(value.header, ["style"], "detail.header");
|
|
1033
|
-
return
|
|
1137
|
+
return rowStylesOf(value.header, "detail.header");
|
|
1138
|
+
};
|
|
1139
|
+
// Which slot each column's header sits in -- the same invariant a total row's
|
|
1140
|
+
// cells carry, walked positionally rather than summed because the diagnostic
|
|
1141
|
+
// has to name the column it broke at. Read off the raw definitions, before
|
|
1142
|
+
// any column is parsed, so `columnOf` can report its verdict in the column's
|
|
1143
|
+
// own turn. Spans are read leniently: an unreadable one covers a single
|
|
1144
|
+
// column and gets its own diagnostic when the column is parsed.
|
|
1145
|
+
//
|
|
1146
|
+
// `own` fills its own slot, `covered` is filled by a neighbour's span, `over`
|
|
1147
|
+
// reaches past the last column, and everything after an `over` is
|
|
1148
|
+
// `unreached` -- one broken span should not also report every column behind
|
|
1149
|
+
// it as headerless.
|
|
1150
|
+
// How many columns a definition's header claims, read leniently: a malformed
|
|
1151
|
+
// column, a missing header and an unreadable span all claim one here, and
|
|
1152
|
+
// each gets its own diagnostic when the column is parsed.
|
|
1153
|
+
/** @type {(def: any) => number} */
|
|
1154
|
+
let headerSpan = (def) => {
|
|
1155
|
+
let header = record(def) ? def.header : null;
|
|
1156
|
+
return record(header) ? spanValue(header.span) : 1;
|
|
1157
|
+
};
|
|
1158
|
+
/** @type {(slots: string[], from: number, to: number, slot: string) => void} */
|
|
1159
|
+
let fill = (slots, from, to, slot) => {
|
|
1160
|
+
for (let at = from; at < to; at++) slots[at] = slot;
|
|
1161
|
+
};
|
|
1162
|
+
/** @type {(defs: any[]) => string[]} */
|
|
1163
|
+
let headerCover = (defs) => {
|
|
1164
|
+
let slots = defs.map(() => "own");
|
|
1165
|
+
let at = 0;
|
|
1166
|
+
while (at < defs.length) {
|
|
1167
|
+
let span = headerSpan(defs[at]);
|
|
1168
|
+
if (at + span > defs.length) {
|
|
1169
|
+
slots[at] = "over";
|
|
1170
|
+
fill(slots, at + 1, defs.length, "unreached");
|
|
1171
|
+
return slots;
|
|
1172
|
+
}
|
|
1173
|
+
fill(slots, at + 1, at + span, "covered");
|
|
1174
|
+
at += span;
|
|
1175
|
+
}
|
|
1176
|
+
return slots;
|
|
1034
1177
|
};
|
|
1035
1178
|
/** @type {(value: any) => { defs: any[] | null, columns: any[] }} */
|
|
1036
1179
|
let tableColumns = (value) => {
|
|
1037
1180
|
let defs = arr(value.columns, "detail.columns", true);
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1181
|
+
let list = defs || [];
|
|
1182
|
+
if (defs && !list.length) bad("detail.columns", "expected at least one column");
|
|
1183
|
+
let slots = headerCover(list);
|
|
1184
|
+
let columns = list
|
|
1185
|
+
.map((def, i) => columnOf(def, "detail.columns[" + i + "]", slots[i]))
|
|
1041
1186
|
.filter((column) => column != null);
|
|
1042
1187
|
checkShares(bad, columns, "detail.columns", "column");
|
|
1043
1188
|
return { defs, columns };
|
|
@@ -1047,8 +1192,8 @@ let bands = (
|
|
|
1047
1192
|
totals
|
|
1048
1193
|
.map((def, i) => {
|
|
1049
1194
|
let path = prefix + "[" + i + "]";
|
|
1050
|
-
let
|
|
1051
|
-
return
|
|
1195
|
+
let total = totalOf(def, path);
|
|
1196
|
+
return total && { cell: total.cell, span: total.span, path };
|
|
1052
1197
|
})
|
|
1053
1198
|
.filter((entry) => entry != null);
|
|
1054
1199
|
/** @type {(rows: any[] | null) => any[]} */
|
|
@@ -1058,7 +1203,7 @@ let bands = (
|
|
|
1058
1203
|
};
|
|
1059
1204
|
/**
|
|
1060
1205
|
* @type {(value: any, defs: any[] | null) =>
|
|
1061
|
-
* { styles:
|
|
1206
|
+
* { styles: RowStyles, visible: any, cells: any[] }[] | null}
|
|
1062
1207
|
*/
|
|
1063
1208
|
let tableTotals = (value, defs) =>
|
|
1064
1209
|
value.total == null
|
|
@@ -1066,76 +1211,91 @@ let bands = (
|
|
|
1066
1211
|
: nonempty(arr(value.total, "detail.total", true)).map((def, r) =>
|
|
1067
1212
|
totalRowOf(def, "detail.total[" + r + "]", defs),
|
|
1068
1213
|
);
|
|
1214
|
+
// Read leniently: a cell whose span is unreadable, or which is not an object
|
|
1215
|
+
// at all, covers one column here, and its own diagnostic comes from parsing
|
|
1216
|
+
// it. Otherwise one mistake would report twice.
|
|
1069
1217
|
/** @type {(cells: any[], defs: any[] | null) => boolean} */
|
|
1070
|
-
let
|
|
1218
|
+
let covering = (cells, defs) =>
|
|
1219
|
+
!defs ||
|
|
1220
|
+
cells.reduce((all, cell) => all + spanValue(record(cell) ? cell.span : null), 0) ===
|
|
1221
|
+
defs.length;
|
|
1071
1222
|
/** @type {(cells: any[] | null, defs: any[] | null, path: string) => any[]} */
|
|
1072
1223
|
let counted = (cells, defs, path) => {
|
|
1073
1224
|
if (!cells) return [];
|
|
1074
|
-
if (!
|
|
1225
|
+
if (!covering(cells, defs)) bad(path + ".cells", "expected cells covering every column");
|
|
1075
1226
|
return cells;
|
|
1076
1227
|
};
|
|
1077
1228
|
/**
|
|
1078
1229
|
* @type {(def: any, path: string, defs: any[] | null) =>
|
|
1079
|
-
* { styles:
|
|
1230
|
+
* { styles: RowStyles, visible: any, cells: any[] }}
|
|
1080
1231
|
*/
|
|
1081
1232
|
let totalRowOf = (def, path, defs) => {
|
|
1082
|
-
if (!obj(def, path)) return { styles:
|
|
1233
|
+
if (!obj(def, path)) return { styles: NO_ROW_STYLE, visible: null, cells: [] };
|
|
1083
1234
|
keys(def, ["cells", "style", "visible"], path);
|
|
1084
1235
|
let cells = counted(arr(def.cells, path + ".cells", true), defs, path);
|
|
1085
1236
|
return {
|
|
1086
|
-
styles:
|
|
1237
|
+
styles: rowStylesOf(def, path),
|
|
1087
1238
|
visible: visibleOf(def, path),
|
|
1088
1239
|
cells: mapTotals(cells, path + ".cells"),
|
|
1089
1240
|
};
|
|
1090
1241
|
};
|
|
1091
1242
|
/**
|
|
1092
1243
|
* @type {(rows: any[], scope: Scope, runners: RunnerSet, columns: any[],
|
|
1093
|
-
* rowVisible: Eval | null, rowStyles:
|
|
1244
|
+
* rowVisible: Eval | null, rowStyles: RowStyles) => Generator<any>}
|
|
1094
1245
|
*/
|
|
1095
1246
|
function* tableRows(rows, scope, runners, columns, rowVisible, rowStyles) {
|
|
1096
1247
|
for (let row of rows) {
|
|
1097
1248
|
let scopeOfRow = runners.bind(scope, row);
|
|
1098
|
-
if (
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
),
|
|
1105
|
-
},
|
|
1106
|
-
{ style: rowStyles(scopeOfRow), run: scopeOfRow.run },
|
|
1107
|
-
);
|
|
1249
|
+
if (hidden(rowVisible, scopeOfRow)) continue;
|
|
1250
|
+
let cells = columns.map((column) =>
|
|
1251
|
+
Object.assign(column.cell(scopeOfRow), { path: column.path }),
|
|
1252
|
+
);
|
|
1253
|
+
fanBox(rowStyles.box(scopeOfRow), cells);
|
|
1254
|
+
yield opt({ type: "row", cells }, { style: rowStyles.rest(scopeOfRow), run: scopeOfRow.run });
|
|
1108
1255
|
}
|
|
1109
1256
|
}
|
|
1110
1257
|
/**
|
|
1111
|
-
* @type {(total: { styles:
|
|
1258
|
+
* @type {(total: { styles: RowStyles, visible: any, cells: any[] }[] | null,
|
|
1112
1259
|
* scope: Scope) => Generator<any>}
|
|
1113
1260
|
*/
|
|
1114
1261
|
function* emitTotals(total, scope) {
|
|
1115
1262
|
if (!total) return;
|
|
1116
1263
|
for (let row of total) {
|
|
1117
1264
|
if (hidden(row.visible, scope)) continue;
|
|
1118
|
-
|
|
1119
|
-
{
|
|
1120
|
-
type: "total-row",
|
|
1121
|
-
cells: row.cells.map(({ cell, path }) => Object.assign(cell(scope), { path })),
|
|
1122
|
-
},
|
|
1123
|
-
{ style: row.styles(scope) },
|
|
1265
|
+
let cells = row.cells.map(({ cell, span, path }) =>
|
|
1266
|
+
opt(Object.assign(cell(scope), { path }), { span: spanned(span) }),
|
|
1124
1267
|
);
|
|
1268
|
+
fanBox(row.styles.box(scope), cells);
|
|
1269
|
+
yield opt({ type: "total-row", cells }, { style: row.styles.rest(scope) });
|
|
1125
1270
|
}
|
|
1126
1271
|
}
|
|
1127
|
-
/** @type {(columns: any[], total: { styles:
|
|
1272
|
+
/** @type {(columns: any[], total: { styles: RowStyles, visible: any, cells: any[] }[] | null, rowVisible: Eval | null, rowStyles: RowStyles, headerStyles: RowStyles) => Band} */
|
|
1128
1273
|
let tableBand = (columns, total, rowVisible, rowStyles, headerStyles) =>
|
|
1129
1274
|
function* (rows, scope, runners) {
|
|
1275
|
+
// A covered column carries no `header` at all: the header row is the
|
|
1276
|
+
// cells the columns declare, which is fewer than the columns whenever
|
|
1277
|
+
// one spans. A consumer that reads no `span` still gets one cell per
|
|
1278
|
+
// uncovered column, which is the shape this had before spans existed.
|
|
1279
|
+
let cols = columns.map((column) =>
|
|
1280
|
+
opt(
|
|
1281
|
+
{},
|
|
1282
|
+
{
|
|
1283
|
+
header:
|
|
1284
|
+
column.header &&
|
|
1285
|
+
opt(column.header.cell(scope), { span: spanned(column.header.span) }),
|
|
1286
|
+
path: column.path,
|
|
1287
|
+
width: column.width,
|
|
1288
|
+
},
|
|
1289
|
+
),
|
|
1290
|
+
);
|
|
1291
|
+
// The header-row box fans onto the cells that are there, which for the
|
|
1292
|
+
// same reason is fewer than the columns whenever one spans. The list is
|
|
1293
|
+
// built only when there is a box to put on it.
|
|
1294
|
+
let box = headerStyles.box(scope);
|
|
1295
|
+
if (box) fanBox(box, cols.map((column) => column.header).filter(Boolean));
|
|
1130
1296
|
yield opt(
|
|
1131
|
-
{
|
|
1132
|
-
|
|
1133
|
-
path: "detail",
|
|
1134
|
-
columns: columns.map((column) =>
|
|
1135
|
-
opt({ header: column.header(scope), path: column.path }, { width: column.width }),
|
|
1136
|
-
),
|
|
1137
|
-
},
|
|
1138
|
-
{ style: headerStyles(scope) },
|
|
1297
|
+
{ type: "table-start", path: "detail", columns: cols },
|
|
1298
|
+
{ style: headerStyles.rest(scope) },
|
|
1139
1299
|
);
|
|
1140
1300
|
yield* tableRows(rows, scope, runners, columns, rowVisible, rowStyles);
|
|
1141
1301
|
yield* emitTotals(total, scope);
|
|
@@ -1329,15 +1489,13 @@ let headerHeightOf = (height, bad) => {
|
|
|
1329
1489
|
else if (!posPts(height)) bad("header.height", "expected a positive number of points");
|
|
1330
1490
|
return posPts(height) ? height : null;
|
|
1331
1491
|
};
|
|
1332
|
-
/** @type {(itemsOf: any) => { items: any, height: number | null }} */
|
|
1333
|
-
let emptyHeader = (itemsOf) => ({ items: itemsOf(null, "header", "report-header"), height: null });
|
|
1334
1492
|
/** @type {(value: any, keys: any, itemsOf: any, bad: any) => { items: any, height: number | null }} */
|
|
1335
1493
|
let reportHeaderOf = (value, keys, itemsOf, bad) => {
|
|
1336
1494
|
if (value == null || Array.isArray(value))
|
|
1337
1495
|
return { items: itemsOf(value, "header", "report-header"), height: null };
|
|
1338
1496
|
if (!record(value)) {
|
|
1339
1497
|
bad("header", "expected an array of items or { height, items }");
|
|
1340
|
-
return
|
|
1498
|
+
return { items: itemsOf(null, "header", "report-header"), height: null };
|
|
1341
1499
|
}
|
|
1342
1500
|
keys(value, ["height", "items"], "header");
|
|
1343
1501
|
return {
|
package/lib/stream.js
CHANGED
|
@@ -38,8 +38,6 @@ let asDeclared = (value, kind) => (kind === "date" ? reviveDate(value) : undefin
|
|
|
38
38
|
let RFC3339 =
|
|
39
39
|
/^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-](\d{2}):(\d{2})))?$/;
|
|
40
40
|
|
|
41
|
-
/** @type {(year: string, month: string) => number} */
|
|
42
|
-
let monthLength = (year, month) => new Date(Date.UTC(+year, +month, 0)).getUTCDate();
|
|
43
41
|
// An absent group is the timestamp half of an optional match, not a breach.
|
|
44
42
|
/** @type {(bound: [string | undefined, number, number]) => boolean} */
|
|
45
43
|
let within = ([n, lo, hi]) => n === undefined || (+n >= lo && +n <= hi);
|
|
@@ -69,7 +67,7 @@ export let reviveDate = (value) => {
|
|
|
69
67
|
/** @type {[string | undefined, number, number][]} */
|
|
70
68
|
let bounds = [
|
|
71
69
|
[month, 1, 12],
|
|
72
|
-
[day, 1,
|
|
70
|
+
[day, 1, new Date(Date.UTC(+year, +month, 0)).getUTCDate()],
|
|
73
71
|
[hour, 0, 23],
|
|
74
72
|
[minute, 0, 59],
|
|
75
73
|
[second, 0, 59],
|
package/lib/style.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The closed style vocabulary
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* its own formatting model, and nothing here knows about any of them.
|
|
2
|
+
* The closed style vocabulary — target-neutral names with typed literal values —
|
|
3
|
+
* and the resolutions the event stream is defined in terms of. Two things, and
|
|
4
|
+
* the split between them is the file's organising claim.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* The **checks** are what the traversal runs: unknown names and mistyped
|
|
7
|
+
* literals are definition errors; `=` results stay lenient at render.
|
|
8
|
+
*
|
|
9
|
+
* The **resolutions** are what a declaration means before any target sees it.
|
|
10
|
+
* A row's box is the one there is (SCHEMA.md, "Style declarations"): it says
|
|
11
|
+
* what the document means rather than what a surface paints, so every target
|
|
12
|
+
* is handed the answer instead of reaching its own. That is the same line
|
|
13
|
+
* `./stream.js` sits on, and the opposite side of it from `finite`/`HEX`,
|
|
14
|
+
* which are restated per target on purpose because a coercion is each target's
|
|
15
|
+
* own edge.
|
|
16
|
+
*
|
|
17
|
+
* Past those, each target maps these names to its own formatting model, and
|
|
18
|
+
* nothing here knows about any of them.
|
|
10
19
|
*/
|
|
11
20
|
/** @type {(value: any) => boolean} */
|
|
12
21
|
let finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
@@ -19,6 +28,8 @@ let spec = (msg, ok) => ({ ok, msg });
|
|
|
19
28
|
let COLOR = spec("expected a #rgb or #rrggbb color", isHex);
|
|
20
29
|
let FLAG = spec("expected a boolean", (value) => typeof value === "boolean");
|
|
21
30
|
let ALIGNMENTS = ["left", "center", "right"];
|
|
31
|
+
// "middle", not "center": that word is already the horizontal one.
|
|
32
|
+
let VALIGNMENTS = ["top", "middle", "bottom"];
|
|
22
33
|
let LINES = ["solid", "dashed", "dotted"];
|
|
23
34
|
let SIDES = ["Top", "Right", "Bottom", "Left"];
|
|
24
35
|
let BORDER_PARTS = ["Width", "Style", "Color"];
|
|
@@ -49,6 +60,7 @@ let STYLES = {
|
|
|
49
60
|
color: COLOR,
|
|
50
61
|
background: COLOR,
|
|
51
62
|
align: spec("expected left, center, or right", (value) => ALIGNMENTS.includes(value)),
|
|
63
|
+
valign: spec("expected top, middle, or bottom", (value) => VALIGNMENTS.includes(value)),
|
|
52
64
|
format: spec("expected number, currency, percent, or date", (value) => FORMATS.includes(value)),
|
|
53
65
|
};
|
|
54
66
|
let FORMATS = ["number", "currency", "percent", "date"];
|
|
@@ -73,6 +85,27 @@ for (let name of FLOW) {
|
|
|
73
85
|
STYLES[name] = POINTS;
|
|
74
86
|
IMAGE_STYLES.push(name);
|
|
75
87
|
}
|
|
88
|
+
// The vocabulary as a list, in the order SCHEMA.md's table reads it: the text
|
|
89
|
+
// names, the flow, then the box one part at a time across the four sides. A
|
|
90
|
+
// tool that offers the vocabulary (the editor's style rail) imports this
|
|
91
|
+
// rather than keeping a copy that has to be told about every new name.
|
|
92
|
+
/** @type {readonly string[]} */
|
|
93
|
+
// The box half of the vocabulary, generated once from the same two tables the
|
|
94
|
+
// checks read. `isBoxName` is the only place the question "does this name
|
|
95
|
+
// belong to the box?" is answered, so a name that does not start with
|
|
96
|
+
// `padding` or `border` could join without every asker being told separately.
|
|
97
|
+
let BOX_NAMES = [
|
|
98
|
+
...SIDES.map((side) => "padding" + side),
|
|
99
|
+
...BORDER_PARTS.flatMap((part) => SIDES.map((side) => "border" + side + part)),
|
|
100
|
+
];
|
|
101
|
+
let BOX = new Set(BOX_NAMES);
|
|
102
|
+
/** @type {(name: string) => boolean} */
|
|
103
|
+
export let isBoxName = (name) => BOX.has(name);
|
|
104
|
+
export let STYLE_NAMES = Object.freeze([
|
|
105
|
+
...Object.keys(STYLES).filter((name) => !FLOW.includes(name) && !isBoxName(name)),
|
|
106
|
+
...FLOW,
|
|
107
|
+
...BOX_NAMES,
|
|
108
|
+
]);
|
|
76
109
|
// One check for both the validating traversal and the compile path: an error
|
|
77
110
|
// message for a declaration, or null when it is acceptable (expressions defer
|
|
78
111
|
// to render).
|
|
@@ -150,9 +183,80 @@ let withoutFlow = (check, subject) => withoutNames(FLOW_NAMES, check, subject);
|
|
|
150
183
|
|
|
151
184
|
export let checkCellStyle = withoutFlow(checkStyle, "a table cell");
|
|
152
185
|
export let checkSlotStyle = withoutFlow(checkStyle, "a split slot");
|
|
153
|
-
|
|
186
|
+
// `valign` is legal exactly where a box has height it did not ask for
|
|
187
|
+
// (CONTEXT.md "Box": slack). A slot's box is the split's height, so a slot --
|
|
188
|
+
// text or image -- reads it; a band image's box is its own, so it does not.
|
|
189
|
+
// That is the one name an image accepts in a slot and nowhere else, the same
|
|
190
|
+
// shape of reason `width` reaches an item only there (docs/adr/0029).
|
|
191
|
+
let SLOT_IMAGE_STYLES = [...IMAGE_STYLES, "valign"];
|
|
192
|
+
export let checkSlotImageStyle = withoutFlow(
|
|
193
|
+
narrowed(SLOT_IMAGE_STYLES, "an image"),
|
|
194
|
+
"a split slot",
|
|
195
|
+
);
|
|
196
|
+
// A stacked text item's box is exactly its content and padding: no slack, so
|
|
197
|
+
// `valign` is refused rather than left unread.
|
|
198
|
+
/** @type {Record<string, true>} */
|
|
199
|
+
let STACKED_NAMES = { valign: true };
|
|
200
|
+
export let checkItemStyle = withoutNames(STACKED_NAMES, checkStyle, "a stacked item");
|
|
154
201
|
// A row box has no cell value, so `format` is refused there the way flow
|
|
155
202
|
// spacing is — it is a presentation of a value, not of a band of cells.
|
|
156
203
|
/** @type {Record<string, true>} */
|
|
157
204
|
let ROW_NAMES = { ...FLOW_NAMES, format: true };
|
|
158
205
|
export let checkRowStyle = withoutNames(ROW_NAMES, checkStyle, "a table row");
|
|
206
|
+
|
|
207
|
+
// --- resolution --------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
// A row's `style` resolves onto its cells (SCHEMA.md, "Style declarations").
|
|
210
|
+
// The box is the half that cannot arrive any other way: a table row is not a
|
|
211
|
+
// box on any surface -- not a `<tr>`, not a worksheet row, not a band of the
|
|
212
|
+
// PDF's own -- so it is resolved here, once, and no target is left to have an
|
|
213
|
+
// opinion about it. Every other declaration on a row reaches its cells by the
|
|
214
|
+
// layering each target already does, which is why only the box moves.
|
|
215
|
+
//
|
|
216
|
+
// Which names are the box is decided where the block is compiled, not here: a
|
|
217
|
+
// literal row style is partitioned once per table rather than once per row.
|
|
218
|
+
// What is left for render is the per-cell layering below.
|
|
219
|
+
|
|
220
|
+
// The three names of each border side, built once. `layer` reaches for them
|
|
221
|
+
// per cell, and rebuilding twelve strings per cell was most of what this cost.
|
|
222
|
+
/** @type {Record<string, string[]>} */
|
|
223
|
+
let BORDER_NAMES = Object.fromEntries(
|
|
224
|
+
SIDES.map((side) => [side, BORDER_PARTS.map((part) => "border" + side + part)]),
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
/** @type {(block: any, side: string) => boolean} */
|
|
228
|
+
let ownsSide = (block, side) => BORDER_NAMES[side].some((name) => Object.hasOwn(block, name));
|
|
229
|
+
|
|
230
|
+
// The spread has already written every name the cell declared, so taking a
|
|
231
|
+
// side is dropping what is left of the row's: the names of that side the cell
|
|
232
|
+
// did not name itself.
|
|
233
|
+
/** @type {(out: any, over: any, side: string) => void} */
|
|
234
|
+
let takeSide = (out, over, side) => {
|
|
235
|
+
for (let name of BORDER_NAMES[side]) if (!Object.hasOwn(over, name)) delete out[name];
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// A border side is won whole by the cell: a cell that named any of the three
|
|
239
|
+
// keys takes that side entirely, so a cell that failed-soft on one name does
|
|
240
|
+
// not pick up the row's other two and become a stroke nobody declared. Only
|
|
241
|
+
// the sides the row's box actually declares can be taken from it, so the
|
|
242
|
+
// caller settles which those are once and hands them down.
|
|
243
|
+
/** @type {(under: any, over: any, sides: string[]) => any} */
|
|
244
|
+
let layer = (under, over, sides) => {
|
|
245
|
+
if (!over) return under;
|
|
246
|
+
let out = { ...under, ...over };
|
|
247
|
+
for (let side of sides) if (ownsSide(over, side)) takeSide(out, over, side);
|
|
248
|
+
return out;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Resolve a row's box onto the row's cells. The cells are written in place:
|
|
253
|
+
* both they and the box were built for this one event and nothing else holds
|
|
254
|
+
* them yet. A row that declares no box is the common case and costs one test.
|
|
255
|
+
*
|
|
256
|
+
* @type {(box: any, cells: any[]) => void}
|
|
257
|
+
*/
|
|
258
|
+
export let fanBox = (box, cells) => {
|
|
259
|
+
if (!box) return;
|
|
260
|
+
let sides = SIDES.filter((side) => ownsSide(box, side));
|
|
261
|
+
for (let cell of cells) cell.style = layer(box, cell.style, sides);
|
|
262
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quario",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A tiny, runtime-neutral report engine — in the makings, not yet released",
|
|
5
5
|
"homepage": "https://getquario.com",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"sjabloon",
|
|
52
52
|
"padvinder"
|
|
53
53
|
],
|
|
54
|
-
"limit": "
|
|
54
|
+
"limit": "11 kB"
|
|
55
55
|
}
|
|
56
56
|
],
|
|
57
57
|
"engines": {
|