@quario/docx 0.1.1 → 0.2.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 CHANGED
@@ -1,5 +1,26 @@
1
1
  # @quario/docx
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - **A group's `break` names a position, and `break: "page"` is retired.** Its values are `"before"`, `"between"`, `"after"` and `"around"`. Write `break: "before"` where you wrote `break: "page"`. That is the whole of the migration, and it changes nothing the report produces.
8
+
9
+ The boundary between two consecutive instances always turns a page. `before` adds the leading edge of the run, so the first instance opens a page of its own. `between` adds neither edge, which keeps the first instance on the page the report header opened — the case the old vocabulary could not express, and the reason for the change. `after` adds the trailing edge, and `around` adds both. A trailing edge turns the page for what follows the run, so a report whose last band is that group reads `after` as `between` and `around` as `before`. A nested group takes its two edges from each instance of the group above it, rather than from the document. `"page"` said which unit a break used, where the four say where it falls; one set cannot say both and still read at a glance.
10
+
11
+ **`reset: "page"` turns no page of its own, and now needs a `break` beside it.** It says only that a new `page.number` / `page.total` sequence starts at this instance. A sequence owns whole pages, so `reset` requires `break` to be `"before"` or `"around"`. Any other `break`, and `reset` with no `break` at all, is a definition error the compile reports. Add `break: "before"` to a group that declares `reset` alone today.
12
+
13
+ The HTML target adds `q-break` to an instance whose leading edge turns, as before, and the new `q-break-after` to one whose trailing edge turns. `@quario/html/style.css` gains `.q-break-after { break-after: page }` beside the rule it already shipped for `.q-break`.
14
+
15
+ The Word target also stops losing a page break a table would swallow. A table carries no paragraph properties, so a break owed where one starts had nowhere to sit and reached the next paragraph instead, on the wrong page or on none. It now gets a paragraph of its own, the same carrier a section break already took.
16
+
17
+ The render-event stream states the two edges rather than the four positions. `group-start` carries `break` where a page turns before the instance, and the new `breakAfter` where one turns after it, so a target reads boundaries and never the position that asked for them.
18
+
19
+ ### Patch Changes
20
+
21
+ - Updated dependencies
22
+ - quario@0.10.0
23
+
3
24
  ## 0.1.1
4
25
 
5
26
  ### Patch Changes
package/README.md CHANGED
@@ -69,7 +69,7 @@ that depends on a style lookup is a look three readers may resolve three ways.
69
69
  Page bands become a section's header and footer. A bare `{{ page.number }}` or `{{ page.total }}`
70
70
  becomes a live `PAGE` / `NUMPAGES` field the reader's own application recomputes. Anything computed
71
71
  from them freezes at the value the render saw. A group's `reset: "page"` opens a section that
72
- restarts the numbering, and `break: "page"` starts the instance on a fresh page.
72
+ restarts the numbering, and a `break` turns a page at every boundary its position names.
73
73
 
74
74
  ## Determinism
75
75
 
@@ -77,6 +77,12 @@ Two renders of one report are byte-identical, on **every supported runtime**. `f
77
77
  JavaScript, so its deflate output does not vary by engine. The package stamps every entry
78
78
  1980-01-01 and nothing in the document carries a clock, so a digest over the bytes is a fair test.
79
79
 
80
+ ## Unlicensed marking
81
+
82
+ An unlicensed render writes the wording from `report-start.marking` as the first paragraph of
83
+ every footer. A licensed render writes none. The per-footer presence is normative. The wording's
84
+ look is best-effort.
85
+
80
86
  ## Documentation
81
87
 
82
88
  [The quario documentation](https://getquario.com/docs/) is the reference.
@@ -96,5 +102,3 @@ Pass your license key once, on the instance. quario verifies it offline:
96
102
  const q = quario({ license: "quario_..." });
97
103
  await q.license; // { licensed: true, licensee: "Acme BV", id: "1-ACME" }
98
104
  ```
99
-
100
- An unlicensed render carries the marking as the first paragraph of every footer.
package/lib/body.js CHANGED
@@ -11,9 +11,13 @@
11
11
  * SCHEMA.md's "an instance already at the top of a page does not force an empty
12
12
  * page".
13
13
  *
14
- * **Page breaks.** `break: "page"` is `pageBreakBefore` on the instance's first
15
- * paragraph, skipped where the instance also declared `reset`, whose section has
16
- * already turned the page, and where it has nothing before it at all.
14
+ * **Page breaks.** The stream resolved a group's `break` position into the two
15
+ * edges of each instance (ADR 0077). A leading edge is `pageBreakBefore` on the
16
+ * instance's first paragraph, skipped where the instance also declared `reset`,
17
+ * whose section has already turned the page, and where it has nothing before it
18
+ * at all. A trailing edge is the same property on whatever paragraph follows the
19
+ * instance, and a document that ends owing one drops it: a break turns the page
20
+ * for what follows, and nothing follows.
17
21
  *
18
22
  * **Page columns.** A count on the report columns the body's own sections; a
19
23
  * count on a group wraps each instance in *continuous* section breaks, so the
@@ -28,19 +32,22 @@
28
32
  *
29
33
  * **Layers.** Outward-in: this target's baseline (`docDefaults`, so nothing is
30
34
  * written per run), the report default, the band-role default, then the node's
31
- * own — SCHEMA.md, "Style declarations". A `false` in an inner layer is a
32
- * declaration and wins, which a plain overwrite already gives.
35
+ * own. A `false` in an inner layer is a declaration and wins.
33
36
  */
34
37
  import { headingAt } from "./stylepart.js";
35
38
  import { paraProps, under } from "./style.js";
36
- import { grid, record, splitting, tbl } from "./table.js";
37
- import { describe, drawing, idOf, sizeOf } from "./picture.js";
39
+ import { grid, record, tbl } from "./table.js";
40
+ import { content, splitBlock } from "./content.js";
38
41
  import { EMPTY_CTX } from "./furniture.js";
39
- import { looking, paragraph, runs } from "./text.js";
42
+ import { paragraph } from "./text.js";
43
+
44
+ // The page break a paragraph wears, spelled once: `push` folds it into a
45
+ // paragraph's own properties, and `carrier` gives it one where no paragraph is
46
+ // coming to take it.
47
+ const BREAK = "<w:pageBreakBefore/>";
40
48
 
41
49
  // The band roles this target supplies a default for, and the only look it puts
42
- // on a report that declared nothing. ADR 0014: a target supplies defaults where
43
- // its consumer has no seam to supply them, and a document is final on open.
50
+ // on a report that declared nothing (ADR 0014: a document is final on open).
44
51
  // `test/omakase-defaults.test.js` holds this copy and the other two in
45
52
  // agreement; it is not shared code, and that is the ADR's point.
46
53
  /** @type {Record<string, any>} */
@@ -91,15 +98,12 @@ export let bodyOf = (section) => {
91
98
  /** What a render carries that the events do not: the report default, the
92
99
  * instance's `format` configuration, the text column and the picture
93
100
  * registry. Settled once, on the opening event, and read-only after —
94
- * `furniture.js`'s `Ctx`, so the two writers describe a render one way.
95
- * @type {import("./furniture.js").Ctx} */
101
+ * `content.js`'s `Ctx`, so the two writers describe a render one way.
102
+ * @type {import("./content.js").Ctx} */
96
103
  let ctx = EMPTY_CTX;
97
104
  /** The table being filled, if any: its grid, and the rows written so far. */
98
105
  /** @type {{ widths: number[], rows: string } | null} */
99
106
  let table = null;
100
- /** The split being filled, if any. Splits never nest, so one is enough. */
101
- /** @type {ReturnType<typeof splitting> | null} */
102
- let split = null;
103
107
 
104
108
  // `CT_PPr` is a sequence and not a bag: `pStyle` is its first child,
105
109
  // `pageBreakBefore` its fourth, and everything `paraProps` writes comes after
@@ -107,17 +111,24 @@ export let bodyOf = (section) => {
107
111
  /** @type {(props: string, style: any, inner: string) => void} */
108
112
  let push = (props, style, inner) => {
109
113
  let look = owed.gapped ? under({ spaceBefore: GAP }, style) : style;
110
- blocks.push({
111
- props: props + (owed.broken ? "<w:pageBreakBefore/>" : "") + paraProps(look),
112
- inner,
113
- });
114
+ blocks.push({ props: props + (owed.broken ? BREAK : "") + paraProps(look), inner });
114
115
  owed = { broken: false, gapped: false };
115
116
  };
116
117
 
118
+ /** An empty paragraph, written where something is owed and no paragraph is
119
+ * coming to take it: a section break whose last block is a table, and a page
120
+ * break a table would otherwise swallow.
121
+ * @type {(props?: string) => void} */
122
+ let carrier = (props = "") => void blocks.push({ props, inner: "" });
123
+
117
124
  /** A block that is not a paragraph -- a table -- which carries no properties
118
- * and so cannot take what the next paragraph is owed.
125
+ * and so cannot take what the next paragraph is owed. A page break takes a
126
+ * carrier of its own first, or the table would swallow it; the band gap
127
+ * does not, because the next paragraph still takes it.
119
128
  * @type {(inner: string) => void} */
120
129
  let block = (inner) => {
130
+ if (owed.broken) carrier(BREAK);
131
+ owed.broken = false;
121
132
  blocks.push({ props: "", inner, raw: true });
122
133
  };
123
134
 
@@ -128,15 +139,17 @@ export let bodyOf = (section) => {
128
139
  sections.push(section(kind));
129
140
  };
130
141
 
131
- /** The report's own page-column count, and the depths of the instances that
132
- * opened a columned section, so `group-end` knows whose to close. */
142
+ /** The report's own page-column count. */
133
143
  let columns = 1;
134
- /** @type {number[]} */
135
- let columned = [];
144
+ /** One frame per open instance, innermost last: the columned section this
145
+ * instance opened, if any, and its trailing page break. The brackets pair,
146
+ * so `group-end` pops the frame `group-start` pushed.
147
+ * @type {{ columns: number, trailing: boolean }[]} */
148
+ let instances = [];
136
149
  /** @type {(value: any) => number} */
137
150
  let counted = (value) => (Number.isInteger(value) && value > 1 ? value : 0);
138
151
 
139
- /** @type {(own: import("./furniture.js").Ctx, event: any) => void} */
152
+ /** @type {(own: import("./content.js").Ctx, event: any) => void} */
140
153
  let start = (own, event) => {
141
154
  ctx = own;
142
155
  columns = counted(event.columns) || 1;
@@ -165,7 +178,7 @@ export let bodyOf = (section) => {
165
178
  let closeSection = () => {
166
179
  let last = blocks.at(-1);
167
180
  // oxlint-disable-next-line no-unused-expressions
168
- (blocks.length === sectionStart || last?.raw) && blocks.push({ props: "", inner: "" });
181
+ (blocks.length === sectionStart || last?.raw) && carrier();
169
182
  /** @type {any} */ (blocks.at(-1)).closes = sections.at(-1);
170
183
  };
171
184
 
@@ -183,13 +196,12 @@ export let bodyOf = (section) => {
183
196
  // has not started: groups read as blocks without authored margins.
184
197
  // oxlint-disable-next-line no-unused-expressions
185
198
  owed.gapped ||= blocks.length > 0;
199
+ // Its own count is what `group-end` closes; a section that merely inherits
200
+ // the report's is not one this instance opened.
186
201
  let own = counted(event.columns);
202
+ instances.push({ columns: own, trailing: event.breakAfter === "page" });
187
203
  let kind = sectionFor(event.reset === "page", own);
188
204
  if (!kind) return breakBefore(event);
189
- // Its own count is what `group-end` closes; a section that merely inherits
190
- // the report's is not one this instance opened.
191
- // oxlint-disable-next-line no-unused-expressions
192
- own && columned.push(event.depth);
193
205
  reopen(kind);
194
206
  };
195
207
 
@@ -210,11 +222,19 @@ export let bodyOf = (section) => {
210
222
  open(kind);
211
223
  };
212
224
 
213
- /** @type {(event: any) => void} */
214
- let groupEnd = (event) => {
215
- if (columned.at(-1) !== event.depth) return;
216
- columned.pop();
217
- reopen({ continuous: true, columns: columns });
225
+ /** What an instance's close owes, in the order the document takes them: the
226
+ * trailing page break the stream stated when it opened, then the columned
227
+ * section it opened.
228
+ * @type {(frame: { columns: number, trailing: boolean }) => void} */
229
+ let closeInstance = (frame) => {
230
+ if (frame.trailing) owed.broken ||= blocks.length > 0;
231
+ if (frame.columns) reopen({ continuous: true, columns });
232
+ };
233
+
234
+ /** @type {() => void} */
235
+ let groupEnd = () => {
236
+ let frame = instances.pop();
237
+ if (frame) closeInstance(frame);
218
238
  };
219
239
 
220
240
  /**
@@ -230,20 +250,6 @@ export let bodyOf = (section) => {
230
250
  return owes;
231
251
  };
232
252
 
233
- /** One item or picture, as the content of the paragraph it becomes. `column`
234
- * is what caps a picture: the text column, or the slot holding it.
235
- * @type {(event: any, style: any, column?: number) => string} */
236
- let contentOf = (event, style, column = columnWidth()) =>
237
- event.type === "image"
238
- ? pictureRun(event, column)
239
- : runs(event.tokens, looking(style, ctx.intl));
240
-
241
- /** @type {(event: any, column: number) => string} */
242
- let pictureRun = (event, column) => {
243
- let rId = ctx.picture(event);
244
- return drawing(event, idOf(rId), rId, describe(event), sizeOf(event, column));
245
- };
246
-
247
253
  /** How wide the body is where it is being written: the text column, divided
248
254
  * between the [page columns](../../../SCHEMA.md#page-columns) of the section
249
255
  * it lands in, less the gutter between them. `708` twips is Word's own. */
@@ -254,23 +260,13 @@ export let bodyOf = (section) => {
254
260
 
255
261
  /** @type {(event: any) => void} */
256
262
  let itemOf = (event) => {
257
- if (split) {
258
- let own = under(split.style, event.style);
259
- return split.slot(own, (column) => contentOf(event, own, column));
260
- }
261
263
  let style = styleOf(event);
262
- push(claimHeading(event.role), style, contentOf(event, style));
264
+ push(claimHeading(event.role), style, content(event, style, columnWidth(), ctx));
263
265
  };
264
266
 
265
267
  /** @type {(event: any) => void} */
266
- let splitStart = (event) => {
267
- split = splitting(event.slots, columnWidth(), styleOf(event));
268
- };
269
-
270
- let splitEnd = () => {
271
- // oxlint-disable-next-line no-unused-expressions
272
- split && block(split.close());
273
- split = null;
268
+ let splitOf = (event) => {
269
+ block(splitBlock(event, columnWidth(), styleOf(event), ctx));
274
270
  };
275
271
 
276
272
  /** What a table's cells are written with. `intl` is settled on the opening
@@ -319,22 +315,27 @@ export let bodyOf = (section) => {
319
315
  "group-end": groupEnd,
320
316
  item: itemOf,
321
317
  image: itemOf,
322
- "split-start": splitStart,
323
- "split-end": splitEnd,
318
+ split: splitOf,
324
319
  "table-start": tableStart,
325
320
  row,
326
321
  "total-row": row,
327
322
  "table-end": tableEnd,
328
323
  },
329
324
  /** @type {(sectPr: (section: any) => string) => string} */
330
- xml: (sectPr) =>
331
- blocks
332
- .map(
333
- (b) =>
334
- /** @type {any} */ (b).raw
335
- ? b.inner
336
- : paragraph(b.props + (b.closes ? sectPr(b.closes) : ""), b.inner),
337
- )
338
- .join("") + sectPr(sections.at(-1)),
325
+ xml: (sectPr) => {
326
+ // A page break the document ends owing is dropped, and `owed` is simply
327
+ // left set: a break turns the page for what follows it, and nothing
328
+ // follows. Word, the PDF and a printed fragment agree on that.
329
+ return (
330
+ blocks
331
+ .map(
332
+ (b) =>
333
+ /** @type {any} */ (b).raw
334
+ ? b.inner
335
+ : paragraph(b.props + (b.closes ? sectPr(b.closes) : ""), b.inner),
336
+ )
337
+ .join("") + sectPr(sections.at(-1))
338
+ );
339
+ },
339
340
  };
340
341
  };
package/lib/content.js ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * What an item, a picture or a split becomes, one way for both writers: the
3
+ * body and a page band fill the same paragraphs and the same one-row table,
4
+ * and differ on nothing but the layer under an event's style and which field
5
+ * numbers the pages. Both arrive in `ctx`, so neither writer keeps a copy.
6
+ */
7
+ import { describe, drawing, idOf, sizeOf } from "./picture.js";
8
+ import { under } from "./style.js";
9
+ import { splitting } from "./table.js";
10
+ import { looking, runs } from "./text.js";
11
+
12
+ /**
13
+ * What a render carries that the events do not: the report default every band
14
+ * item wears under its own, the instance's `format` configuration, the text
15
+ * column a picture is capped at, the registry a picture's bytes become a part
16
+ * in, and -- for a page band only -- which field gives the length of the
17
+ * sequence its section numbers. Both writers hand one of these down, so they
18
+ * describe a render one way.
19
+ *
20
+ * @typedef {{ base: any, intl: any, width: number,
21
+ * picture: (event: any) => string, sequence?: string }} Ctx
22
+ */
23
+
24
+ /**
25
+ * One item's or picture's content: the runs a text item's tokens become, or
26
+ * the inline drawing a picture is. `column` is what caps a picture: the text
27
+ * column, or the slot holding it.
28
+ *
29
+ * @type {(event: any, style: any, column: number, ctx: Ctx) => string}
30
+ */
31
+ export let content = (event, style, column, ctx) => {
32
+ if (event.type !== "image") return runs(event.tokens, looking(style, ctx.intl, ctx.sequence));
33
+ let rId = ctx.picture(event);
34
+ return drawing(event, idOf(rId), rId, describe(event), sizeOf(event, column));
35
+ };
36
+
37
+ /**
38
+ * One split, folded, as the borderless one-row table it becomes: a slot per
39
+ * cell, each slot's own style layered over the split's, which is `style`.
40
+ *
41
+ * @type {(event: any, width: number, style: any, ctx: Ctx) => string}
42
+ */
43
+ export let splitBlock = (event, width, style, ctx) => {
44
+ let split = splitting(event.slots, width, style);
45
+ for (let slot of event.items) {
46
+ let own = under(split.style, slot.style);
47
+ split.slot(own, (column) => content(slot, own, column, ctx));
48
+ }
49
+ return split.close();
50
+ };
package/lib/furniture.js CHANGED
@@ -14,16 +14,15 @@
14
14
  * what the probe below saw.
15
15
  *
16
16
  * *Where* on the page comes from the **probe**: the band closure is evaluated
17
- * at page 1 of 2 and at page 2 of 2, and the two results are compared. Equal
18
- * which is what a band holding nothing but fields and literal text is — means
19
- * one part and no `titlePg`; different means a first-page part, a default one,
20
- * and `titlePg` in the section. The probe assumes a total of two, so a band
21
- * gated on `page.total` sees 2, exactly as the PDF target's height probe does.
17
+ * at page 1 of 2 and at page 2 of 2, and the two results are compared. Equal
18
+ * means one part and no `titlePg`; different means a first-page part, a
19
+ * default one, and `titlePg` in the section. The probe assumes a total of two,
20
+ * so a band gated on `page.total` sees 2.
22
21
  */
23
- import { describe, drawing, idOf, sizeOf } from "./picture.js";
22
+ import { splits } from "quario";
23
+ import { content, splitBlock } from "./content.js";
24
24
  import { paraProps, under } from "./style.js";
25
- import { splitting } from "./table.js";
26
- import { looking, paragraph, run, runs } from "./text.js";
25
+ import { paragraph, run } from "./text.js";
27
26
  import { R, W, XML } from "./xml.js";
28
27
 
29
28
  // The two pages every band is read at, in order: the first of two, then a
@@ -40,67 +39,32 @@ const MARKING_LOOK = '<w:color w:val="808080"/><w:sz w:val="14"/>';
40
39
 
41
40
  /**
42
41
  * One band's events as the blocks they become: an item or a picture is a
43
- * paragraph, a split is a borderless one-row table, and the bracket is filled
44
- * exactly as the body fills one `splitting` is the shared rule, so the two
45
- * cannot drift on what a split is.
46
- *
47
- * `ctx` is what a render carries that the events do not: the report default
48
- * every band item wears under its own, the instance's `format` configuration,
49
- * the text column a picture is capped at, and the registry a picture's bytes
50
- * become a part in.
42
+ * paragraph, a split is a borderless one-row table. `splitting` is the shared
43
+ * rule, so this and the body cannot drift on what a split is.
51
44
  *
52
45
  * @type {(events: any[], ctx: Band) => string}
53
46
  */
54
47
  let banded = (events, ctx) => {
55
- /** @type {{ out: string, split: ReturnType<typeof splitting> | null }} */
56
- let state = { out: "", split: null };
57
- for (let e of events) STEPS.get(e.type)?.(state, e, ctx);
58
- return state.out;
59
- };
60
-
61
- /** @type {(state: any, e: any, ctx: Band) => void} */
62
- let open = (state, e, ctx) => {
63
- state.split = splitting(e.slots, ctx.width, under(ctx.base, e.style));
48
+ let out = "";
49
+ for (let e of splits(events)) out += STEPS[e.type]?.(e, ctx) ?? "";
50
+ return out;
64
51
  };
65
52
 
66
- /** @type {(state: any) => void} */
67
- let close = (state) => {
68
- state.out += state.split ? state.split.close() : "";
69
- state.split = null;
70
- };
71
-
72
- /** One item or picture, into the split being filled or into the band itself.
73
- * @type {(state: any, e: any, ctx: Band) => void} */
74
- let draw = (state, e, ctx) => {
75
- if (state.split) {
76
- let own = under(state.split.style, e.style);
77
- return state.split.slot(own, (/** @type {number} */ column) => content(e, own, column, ctx));
78
- }
53
+ /** One item or picture, as the paragraph it becomes in the band.
54
+ * @type {(e: any, ctx: Band) => string} */
55
+ let draw = (e, ctx) => {
79
56
  let style = under(ctx.base, e.style);
80
- state.out += paragraph(paraProps(style), content(e, style, ctx.width, ctx));
57
+ return paragraph(paraProps(style), content(e, style, ctx.width, ctx));
81
58
  };
82
59
 
83
- // What each kind of band event does to the band being built. An event this does
84
- // not know contributes nothing rather than failing, which is what lets a
85
- // consumer written before an event kind existed go on working.
86
- /** @type {Map<string, (state: any, e: any, ctx: Band) => void>} */
87
- const STEPS = new Map([
88
- ["split-start", open],
89
- ["split-end", close],
90
- ["item", draw],
91
- ["image", draw],
92
- ]);
60
+ /** @type {(e: any, ctx: Band) => string} */
61
+ let drawSplit = (e, ctx) => splitBlock(e, ctx.width, under(ctx.base, e.style), ctx);
93
62
 
94
- /**
95
- * One item's or picture's content.
96
- *
97
- * @type {(e: any, style: any, column: number, ctx: Band) => string}
98
- */
99
- let content = (e, style, column, ctx) => {
100
- if (e.type !== "image") return runs(e.tokens, looking(style, ctx.intl, ctx.sequence));
101
- let rId = ctx.picture(e);
102
- return drawing(e, idOf(rId), rId, describe(e), sizeOf(e, column));
103
- };
63
+ // What each kind of band event becomes. An event this does not know
64
+ // contributes nothing rather than failing, which is what lets a consumer
65
+ // written before an event kind existed go on working.
66
+ /** @type {Record<string, (e: any, ctx: Band) => string>} */
67
+ const STEPS = { split: drawSplit, item: draw, image: draw };
104
68
 
105
69
  /**
106
70
  * One reading of a band at one page, behind whatever the target leads the part
@@ -110,16 +74,7 @@ let content = (e, style, column, ctx) => {
110
74
  */
111
75
  let reading = (closure, at, lead, ctx) => lead + (closure ? banded(closure(at), ctx) : "");
112
76
 
113
- /**
114
- * What a render carries that the events do not: the report default every band
115
- * item wears under its own, the instance's `format` configuration, the text
116
- * column a picture is capped at, and the registry a picture's bytes become a
117
- * part in. `body.js` holds the same shape, so the two writers describe a render
118
- * one way.
119
- *
120
- * @typedef {{ base: any, intl: any, width: number,
121
- * picture: (event: any) => string }} Ctx
122
- */
77
+ /** @typedef {import('./content.js').Ctx} Ctx */
123
78
 
124
79
  /**
125
80
  * A `Ctx` for one sequence: the same render, plus which field gives the length
@@ -130,9 +85,9 @@ let reading = (closure, at, lead, ctx) => lead + (closure ? banded(closure(at),
130
85
  */
131
86
 
132
87
  /**
133
- * The part registry: every distinct part written once, whatever asks for it.
134
- * Sections opened by `reset: "page"` all number themselves the same way, so a
135
- * document of two hundred invoices carries one footer part, not two hundred.
88
+ * The part registry: every distinct part written once. Sections opened by
89
+ * `reset: "page"` all number themselves the same way, so a document of two
90
+ * hundred invoices carries one footer part.
136
91
  *
137
92
  * @param {any} [page] `report-start.page`, absent when no band is declared.
138
93
  * @param {string} [marking] The wording, on an unlicensed render.
@@ -167,8 +122,7 @@ export let furnish = (page, marking, relate = () => "", ctx = EMPTY_CTX) => {
167
122
  // reads it as "the first page takes the first-page parts" -- so a band with
168
123
  // no `first` reference shows *nothing* on page one once the other band has
169
124
  // turned it on. Both therefore name a first-page part whenever either needs
170
- // one; the steady one names the part it already wrote, which the registry
171
- // hands back rather than writing twice.
125
+ // one; the steady one names the part it already wrote.
172
126
  /** @type {(kind: string, pair: string[], titlePg: boolean) => string} */
173
127
  let references = (kind, [first, later], titlePg) => {
174
128
  /** @type {(type: string, body: string) => string} */
package/lib/index.js CHANGED
@@ -6,12 +6,10 @@
6
6
  * one is quario's.
7
7
  *
8
8
  * This module is the entry — the options a host writes, the walk over the
9
- * stream, and the parts a package is made of. The writers each event kind
10
- * needs live beside it: `body.js` for the paragraphs a report resolves to,
11
- * `furniture.js` for the parts a page band becomes, `styles.js` for the base
12
- * every part inherits, and `text.js` for the runs all of them are made of.
9
+ * stream, and the parts a package is made of. The writers live beside it:
10
+ * `body.js`, `furniture.js`, `styles.js` and `text.js`.
13
11
  */
14
- import { hostMeta, hostOptions, walk } from "quario";
12
+ import { hostMeta, hostOptions, splits, walk } from "quario";
15
13
  import { bodyOf } from "./body.js";
16
14
  import { DOCUMENT_NS, furnish } from "./furniture.js";
17
15
  import { STYLES } from "./stylepart.js";
@@ -153,8 +151,6 @@ let parts = (body, meta, furniture, rels) => ({
153
151
  * bytes described in SCHEMA.md ("The DOCX target"). Options are taken and
154
152
  * validated at the factory call.
155
153
  *
156
- * The options are described once, in the hand-written public declarations,
157
- * and read back here -- a second copy in JSDoc is a copy that drifts.
158
154
  * @import { DocxOptions } from './index.d.ts'
159
155
  *
160
156
  * @param {DocxOptions} [options] Host controls (see SCHEMA.md, "The DOCX target").
@@ -176,9 +172,7 @@ export function docx(options) {
176
172
  // The sections are written after the walk, so the opening event is taken
177
173
  // in its handler rather than peeked ahead of the driver: the margin a
178
174
  // document may declare instead of the host is settled by the time any
179
- // second event is pulled, and the stream reaches `walk` unwrapped. The
180
- // walk is what spends the render's budget and breathes for the host
181
- // meanwhile.
175
+ // second event is pulled, and the stream reaches `walk` unwrapped.
182
176
  let geo = geometry(page);
183
177
  // Every relationship the document carries, in one place: the parts it
184
178
  // always has, then whatever the furniture and the body ask for. One
@@ -194,13 +188,11 @@ export function docx(options) {
194
188
  rels.push({ path: "word/" + target, target, kind, rId });
195
189
  return rId;
196
190
  };
197
- // One part per distinct picture. Keyed on the bytes themselves: the
191
+ // One part per distinct picture, keyed on the bytes themselves: the
198
192
  // page-band probe reads each band twice and every section reads it again,
199
- // and a logo that arrived by reading a field is the same array each time,
200
- // so the part -- and the header XML naming it -- stays identical and the
201
- // furniture's own dedup still collapses the sections. A `source` that
202
- // *computes* fresh bytes per call writes a part per call, which is
203
- // wasteful and still correct.
193
+ // so the part stays identical and the furniture's dedup still collapses
194
+ // the sections. A `source` computing fresh bytes per call writes a part
195
+ // per call, which is wasteful and still correct.
204
196
  /** @type {Map<Uint8Array, string>} */
205
197
  let drawn = new Map();
206
198
  /** @type {(event: any) => string} */
@@ -221,13 +213,13 @@ export function docx(options) {
221
213
  ...furniture.refsFor(kind.reset ? SECTION_PAGES : DOCUMENT_PAGES),
222
214
  });
223
215
  let body = bodyOf(section);
224
- await walk(stream(data), {
216
+ await walk(splits(stream(data)), {
225
217
  ...body.handlers,
226
218
  "report-start": (event) => {
227
219
  geo = geometry(page, event);
228
220
  // What a render carries that the events do not, settled once and handed
229
221
  // to both writers: a fifth instance field must be read here and nowhere
230
- // else (`furniture.js`, `Ctx`).
222
+ // else (`content.js`, `Ctx`).
231
223
  let ctx = {
232
224
  base: event.style || null,
233
225
  intl: { locale: event.locale, currency: event.currency, timeZone: event.timeZone },
package/lib/pack.js CHANGED
@@ -3,11 +3,8 @@
3
3
  * fflate, which is what makes the writer a seam — a later one (a sibling OOXML
4
4
  * writer, `CompressionStream`) replaces this file and nothing else.
5
5
  *
6
- * Only the synchronous names are ever called. The asynchronous family would
7
- * reach for a worker built from a string, and the suites run under
8
- * `--disallow-code-generation-from-strings` with the browser page under a
9
- * `default-src 'none'` policy, so the path this target takes is proved rather
10
- * than promised.
6
+ * Only the synchronous names are ever called: the asynchronous family would
7
+ * reach for a worker built from a string.
11
8
  */
12
9
  import { strToU8, zipSync } from "fflate";
13
10
 
package/lib/page.js CHANGED
@@ -5,14 +5,11 @@
5
5
  * — the rules and their wording, not the frame derivation this target has no
6
6
  * use for, because Word does the paginating.
7
7
  *
8
- * ADR 0039 gates the page-size table to one home, and `test/page-sizes.test.js`
9
- * is that gate: a second table is a size that means one thing here and another
10
- * there. This target is the one consumer that cannot reach the layout's — a
11
- * flow target measures nothing, and taking `@quario/layout` would install the
12
- * pagination engine and its font metrics for a two-entry table (ADR 0014: a
13
- * target imports only public engine helpers). So the copy is hand-synced under
14
- * that same gate, which asserts this table and these wordings against the
15
- * layout's: change one and change the other, or the gate fails.
8
+ * This target cannot reach the layout's: taking `@quario/layout` would install
9
+ * the pagination engine and its font metrics for a two-entry table (ADR 0014).
10
+ * So the copy is hand-synced under `test/page-sizes.test.js`, which asserts
11
+ * this table and these wordings against the layout's: change one and change
12
+ * the other, or the gate fails (ADR 0039).
16
13
  */
17
14
 
18
15
  // The named sizes, in points, exactly as `@quario/layout` states them; the
package/lib/picture.js CHANGED
@@ -71,8 +71,7 @@ export let drawing = (event, id, rId, alt, size) => {
71
71
  };
72
72
 
73
73
  /**
74
- * A picture's description, joined through the engine's own display rule so a
75
- * `Date` in an `alt` reads as ISO 8601 UTC here as it does everywhere else, and
74
+ * A picture's description, joined through the engine's own display rule, so
76
75
  * the bytes do not move with the host's zone.
77
76
  *
78
77
  * @type {(event: any) => string}
@@ -81,10 +80,9 @@ export let describe = (event) => text(event.alt || []);
81
80
 
82
81
  /**
83
82
  * The number that names a drawing: its relationship's. One id space for the
84
- * whole document, which is what `docPr` wants a counter per writer hands the
85
- * same number to a page band's picture and the body's and stable across the
86
- * two readings of a band, where a running count would move and make every band
87
- * holding a logo look like one that differs on the first page.
83
+ * whole document, which is what `docPr` wants, and stable across the two
84
+ * readings of a band a running count would move and make every band holding
85
+ * a logo look like one that differs on the first page.
88
86
  *
89
87
  * @type {(rId: string) => number}
90
88
  */
package/lib/style.js CHANGED
@@ -1,9 +1,7 @@
1
1
  /**
2
- * This target's reading of the closed style vocabulary the counterpart of the
3
- * CSS table in the HTML target, the points-and-colours module in the layout,
4
- * and `style.js` in the XLSX target. Declarations are lenient at render: a
5
- * value of the wrong shape contributes nothing rather than reaching the
6
- * document.
2
+ * This target's reading of the closed style vocabulary. Declarations are
3
+ * lenient at render: a value of the wrong shape contributes nothing rather
4
+ * than reaching the document.
7
5
  *
8
6
  * Everything here is a property *string*, in the order the schema fixes.
9
7
  * `CT_RPr`, `CT_PPrBase`, `CT_TcPr` and the border containers are sequences,
@@ -22,10 +20,9 @@ let HEX = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
22
20
 
23
21
  // A border container's sides, in the order `CT_PBdr` and `CT_TcBorders` both
24
22
  // declare them -- which is *not* the clockwise order the authoring surface
25
- // names them in. Each carries the declaration names it reads and the element
26
- // name it writes, built once: `borders` runs per paragraph and per cell, and
27
- // rebuilding four key strings per side per call is most of the garbage this
28
- // module makes.
23
+ // names them in. Built once: `borders` runs per paragraph and per cell, and
24
+ // rebuilding four key strings per side per call is most of this module's
25
+ // garbage.
29
26
  let SIDES = ["Top", "Left", "Bottom", "Right"].map((side) => ({
30
27
  tag: side.toLowerCase(),
31
28
  width: "border" + side + "Width",
@@ -74,9 +71,8 @@ let underline = (value) =>
74
71
  /**
75
72
  * The face a declared family names. A generic maps to the name all three
76
73
  * readers ship with; any other is the author's own string, written for the
77
- * reader to substitute and **escaped** — it is the one value in this module
78
- * that is not a number, a hex colour or a closed name, so it is the one that
79
- * reaches an attribute as the author wrote it (root CLAUDE.md, constraint 4).
74
+ * reader to substitute and **escaped** — the one value in this module that is
75
+ * not a number, a hex colour or a closed name.
80
76
  *
81
77
  * @type {(family: any) => string}
82
78
  */
@@ -123,13 +119,10 @@ let tint = (value) => {
123
119
  * Unicode default case mapping, never the host's locale, exactly as the PDF
124
120
  * target does for its own reason (`text.js`).
125
121
  *
126
- * `boxed` is the same question `paraProps` asks below and means the same thing:
127
- * *is this element the one that draws the box?* The box belongs to the
128
- * innermost container that can hold one, so a `background` on a cell or an item
129
- * is that container's shading and a run draws none which is why the default
130
- * here is `false` and there is `true`. Only a
131
- * [styled run](../../../SCHEMA.md#cell-values) has nothing but the run to draw
132
- * its own on.
122
+ * `boxed` is the same question `paraProps` asks below: *is this element the
123
+ * one that draws the box?* The box belongs to the innermost container that can
124
+ * hold one, which is why the default here is `false` and there `true`. Only a
125
+ * [styled run](../../../SCHEMA.md#cell-values) has nothing but the run.
133
126
  *
134
127
  * @type {(style: any, boxed?: boolean) => string}
135
128
  */
package/lib/stylepart.js CHANGED
@@ -1,8 +1,7 @@
1
1
  /**
2
2
  * The `styles.xml` part, written on every render and carrying two things.
3
- * Named for the part rather than for the vocabulary: `style.js` beside it is
4
- * this target's reading of what an author declares, and the two are imported
5
- * together often enough that one letter between them would be a trap.
3
+ * Named for the part rather than the vocabulary: `style.js` beside it is this
4
+ * target's reading of what an author declares.
6
5
  *
7
6
  * `docDefaults` is the base every run and paragraph inherits: the sans
8
7
  * generic's own name, the report default's size, and the line spacing the
package/package.json CHANGED
@@ -1,7 +1,14 @@
1
1
  {
2
2
  "name": "@quario/docx",
3
- "version": "0.1.1",
4
- "description": "The Word render target for quario a flow target: real Word tables, a navigable outline, live page-number fields",
3
+ "version": "0.2.0",
4
+ "description": "Tiny, flow-based Word render target for quario. Real Word tables, a navigable outline, live page-number fields.",
5
+ "keywords": [
6
+ "csp",
7
+ "docx",
8
+ "ooxml",
9
+ "quario",
10
+ "word"
11
+ ],
5
12
  "homepage": "https://getquario.com",
6
13
  "license": "SEE LICENSE IN LICENSE",
7
14
  "repository": {
@@ -41,12 +48,12 @@
41
48
  "devDependencies": {
42
49
  "@arethetypeswrong/cli": "^0.18.3",
43
50
  "@size-limit/preset-small-lib": "^13.0.3",
44
- "quario": "^0.9.0",
51
+ "quario": "^0.10.0",
45
52
  "size-limit": "^13.0.3",
46
53
  "typescript": "^7.0.2"
47
54
  },
48
55
  "peerDependencies": {
49
- "quario": "^0.9.0"
56
+ "quario": "^0.10.0"
50
57
  },
51
58
  "size-limit": [
52
59
  {