@quario/layout 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/text.js CHANGED
@@ -3,25 +3,30 @@
3
3
  * their resolved typography, and a greedy breaker turns those into lines. Pure
4
4
  * measurement against the font registry — nothing here touches the page.
5
5
  */
6
- import { display, format } from "quario";
6
+ import { display, format, styledRuns } from "quario";
7
7
  import { ascOf, face, printable, width } from "./fonts.js";
8
- import { LEAD, col, dressed, sizeOf, upper } from "./style.js";
8
+ import { LEAD, col, merge, sameCol, sizeOf, upper } from "./style.js";
9
9
 
10
10
  // What measuring needs and no more, and it is exactly the render's settings:
11
11
  // the faces to measure against, the base size and family a style falls back to,
12
- // and the intl three a formatted value resolves in. Declared next door, in
13
- // `canvas.js`, where it is built and settled nothing here can touch a page,
14
- // and a caller hands over `canvas.settings` rather than the canvas.
15
- /** @typedef {import('./canvas.js').Settings} Settings */
12
+ // and the intl three a formatted value resolves in. Declared in `settings.js`,
13
+ // the module that owns them; a caller hands over `canvas.settings` rather than
14
+ // the canvas, so nothing here can touch a page.
15
+ /** @typedef {import('./settings.js').Settings} Settings */
16
16
 
17
+ // The typography one styled run wears, resolved once and stamped on every atom
18
+ // of it. Decoration and the highlight ride here rather than on the line,
19
+ // because a run is what wears them: underlining a word has to stop where the
20
+ // word does (ADR 0061).
17
21
  /**
18
- * @typedef {{ text: string, font: any, size: number, color: any,
19
- * space: boolean, hard: boolean }} Atom
22
+ * @typedef {{ font: any, size: number, color: any, bg: any,
23
+ * underline: boolean, strikethrough: boolean }} Look
20
24
  */
25
+ /** @typedef {Look & { text: string, space: boolean, hard: boolean }} Atom */
26
+ /** @typedef {Look & { text: string, w: number }} Piece */
21
27
  /**
22
- * @typedef {{ pieces: { text: string, font: any, size: number, color: any,
23
- * w: number }[], w: number, h: number, size: number, asc: number,
24
- * underline?: boolean, strikethrough?: boolean }} Line
28
+ * @typedef {{ pieces: Piece[], w: number, h: number, size: number,
29
+ * asc: number }} Line
25
30
  */
26
31
 
27
32
  /** @typedef {{ cur: Atom[], w: number, lines: Line[], base: number }} Wrap */
@@ -29,7 +34,7 @@ import { LEAD, col, dressed, sizeOf, upper } from "./style.js";
29
34
  /** @type {(token: any, style: any, settings: Settings) => string} */
30
35
  let rawOf = (token, style, settings) => {
31
36
  if ("literal" in token) return token.literal;
32
- return format(token.value, style?.format, settings) ?? display(token.value);
37
+ return format(token.value, style, settings) ?? display(token.value);
33
38
  };
34
39
 
35
40
  // This layout has no text-transform to defer to, so `uppercase` is applied to
@@ -40,40 +45,74 @@ let rawOf = (token, style, settings) => {
40
45
  /** @type {(text: string, style: any) => string} */
41
46
  let cased = (text, style) => (upper(style) ? text.toUpperCase() : text);
42
47
 
43
- /** @type {(out: Atom[], font: any, size: number, color: any, line: string) => void} */
44
- let pushParts = (out, font, size, color, line) => {
45
- for (let part of printable(font, line).split(/( +)/).filter(Boolean))
46
- out.push({ text: part, font, size, color, space: part[0] === " ", hard: false });
48
+ /** @type {(out: Atom[], look: Look, line: string) => void} */
49
+ let pushParts = (out, look, line) => {
50
+ for (let part of printable(look.font, line).split(/( +)/).filter(Boolean))
51
+ out.push({ ...look, text: part, space: part[0] === " ", hard: false });
47
52
  };
48
53
 
49
- /** @type {(out: Atom[], font: any, size: number, color: any, i: number, line: string) => void} */
50
- let pushLine = (out, font, size, color, i, line) => {
51
- if (i) out.push({ text: "", font, size, color, space: false, hard: true });
52
- pushParts(out, font, size, color, line);
54
+ /** @type {(out: Atom[], look: Look, i: number, line: string) => void} */
55
+ let pushLine = (out, look, i, line) => {
56
+ if (i) out.push({ ...look, text: "", space: false, hard: true });
57
+ pushParts(out, look, line);
53
58
  };
54
59
 
55
- /** @type {(settings: Settings, style: any) => { font: any, size: number, color: any }} */
60
+ /** @type {(settings: Settings, style: any) => Look} */
56
61
  let look = (settings, style) => {
57
62
  let resolved = style || {};
58
63
  return {
59
64
  font: face(settings.fonts, resolved, settings.family),
60
65
  size: sizeOf(resolved, settings.base),
61
66
  color: col(resolved.color),
67
+ bg: col(resolved.background),
68
+ underline: resolved.underline === true,
69
+ strikethrough: resolved.strikethrough === true,
62
70
  };
63
71
  };
64
72
 
65
- // Flatten a cell's tokens to word/space atoms carrying the cell's resolved
66
- // typography one face, size and colour for the whole cell. CR, LF, and
67
- // CRLF are one hard break each (SCHEMA.md, Cell values).
73
+ /** @type {(seen: Look, base: any) => Look} */
74
+ let highlighted = (seen, base) => (sameCol(seen.bg, base) ? { ...seen, bg: null } : seen);
75
+
76
+ // What one styled run draws in: the style it presents its values through, and
77
+ // the look its atoms wear. Its own declarations over the cell's where it
78
+ // carries a block, and the cell's answer unchanged where it does not — asked
79
+ // once, so the run's two questions cannot be branched on separately.
80
+ /** @type {(settings: Settings, style: any, styled: any, bare: Look, base: any) =>
81
+ * { worn: any, seen: Look} }*/
82
+ let wornBy = (settings, style, styled, bare, base) => {
83
+ if (!styled.style) return { worn: style, seen: bare };
84
+ let worn = merge(style, styled.style);
85
+ return { worn, seen: highlighted(look(settings, worn), base) };
86
+ };
87
+
88
+ // One styled run's tokens flattened to atoms, all of them wearing its look.
89
+ /** @type {(out: Atom[], settings: Settings, styled: any, dress: { worn: any, seen: Look }) => void} */
90
+ let pushStyledRun = (out, settings, styled, { worn, seen }) => {
91
+ for (let token of styled.tokens)
92
+ for (let [i, line] of cased(rawOf(token, worn, settings), worn)
93
+ .split(/\r\n|\r|\n/)
94
+ .entries())
95
+ pushLine(out, seen, i, line);
96
+ };
97
+
98
+ // Flatten a cell's tokens to word/space atoms carrying their run's resolved
99
+ // typography. A run's style is the engine's fully-resolved answer for that
100
+ // stretch, so it lays over the cell's *effective* style -- the row's, the band
101
+ // role's or the split's, whichever reached this cell -- rather than replacing
102
+ // it: the run says what it says and the layers outside the cell stand where it
103
+ // is silent. A cell with no authored runs is one run wearing the cell's own
104
+ // look, which is what it always was. CR, LF, and CRLF are one hard break each
105
+ // (SCHEMA.md, Cell values).
68
106
  /** @type {(settings: Settings, tokens: any[], style: any) => Atom[]} */
69
107
  let atoms = (settings, tokens, style) => {
70
108
  let out = /** @type {Atom[]} */ ([]);
71
- let { font, size, color } = look(settings, style);
72
- for (let token of tokens)
73
- for (let [i, line] of cased(rawOf(token, style, settings), style)
74
- .split(/\r\n|\r|\n/)
75
- .entries())
76
- pushLine(out, font, size, color, i, line);
109
+ let own = look(settings, style);
110
+ // The cell's own `background` is already painted at cell scope, so a run
111
+ // highlights only where it names a different one -- otherwise every ordinary
112
+ // cell with a background would paint it twice, once per line.
113
+ let bare = { ...own, bg: null };
114
+ for (let styled of styledRuns(tokens))
115
+ pushStyledRun(out, settings, styled, wornBy(settings, style, styled, bare, own.bg));
77
116
  return out;
78
117
  };
79
118
 
@@ -82,10 +121,22 @@ let trimEnd = (cur) => {
82
121
  while (cur.length && cur[cur.length - 1].space) cur.pop();
83
122
  };
84
123
 
85
- /** @type {(pieces: Line['pieces'], atom: Atom, atomWidth: number) => void} */
124
+ // Whether two stretches draw identically, and so may be one piece. Colours
125
+ // compare channel-wise because `col()` mints a fresh object per read and
126
+ // interns nothing, so identity would split every run in two.
127
+ /** @type {(a: Look, b: Look) => boolean} */
128
+ let sameFace = (a, b) => a.font === b.font && a.size === b.size;
129
+ /** @type {(a: Look, b: Look) => boolean} */
130
+ let sameInk = (a, b) => sameCol(a.color, b.color) && sameCol(a.bg, b.bg);
131
+ /** @type {(a: Look, b: Look) => boolean} */
132
+ let sameRule = (a, b) => a.underline === b.underline && a.strikethrough === b.strikethrough;
133
+ /** @type {(a: Look, b: Look) => boolean} */
134
+ let sameLook = (a, b) => sameFace(a, b) && sameInk(a, b) && sameRule(a, b);
135
+
136
+ /** @type {(pieces: Piece[], atom: Atom, atomWidth: number) => void} */
86
137
  let mergeAtom = (pieces, atom, atomWidth) => {
87
138
  let last = pieces[pieces.length - 1];
88
- if (last) {
139
+ if (last && sameLook(last, atom)) {
89
140
  last.text += atom.text;
90
141
  last.w += atomWidth;
91
142
  return;
@@ -95,6 +146,9 @@ let mergeAtom = (pieces, atom, atomWidth) => {
95
146
  font: atom.font,
96
147
  size: atom.size,
97
148
  color: atom.color,
149
+ bg: atom.bg,
150
+ underline: atom.underline,
151
+ strikethrough: atom.strikethrough,
98
152
  w: atomWidth,
99
153
  });
100
154
  };
@@ -219,9 +273,9 @@ let placeAtom = (state, atom, avail) => {
219
273
  };
220
274
 
221
275
  // Greedy wrap against `avail`: spaces never start a line, an over-wide word
222
- // breaks by character, hard breaks always break. A cell's atoms share one
223
- // typography, so a line's atoms merge into a single draw piece. `size` is the
224
- // empty-run height — a blank item, a hard-break hole.
276
+ // breaks by character, hard breaks always break. Atoms that draw alike merge
277
+ // into one piece, so a cell with no styled runs is still one piece per line.
278
+ // `size` is the empty-run height — a blank item, a hard-break hole.
225
279
  /** @type {(list: Atom[], avail: number, size: number) => Line[]} */
226
280
  let wrap = (list, avail, size) => {
227
281
  /** @type {Wrap} */
@@ -231,22 +285,7 @@ let wrap = (list, avail, size) => {
231
285
  return state.lines;
232
286
  };
233
287
 
234
- // Stamp cell-level decorations onto every wrapped fragment. Measurement does
235
- // not need them; drawing does, and wrapping must not lose them.
236
- /** @type {(line: Line, underline: boolean, strikethrough: boolean) => void} */
237
- let stamp = (line, underline, strikethrough) => {
238
- if (underline) line.underline = true;
239
- if (strikethrough) line.strikethrough = true;
240
- };
241
-
242
- /** @type {(lines: Line[], style: any) => Line[]} */
243
- let dress = (lines, style) => {
244
- if (!dressed(style)) return lines;
245
- for (let line of lines) stamp(line, !!style.underline, !!style.strikethrough);
246
- return lines;
247
- };
248
-
249
288
  /** @type {(lines: Line[]) => number} */
250
289
  let heightOf = (lines) => lines.reduce((total, line) => total + line.h, 0);
251
290
 
252
- export { atoms, dress, heightOf, wrap };
291
+ export { atoms, heightOf, wrap };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quario/layout",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "The paged display list for quario — the layout the PDF target writes and the viewer paints — in the makings, not yet released",
5
5
  "homepage": "https://getquario.com",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -43,13 +43,13 @@
43
43
  "@size-limit/preset-small-lib": "^13.0.3",
44
44
  "@types/fontkit": "^2.0.9",
45
45
  "fontkit": "^2.0.4",
46
- "quario": "^0.5.0",
46
+ "quario": "^0.7.0",
47
47
  "size-limit": "^13.0.3",
48
48
  "typescript": "^7.0.2"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "fontkit": "^2.0.4",
52
- "quario": "^0.5.0"
52
+ "quario": "^0.7.0"
53
53
  },
54
54
  "peerDependenciesMeta": {
55
55
  "fontkit": {