quario 0.1.0 → 0.3.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/lib/scope.js CHANGED
@@ -119,12 +119,6 @@ let RUNNERS = {
119
119
  };
120
120
  },
121
121
  };
122
- /** @type {(fold: Fold) => Eval} */
123
- let startRunner = ({ fn, per }) => {
124
- let step = RUNNERS[fn]();
125
- return (scope) => step(per ? per(scope) : 1);
126
- };
127
-
128
122
  // The runner set as a value, and the two things a band does with it. `extend`
129
123
  // is the whole of persist-vs-reset: a group's own runners are fresh per
130
124
  // instance, the outer ones fold on unchanged.
@@ -136,10 +130,10 @@ let runnerSet = (runners) => {
136
130
  // Declaring no runners is not a change of set, so a band whose `run`
137
131
  // block is absent renders under this very value rather than a copy.
138
132
  if (!folds.length) return value;
139
- let started = folds.map(([name, fold]) => /** @type {[string, Eval]} */ ([
140
- name,
141
- startRunner(fold),
142
- ]));
133
+ let started = folds.map(([name, { fn, per }]) => {
134
+ let step = RUNNERS[fn]();
135
+ return /** @type {[string, Eval]} */ ([name, (scope) => step(per ? per(scope) : 1)]);
136
+ });
143
137
  return runnerSet([...runners, ...started]);
144
138
  },
145
139
  // The per-row detail scope: `@` bound to the row, its running values folded
package/lib/stream.js CHANGED
@@ -8,15 +8,15 @@
8
8
  * loop back is a stream rule — the two are independent, and a target breathes
9
9
  * while stamping finished pages, which is no walk at all (CONTEXT.md).
10
10
  */
11
- // The token-join sjabloon renders with: literals verbatim, values
12
- // `String(value ?? '')`. Re-exported so event consumers derive display text
13
- // from a cell's tokens without hand-rolling the join.
14
- export { text } from "sjabloon";
11
+ // The token-join sjabloon renders with — literals verbatim, values through
12
+ // the scalar `display()` rule — re-exported together so event consumers
13
+ // derive display text from a cell's tokens without hand-rolling the join.
14
+ export { display, text } from "sjabloon";
15
15
 
16
16
  /** @type {(value: any) => boolean} */
17
- let finiteNum = (value) => typeof value === "number" && Number.isFinite(value);
17
+ export let finiteNum = (value) => typeof value === "number" && Number.isFinite(value);
18
18
  /** @type {(value: any) => boolean} */
19
- let finiteDate = (value) => value instanceof Date && Number.isFinite(value.getTime());
19
+ export let finiteDate = (value) => value instanceof Date && Number.isFinite(value.getTime());
20
20
  /** @type {(value: any) => number | boolean | Date | undefined} */
21
21
  let asTyped = (value) => {
22
22
  if (finiteNum(value)) return value;
@@ -42,7 +42,7 @@ export function typed(tokens) {
42
42
 
43
43
  // The report's own bands, by the role their items wear (CONTEXT.md, "Band").
44
44
  // The page ones are in it although no walk emits them: the question is whose
45
- // band a role names, not what a walk hands over (ADR 0021).
45
+ // band a role names, not what a walk hands over (ADR 0028).
46
46
  let REPORT_BANDS = new Set([
47
47
  "report-header",
48
48
  "report-footer",
@@ -55,7 +55,7 @@ let REPORT_BANDS = new Set([
55
55
  * Whether a band item's `role` names one of the report's own bands rather than
56
56
  * a group instance's. This says whose band it is, never how to lay one out —
57
57
  * what a consumer does with the answer is its own. See SCHEMA.md ("Event
58
- * stream") for the contract and ADR 0021 for why the engine classifies rather
58
+ * stream") for the contract and ADR 0028 for why the engine classifies rather
59
59
  * than carrying a verdict on the events themselves.
60
60
  *
61
61
  * @param {string} role An item or image event's `role`.
package/lib/style.js CHANGED
@@ -19,12 +19,21 @@ let spec = (msg, ok) => ({ ok, msg });
19
19
  let COLOR = spec("expected a #rgb or #rrggbb color", isHex);
20
20
  let FLAG = spec("expected a boolean", (value) => typeof value === "boolean");
21
21
  let ALIGNMENTS = ["left", "center", "right"];
22
- // The declarations an image item accepts. The rest of the vocabulary describes
23
- // text, which an image does not have, so anything else on one is a definition
24
- // error rather than a silent no-op (SCHEMA.md, "Image item"). The subset lives
25
- // beside the vocabulary it narrows, and `checkImageStyle` below is how the
26
- // traversal asks for it -- it keeps no second list of its own.
22
+ let LINES = ["solid", "dashed", "dotted"];
23
+ let SIDES = ["Top", "Right", "Bottom", "Left"];
24
+ let BORDER_PARTS = ["Width", "Style", "Color"];
25
+ // The declarations an image item accepts. Text names are refused; the box and
26
+ // flow spacing are legal, because a band image has a box and sits in the flow
27
+ // the way a text item does (SCHEMA.md, "Image item"). The subset lives beside
28
+ // the vocabulary it narrows, and `checkImageStyle` below is how the traversal
29
+ // asks for it -- it keeps no second list of its own.
27
30
  let IMAGE_STYLES = ["align", "background"];
31
+ // The declarations a report default accepts. A report default states what the
32
+ // document is set in, so it carries only the two declarations that describe a
33
+ // typeface -- and, unlike the rest of the vocabulary, only the two no band-role
34
+ // default has to argue with. Anything else is a definition error rather than a
35
+ // silent no-op, exactly as on an image (SCHEMA.md, "Report default").
36
+ let REPORT_STYLES = ["family", "size"];
28
37
  /** @type {Record<string, StyleSpec>} */
29
38
  let STYLES = {
30
39
  family: spec(
@@ -36,10 +45,34 @@ let STYLES = {
36
45
  italic: FLAG,
37
46
  underline: FLAG,
38
47
  strikethrough: FLAG,
48
+ uppercase: FLAG,
39
49
  color: COLOR,
40
50
  background: COLOR,
41
51
  align: spec("expected left, center, or right", (value) => ALIGNMENTS.includes(value)),
52
+ format: spec("expected number, currency, percent, or date", (value) => FORMATS.includes(value)),
42
53
  };
54
+ let FORMATS = ["number", "currency", "percent", "date"];
55
+ let POINTS = spec(
56
+ "expected a non-negative number of points",
57
+ (value) => finite(value) && value >= 0,
58
+ );
59
+ let LINE = spec("expected solid, dashed, or dotted", (value) => LINES.includes(value));
60
+ /** @type {Record<string, StyleSpec>} */
61
+ let BORDER = { Width: POINTS, Style: LINE, Color: COLOR };
62
+ for (let side of SIDES) {
63
+ STYLES["padding" + side] = POINTS;
64
+ IMAGE_STYLES.push("padding" + side);
65
+ for (let part of BORDER_PARTS) {
66
+ let name = "border" + side + part;
67
+ STYLES[name] = BORDER[part];
68
+ IMAGE_STYLES.push(name);
69
+ }
70
+ }
71
+ let FLOW = ["spaceBefore", "spaceAfter"];
72
+ for (let name of FLOW) {
73
+ STYLES[name] = POINTS;
74
+ IMAGE_STYLES.push(name);
75
+ }
43
76
  // One check for both the validating traversal and the compile path: an error
44
77
  // message for a declaration, or null when it is acceptable (expressions defer
45
78
  // to render).
@@ -55,12 +88,71 @@ export let checkStyle = (name, value) => {
55
88
  return rule.ok(value) ? null : rule.msg;
56
89
  };
57
90
 
58
- // The same check narrowed to what an image accepts, so a text declaration on
59
- // one is refused as pointedly as an unknown name. A node that takes part of
60
- // the vocabulary names this checker rather than passing a flag, which is what
61
- // keeps the subset and its wording here, together.
62
- /** @type {(name: string, value: any) => string | null} */
63
- export let checkImageStyle = (name, value) =>
64
- Object.hasOwn(STYLES, name) && !IMAGE_STYLES.includes(name)
65
- ? 'style "' + name + '" does not apply to an image'
91
+ // A border side is width, style, and colour together, or none. Incomplete
92
+ // literals are a definition error; a side that still has an expression defers
93
+ // to render, where an incomplete result contributes nothing rather than
94
+ // becoming a solid black stroke. Per-name checks run first; a side that
95
+ // already has a located problem is left alone so the author sees one cause.
96
+ /** @type {(present: string[]) => boolean} */
97
+ let incomplete = (present) => present.length > 0 && present.length < 3;
98
+ /** @type {(block: any, check: (name: string, value: any) => string | null, name: string) => boolean} */
99
+ let deferred = (block, check, name) => isExpr(block[name]) || !!check(name, block[name]);
100
+
101
+ /** @type {(block: any, check?: (name: string, value: any) => string | null) => [string, string][]} */
102
+ export let checkBorderSides = (block, check = checkStyle) => {
103
+ /** @type {[string, string][]} */
104
+ let out = [];
105
+ for (let side of SIDES) {
106
+ let names = BORDER_PARTS.map((part) => "border" + side + part);
107
+ let present = names.filter((name) => Object.hasOwn(block, name));
108
+ if (incomplete(present) && !present.some((name) => deferred(block, check, name)))
109
+ out.push([present[0], "border" + side + " needs width, style, and color together"]);
110
+ }
111
+ return out;
112
+ };
113
+
114
+ // The same check narrowed to a subset of the vocabulary, so a declaration a
115
+ // node cannot wear is refused as pointedly as an unknown name rather than
116
+ // silently doing nothing. A node that takes part of the vocabulary names the
117
+ // checker this makes rather than passing a flag, which is what keeps each
118
+ // subset and its wording together, here.
119
+ //
120
+ // `subject` completes "does not apply to ___", so it carries its own article.
121
+ /** @type {(allowed: string[], subject: string) => (name: string, value: any) => string | null} */
122
+ let narrowed = (allowed, subject) => (name, value) =>
123
+ Object.hasOwn(STYLES, name) && !allowed.includes(name)
124
+ ? 'style "' + name + '" does not apply to ' + subject
66
125
  : checkStyle(name, value);
126
+
127
+ export let checkImageStyle = narrowed(IMAGE_STYLES, "an image");
128
+ // "a report default" rather than the key's name: the key is `style` like every
129
+ // other one, so the message has to say which `style` refused it.
130
+ export let checkReportStyle = narrowed(REPORT_STYLES, "a report default");
131
+
132
+ // Flow spacing is a band-item pad. A table cell, a row box, and a split slot
133
+ // have no flow, so the names are refused there the way a text declaration is
134
+ // on an image (docs/adr/0037).
135
+ /** @type {Record<string, true>} */
136
+ let FLOW_NAMES = Object.fromEntries(FLOW.map((name) => [name, true]));
137
+ /**
138
+ * @type {(names: Record<string, true>, check: (name: string, value: any) => string | null, subject: string) =>
139
+ * (name: string, value: any) => string | null}
140
+ */
141
+ let withoutNames = (names, check, subject) => (name, value) =>
142
+ Object.hasOwn(names, name)
143
+ ? 'style "' + name + '" does not apply to ' + subject
144
+ : check(name, value);
145
+ /**
146
+ * @type {(check: (name: string, value: any) => string | null, subject: string) =>
147
+ * (name: string, value: any) => string | null}
148
+ */
149
+ let withoutFlow = (check, subject) => withoutNames(FLOW_NAMES, check, subject);
150
+
151
+ export let checkCellStyle = withoutFlow(checkStyle, "a table cell");
152
+ export let checkSlotStyle = withoutFlow(checkStyle, "a split slot");
153
+ export let checkSlotImageStyle = withoutFlow(checkImageStyle, "a split slot");
154
+ // A row box has no cell value, so `format` is refused there the way flow
155
+ // spacing is — it is a presentation of a value, not of a band of cells.
156
+ /** @type {Record<string, true>} */
157
+ let ROW_NAMES = { ...FLOW_NAMES, format: true };
158
+ export let checkRowStyle = withoutNames(ROW_NAMES, checkStyle, "a table row");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quario",
3
- "version": "0.1.0",
3
+ "version": "0.3.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",
@@ -33,9 +33,9 @@
33
33
  "postpack": "node -e \"require('fs').rmSync('LICENSE',{force:true})\""
34
34
  },
35
35
  "dependencies": {
36
- "padvinder": "^0.4.0",
37
- "sjabloon": "^0.8.0",
38
- "xprsn": "^0.9.0"
36
+ "padvinder": "^0.9.0",
37
+ "sjabloon": "^0.12.0",
38
+ "xprsn": "^0.11.1"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@arethetypeswrong/cli": "^0.18.3",
@@ -51,7 +51,7 @@
51
51
  "sjabloon",
52
52
  "padvinder"
53
53
  ],
54
- "limit": "8 kB"
54
+ "limit": "10 kB"
55
55
  }
56
56
  ],
57
57
  "engines": {