@quario/pdf 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/outline.js CHANGED
@@ -49,26 +49,6 @@ let linkKids = (entry, refs, kids, n) => {
49
49
  entry.Count = n;
50
50
  };
51
51
 
52
- /** @type {(ctx: any, refs: any[], root: any, parent: number[], marks: any[], pageRefs: any[], i: number, mark: any) => void} */
53
- let writeEntry = (ctx, refs, root, parent, marks, pageRefs, i, mark) => {
54
- let siblings = kidsOf(parent, parent[i]);
55
- let index = siblings.indexOf(i);
56
- let kids = kidsOf(parent, i);
57
- let target = pageRefs[Math.min(mark.page, pageRefs.length - 1)];
58
- /** @type {Record<string, any>} */
59
- let entry = {
60
- Title: PDFHexString.fromText(mark.title),
61
- Parent: parentRef(parent, i, root, refs),
62
- // `/XYZ <x> <y> null`: scroll to the mark's line, keep the zoom. The
63
- // left edge is the mark's own column, so two instances sharing a y in
64
- // two page columns are two distinct destinations.
65
- Dest: [target, PDFName.of("XYZ"), PDFNumber.of(mark.x), PDFNumber.of(mark.y), null],
66
- };
67
- linkSiblings(entry, refs, siblings, index);
68
- linkKids(entry, refs, kids, descendants(marks, i));
69
- ctx.assign(refs[i], ctx.obj(entry));
70
- };
71
-
72
52
  /**
73
53
  * Attach an outline built from `marks` to the document.
74
54
  *
@@ -82,8 +62,24 @@ export function outline(doc, marks, pageRefs) {
82
62
  let parent = parents(marks);
83
63
  let refs = marks.map(() => ctx.nextRef());
84
64
  let root = ctx.nextRef();
85
- for (let [i, mark] of marks.entries())
86
- writeEntry(ctx, refs, root, parent, marks, pageRefs, i, mark);
65
+ for (let [i, mark] of marks.entries()) {
66
+ let siblings = kidsOf(parent, parent[i]);
67
+ let index = siblings.indexOf(i);
68
+ let kids = kidsOf(parent, i);
69
+ let target = pageRefs[Math.min(mark.page, pageRefs.length - 1)];
70
+ /** @type {Record<string, any>} */
71
+ let entry = {
72
+ Title: PDFHexString.fromText(mark.title),
73
+ Parent: parentRef(parent, i, root, refs),
74
+ // `/XYZ <x> <y> null`: scroll to the mark's line, keep the zoom. The
75
+ // left edge is the mark's own column, so two instances sharing a y in
76
+ // two page columns are two distinct destinations.
77
+ Dest: [target, PDFName.of("XYZ"), PDFNumber.of(mark.x), PDFNumber.of(mark.y), null],
78
+ };
79
+ linkSiblings(entry, refs, siblings, index);
80
+ linkKids(entry, refs, kids, descendants(marks, i));
81
+ ctx.assign(refs[i], ctx.obj(entry));
82
+ }
87
83
 
88
84
  let top = kidsOf(parent, -1);
89
85
  ctx.assign(
package/lib/style.js CHANGED
@@ -4,13 +4,12 @@
4
4
  * render: a value of the wrong shape contributes nothing rather than reaching
5
5
  * the page.
6
6
  *
7
- * `finite`/`HEX` are restated per target on purpose: the coercions are each
8
- * target's own edge, never shared engine code.
7
+ * `HEX` is restated per target on purpose: the coercion is each target's own
8
+ * edge, never shared engine code. Finiteness is not one of those coercions —
9
+ * `Number.isFinite` never coerces, so this target asks it directly.
9
10
  */
10
11
  import { rgb } from "pdf-lib";
11
12
 
12
- /** @type {(value: any) => boolean} */
13
- let finite = (value) => typeof value === "number" && Number.isFinite(value);
14
13
  let HEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
15
14
 
16
15
  // Leading as a fraction of the font size, cell padding, the gap between a
@@ -51,17 +50,20 @@ let col = (value) => {
51
50
  };
52
51
 
53
52
  /** @type {(style: any, base: number) => number} */
54
- let sizeOf = (style, base) => (finite(style.size) && style.size > 0 ? style.size : base);
53
+ let sizeOf = (style, base) => (Number.isFinite(style.size) && style.size > 0 ? style.size : base);
55
54
 
56
- // Style blocks layer outward-in: row under cell.
55
+ // Style blocks layer outward-in: row under cell, split under slot. Those are
56
+ // the two innermost of the four layers; the outer two reach a node without
57
+ // being merged into it -- the band-role default through `roled` below, and the
58
+ // report default through the canvas, as `layout.js`'s `adoptDefault` explains.
57
59
  /** @type {(under: any, over: any) => any} */
58
60
  let merge = (under, over) => (under ? (over ? { ...under, ...over } : under) : over || {});
59
61
 
60
- // Band-role omakase defaults — the outermost layer of that same stack, under
61
- // the author's own style, which therefore always wins. Only the headline
62
- // roles carry one; the XLSX target carries the same two, byte-identical, in
63
- // `packages/xlsx/lib/index.js`, and they move together (SCHEMA.md states the
64
- // pair for both). A target can only import public engine helpers, so there is
62
+ // Band-role omakase defaults — the third of those four layers, over the report
63
+ // default and under the author's own, which therefore always wins. Only the
64
+ // headline roles carry one; the XLSX target carries the same two,
65
+ // byte-identical, in `packages/xlsx/lib/index.js`, and they move together
66
+ // (SCHEMA.md states the pair for both). A target can only import public engine helpers, so there is
65
67
  // nowhere to share this from and the two copies are synced by hand.
66
68
  //
67
69
  // `@quario/html` deliberately carries none of this, nor the leading, padding
@@ -81,4 +83,15 @@ let roled = (event) =>
81
83
  ? { ...event, style: merge(ROLES[event.role], event.style) }
82
84
  : event;
83
85
 
84
- export { BAND, BLACK, GUTTER, LEAD, PADX, PADY, col, merge, roled, shift, sizeOf };
86
+ // Whether a resolved style asks for either text decoration -- one reading for
87
+ // the wrapper that stamps it on a line and the canvas that draws it.
88
+ /** @type {(style: any) => boolean} */
89
+ let dressed = (style) => !!(style && (style.underline || style.strikethrough));
90
+
91
+ // Whether a resolved style asks for capitals. This target has no
92
+ // text-transform to defer to, so the reading is here beside the rest of the
93
+ // vocabulary and `text.js` applies it before measuring.
94
+ /** @type {(style: any) => boolean} */
95
+ let upper = (style) => !!(style && style.uppercase);
96
+
97
+ export { BAND, BLACK, GUTTER, LEAD, PADX, PADY, col, dressed, merge, roled, shift, sizeOf, upper };
package/lib/text.js CHANGED
@@ -3,13 +3,19 @@
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
7
  import { ascOf, face, printable, width } from "./fonts.js";
7
- import { LEAD, col, sizeOf } from "./style.js";
8
+ import { LEAD, col, dressed, sizeOf, upper } from "./style.js";
8
9
 
9
- // What measuring needs and no more: the embedded faces to measure against
10
- // and the base size a style falls back to. A `Canvas` satisfies it, so
11
- // callers pass theirs straight in but nothing here can touch a page.
12
- /** @typedef {{ fonts: import('./fonts.js').Fonts, base: number }} Metrics */
10
+ // What measuring needs and no more: the embedded faces to measure against, and
11
+ // the base size and family a style falls back to the report default's two
12
+ // declarations, which the canvas carries for the whole render. A `Canvas`
13
+ // satisfies it, so callers pass theirs straight in — but nothing here can
14
+ // touch a page.
15
+ /**
16
+ * @typedef {{ fonts: import('./fonts.js').Fonts, base: number,
17
+ * family: string | null, locale?: string, currency?: string, timeZone?: string }} Metrics
18
+ */
13
19
 
14
20
  /**
15
21
  * @typedef {{ text: string, font: any, size: number, color: any,
@@ -21,13 +27,21 @@ import { LEAD, col, sizeOf } from "./style.js";
21
27
  * underline?: boolean, strikethrough?: boolean }} Line
22
28
  */
23
29
 
24
- /**
25
- * @typedef {{ cur: Atom[], w: number, lines: Line[], base: number,
26
- * fonts: import('./fonts.js').Fonts }} Wrap
27
- */
30
+ /** @typedef {{ cur: Atom[], w: number, lines: Line[], base: number }} Wrap */
28
31
 
29
- /** @type {(token: any) => string} */
30
- let rawOf = (token) => ("literal" in token ? token.literal : String(token.value ?? ""));
32
+ /** @type {(token: any, style: any, metrics: Metrics) => string} */
33
+ let rawOf = (token, style, metrics) => {
34
+ if ("literal" in token) return token.literal;
35
+ return format(token.value, style?.format, metrics) ?? display(token.value);
36
+ };
37
+
38
+ // This target has no text-transform to defer to, so `uppercase` is applied to
39
+ // the string before it is measured -- the widths have to be the widths of what
40
+ // is actually drawn. `toUpperCase` rather than `toLocaleUpperCase`: this
41
+ // target's output is byte-reproducible, so the host's locale must not reach
42
+ // the glyphs.
43
+ /** @type {(text: string, style: any) => string} */
44
+ let cased = (text, style) => (upper(style) ? text.toUpperCase() : text);
31
45
 
32
46
  /** @type {(font: any, line: string) => string[]} */
33
47
  let partsOf = (font, line) => printable(font, line).split(/( +)/).filter(Boolean);
@@ -48,20 +62,23 @@ let pushLine = (out, font, size, color, i, line) => {
48
62
  let look = (metrics, style) => {
49
63
  let resolved = style || {};
50
64
  return {
51
- font: face(metrics.fonts, resolved),
65
+ font: face(metrics.fonts, resolved, metrics.family),
52
66
  size: sizeOf(resolved, metrics.base),
53
67
  color: col(resolved.color),
54
68
  };
55
69
  };
56
70
 
57
71
  // Flatten a cell's tokens to word/space atoms carrying the cell's resolved
58
- // typography — one face, size and colour for the whole cell.
72
+ // typography — one face, size and colour for the whole cell. CR, LF, and
73
+ // CRLF are one hard break each (SCHEMA.md, Cell values).
59
74
  /** @type {(metrics: Metrics, tokens: any[], style: any) => Atom[]} */
60
75
  let atoms = (metrics, tokens, style) => {
61
76
  let out = /** @type {Atom[]} */ ([]);
62
77
  let { font, size, color } = look(metrics, style);
63
78
  for (let token of tokens)
64
- for (let [i, line] of rawOf(token).split("\n").entries())
79
+ for (let [i, line] of cased(rawOf(token, style, metrics), style)
80
+ .split(/\r\n|\r|\n/)
81
+ .entries())
65
82
  pushLine(out, font, size, color, i, line);
66
83
  return out;
67
84
  };
@@ -103,7 +120,9 @@ let growPiece = (run, atom) => {
103
120
  /** @type {(state: Wrap, run: Run) => void} */
104
121
  let finish = (state, run) => {
105
122
  if (!run.size) run.size = state.base;
106
- if (!run.asc) run.asc = ascOf(face(state.fonts, {}), run.size);
123
+ // No `asc` fallback: a run with no atoms is a blank line, and an ascender
124
+ // only places pieces. `drawLine` loops over none and `decorateLine` skips a
125
+ // line with no width, so a measured one would be a face lookup nothing reads.
107
126
  state.lines.push({
108
127
  pieces: run.pieces,
109
128
  w: run.w,
@@ -207,11 +226,12 @@ let placeAtom = (state, atom, avail) => {
207
226
 
208
227
  // Greedy wrap against `avail`: spaces never start a line, an over-wide word
209
228
  // breaks by character, hard breaks always break. A cell's atoms share one
210
- // typography, so a line's atoms merge into a single draw piece.
211
- /** @type {(metrics: Metrics, list: Atom[], avail: number) => Line[]} */
212
- let wrap = (metrics, list, avail) => {
229
+ // typography, so a line's atoms merge into a single draw piece. `size` is the
230
+ // empty-run height a blank item, a hard-break hole.
231
+ /** @type {(metrics: Metrics, list: Atom[], avail: number, size: number) => Line[]} */
232
+ let wrap = (metrics, list, avail, size) => {
213
233
  /** @type {Wrap} */
214
- let state = { cur: [], w: 0, lines: [], base: metrics.base, fonts: metrics.fonts };
234
+ let state = { cur: [], w: 0, lines: [], base: size };
215
235
  for (let atom of list) placeAtom(state, atom, avail);
216
236
  emit(state);
217
237
  return state.lines;
@@ -225,9 +245,6 @@ let stamp = (line, underline, strikethrough) => {
225
245
  if (strikethrough) line.strikethrough = true;
226
246
  };
227
247
 
228
- /** @type {(style: any) => boolean} */
229
- let dressed = (style) => !!(style && (style.underline || style.strikethrough));
230
-
231
248
  /** @type {(lines: Line[], style: any) => Line[]} */
232
249
  let dress = (lines, style) => {
233
250
  if (!dressed(style)) return lines;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quario/pdf",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "The browserless, paginated PDF render target for quario — in the makings, not yet released",
5
5
  "homepage": "https://getquario.com",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -42,13 +42,13 @@
42
42
  "@arethetypeswrong/cli": "^0.18.3",
43
43
  "@pdf-lib/fontkit": "^1.1.1",
44
44
  "@size-limit/preset-small-lib": "^13.0.3",
45
- "quario": "^0.1.0",
45
+ "quario": "^0.3.0",
46
46
  "size-limit": "^13.0.3",
47
47
  "typescript": "^7.0.2"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@pdf-lib/fontkit": "^1.1.1",
51
- "quario": "^0.1.0"
51
+ "quario": "^0.3.0"
52
52
  },
53
53
  "peerDependenciesMeta": {
54
54
  "@pdf-lib/fontkit": {
@@ -62,7 +62,7 @@
62
62
  "quario",
63
63
  "pdf-lib"
64
64
  ],
65
- "limit": "9.2 kB"
65
+ "limit": "10.5 kB"
66
66
  }
67
67
  ],
68
68
  "engines": {