@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 +21 -0
- package/README.md +7 -3
- package/lib/body.js +74 -73
- package/lib/content.js +50 -0
- package/lib/furniture.js +28 -74
- package/lib/index.js +10 -18
- package/lib/pack.js +2 -5
- package/lib/page.js +5 -8
- package/lib/picture.js +4 -6
- package/lib/style.js +12 -19
- package/lib/stylepart.js +2 -3
- package/package.json +11 -4
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
|
|
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.**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
|
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,
|
|
37
|
-
import {
|
|
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 {
|
|
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
|
|
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
|
-
* `
|
|
95
|
-
* @type {import("./
|
|
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
|
|
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
|
-
/**
|
|
135
|
-
|
|
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("./
|
|
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) &&
|
|
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
|
-
/**
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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,
|
|
264
|
+
push(claimHeading(event.role), style, content(event, style, columnWidth(), ctx));
|
|
263
265
|
};
|
|
264
266
|
|
|
265
267
|
/** @type {(event: any) => void} */
|
|
266
|
-
let
|
|
267
|
-
|
|
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
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
*
|
|
19
|
-
* one
|
|
20
|
-
*
|
|
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 {
|
|
22
|
+
import { splits } from "quario";
|
|
23
|
+
import { content, splitBlock } from "./content.js";
|
|
24
24
|
import { paraProps, under } from "./style.js";
|
|
25
|
-
import {
|
|
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
|
|
44
|
-
*
|
|
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
|
-
|
|
56
|
-
let
|
|
57
|
-
|
|
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
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
57
|
+
return paragraph(paraProps(style), content(e, style, ctx.width, ctx));
|
|
81
58
|
};
|
|
82
59
|
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
134
|
-
*
|
|
135
|
-
*
|
|
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
|
|
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
|
|
10
|
-
*
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
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 (`
|
|
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
|
|
7
|
-
* reach for a worker built from a string
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
|
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
|
|
85
|
-
*
|
|
86
|
-
*
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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.
|
|
26
|
-
//
|
|
27
|
-
//
|
|
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** —
|
|
78
|
-
*
|
|
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
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
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
|
|
4
|
-
*
|
|
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.
|
|
4
|
-
"description": "
|
|
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.
|
|
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.
|
|
56
|
+
"quario": "^0.10.0"
|
|
50
57
|
},
|
|
51
58
|
"size-limit": [
|
|
52
59
|
{
|