@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/CHANGELOG.md +70 -0
- package/lib/canvas.js +63 -115
- package/lib/fonts.js +19 -9
- package/lib/image.js +1 -1
- package/lib/index.d.ts +20 -2
- package/lib/index.js +19 -12
- package/lib/layout.js +321 -369
- package/lib/measure.js +263 -0
- package/lib/page.js +40 -2
- package/lib/paint.js +30 -7
- package/lib/settings.js +88 -0
- package/lib/style.js +29 -13
- package/lib/text.js +90 -51
- package/package.json +3 -3
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,
|
|
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
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
/** @typedef {import('./
|
|
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 {{
|
|
19
|
-
*
|
|
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:
|
|
23
|
-
*
|
|
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
|
|
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[],
|
|
44
|
-
let pushParts = (out,
|
|
45
|
-
for (let part of printable(font, line).split(/( +)/).filter(Boolean))
|
|
46
|
-
out.push({ text: part,
|
|
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[],
|
|
50
|
-
let pushLine = (out,
|
|
51
|
-
if (i) out.push({ text: "",
|
|
52
|
-
pushParts(out,
|
|
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) =>
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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.
|
|
223
|
-
//
|
|
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,
|
|
291
|
+
export { atoms, heightOf, wrap };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quario/layout",
|
|
3
|
-
"version": "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.
|
|
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.
|
|
52
|
+
"quario": "^0.7.0"
|
|
53
53
|
},
|
|
54
54
|
"peerDependenciesMeta": {
|
|
55
55
|
"fontkit": {
|