@quario/docx 0.1.1 → 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/CHANGELOG.md +39 -0
- package/README.md +7 -3
- package/lib/body.js +129 -95
- package/lib/content.js +51 -0
- package/lib/furniture.js +87 -95
- package/lib/index.d.ts +24 -0
- package/lib/index.js +91 -52
- package/lib/pack.js +2 -5
- package/lib/page.js +5 -8
- package/lib/picture.js +4 -6
- package/lib/rels.js +66 -0
- package/lib/style.js +12 -19
- package/lib/stylepart.js +8 -7
- package/lib/table.js +8 -7
- package/lib/text.js +65 -13
- package/package.json +15 -7
package/lib/furniture.js
CHANGED
|
@@ -14,16 +14,16 @@
|
|
|
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 {
|
|
25
|
+
import { relationships } from "./rels.js";
|
|
26
|
+
import { paragraph, run } from "./text.js";
|
|
27
27
|
import { R, W, XML } from "./xml.js";
|
|
28
28
|
|
|
29
29
|
// The two pages every band is read at, in order: the first of two, then a
|
|
@@ -40,67 +40,32 @@ const MARKING_LOOK = '<w:color w:val="808080"/><w:sz w:val="14"/>';
|
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* 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.
|
|
43
|
+
* paragraph, a split is a borderless one-row table. `splitting` is the shared
|
|
44
|
+
* rule, so this and the body cannot drift on what a split is.
|
|
51
45
|
*
|
|
52
46
|
* @type {(events: any[], ctx: Band) => string}
|
|
53
47
|
*/
|
|
54
48
|
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));
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
/** @type {(state: any) => void} */
|
|
67
|
-
let close = (state) => {
|
|
68
|
-
state.out += state.split ? state.split.close() : "";
|
|
69
|
-
state.split = null;
|
|
49
|
+
let out = "";
|
|
50
|
+
for (let e of splits(events)) out += STEPS[e.type](e, ctx);
|
|
51
|
+
return out;
|
|
70
52
|
};
|
|
71
53
|
|
|
72
|
-
/** One item or picture,
|
|
73
|
-
* @type {(
|
|
74
|
-
let draw = (
|
|
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
|
-
}
|
|
54
|
+
/** One item or picture, as the paragraph it becomes in the band.
|
|
55
|
+
* @type {(e: any, ctx: Band) => string} */
|
|
56
|
+
let draw = (e, ctx) => {
|
|
79
57
|
let style = under(ctx.base, e.style);
|
|
80
|
-
|
|
58
|
+
return paragraph(paraProps(style), content(e, style, ctx.width, ctx));
|
|
81
59
|
};
|
|
82
60
|
|
|
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
|
-
]);
|
|
61
|
+
/** @type {(e: any, ctx: Band) => string} */
|
|
62
|
+
let drawSplit = (e, ctx) => splitBlock(e, ctx.width, under(ctx.base, e.style), ctx);
|
|
93
63
|
|
|
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
|
-
};
|
|
64
|
+
// What each kind of band event becomes. An event this does not know
|
|
65
|
+
// contributes nothing rather than failing, which is what lets a consumer
|
|
66
|
+
// written before an event kind existed go on working.
|
|
67
|
+
/** @type {Record<string, (e: any, ctx: Band) => string>} */
|
|
68
|
+
const STEPS = { split: drawSplit, item: draw, image: draw };
|
|
104
69
|
|
|
105
70
|
/**
|
|
106
71
|
* One reading of a band at one page, behind whatever the target leads the part
|
|
@@ -110,16 +75,7 @@ let content = (e, style, column, ctx) => {
|
|
|
110
75
|
*/
|
|
111
76
|
let reading = (closure, at, lead, ctx) => lead + (closure ? banded(closure(at), ctx) : "");
|
|
112
77
|
|
|
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
|
-
*/
|
|
78
|
+
/** @typedef {import('./content.js').Ctx} Ctx */
|
|
123
79
|
|
|
124
80
|
/**
|
|
125
81
|
* A `Ctx` for one sequence: the same render, plus which field gives the length
|
|
@@ -130,18 +86,24 @@ let reading = (closure, at, lead, ctx) => lead + (closure ? banded(closure(at),
|
|
|
130
86
|
*/
|
|
131
87
|
|
|
132
88
|
/**
|
|
133
|
-
* The part registry: every distinct part written once
|
|
134
|
-
*
|
|
135
|
-
*
|
|
89
|
+
* The part registry: every distinct part written once. Sections opened by
|
|
90
|
+
* `reset: "page"` all number themselves the same way, so a document of two
|
|
91
|
+
* hundred invoices carries one footer part.
|
|
136
92
|
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
93
|
+
* A band's own picture or link is a relationship of *its* part rather than of
|
|
94
|
+
* `document.xml`, so each reading records what it allocated and the part is
|
|
95
|
+
* written beside its own `.rels`. A reader that cannot resolve an `r:id` in the
|
|
96
|
+
* part it reads it from has a broken reference, however well the document
|
|
97
|
+
* resolves the same id.
|
|
98
|
+
*
|
|
99
|
+
* @param {any} page `report-start.page`, absent when no band is declared.
|
|
100
|
+
* @param {string} marking The wording, on an unlicensed render.
|
|
101
|
+
* @param {{ rels: any[], relate: (kind: string, target: string) => string }}
|
|
102
|
+
* registry The document's one relationship registry; the caller owns it,
|
|
103
|
+
* because the body draws pictures and links too.
|
|
104
|
+
* @param {Ctx} ctx What a render carries that the events do not.
|
|
143
105
|
*/
|
|
144
|
-
export let furnish = (page, marking,
|
|
106
|
+
export let furnish = (page, marking, registry, ctx) => {
|
|
145
107
|
/** @type {Record<string, string>} */
|
|
146
108
|
let parts = {};
|
|
147
109
|
/** @type {Map<string, string>} */
|
|
@@ -149,17 +111,34 @@ export let furnish = (page, marking, relate = () => "", ctx = EMPTY_CTX) => {
|
|
|
149
111
|
/** @type {Record<string, number>} */
|
|
150
112
|
let counts = { header: 0, footer: 0 };
|
|
151
113
|
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
114
|
+
// What the reading being taken has allocated: replaced per reading, and
|
|
115
|
+
// closed over by the wrappers below, so each part learns its own.
|
|
116
|
+
/** @type {Set<string>} */
|
|
117
|
+
let used = new Set();
|
|
118
|
+
/** @type {(allocate: any) => any} */
|
|
119
|
+
let recording = (allocate) => (/** @type {any} */ of) => {
|
|
120
|
+
let rId = allocate(of);
|
|
121
|
+
used.add(rId);
|
|
122
|
+
return rId;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// A part for this body, or the one already written for it. Identical markup
|
|
126
|
+
// refers to identical ids -- a picture is one part however often it is drawn
|
|
127
|
+
// -- so a part found by its markup is found with its relationships too.
|
|
128
|
+
/** @type {(kind: string, reading: { xml: string, used: Set<string> }) => string} */
|
|
129
|
+
let partFor = (kind, reading) => {
|
|
155
130
|
let tag = kind === "header" ? "hdr" : "ftr";
|
|
156
|
-
let xml = XML + `<w:${tag} ${W}>` +
|
|
131
|
+
let xml = XML + `<w:${tag} ${W}>` + reading.xml + `</w:${tag}>`;
|
|
157
132
|
let seen = byXml.get(xml);
|
|
158
133
|
if (seen) return seen;
|
|
159
134
|
let target = kind + ++counts[kind] + ".xml";
|
|
160
|
-
let rId = relate(kind, target);
|
|
135
|
+
let rId = registry.relate(kind, target);
|
|
161
136
|
byXml.set(xml, rId);
|
|
162
137
|
parts["word/" + target] = xml;
|
|
138
|
+
if (reading.used.size)
|
|
139
|
+
parts["word/_rels/" + target + ".rels"] = relationships(
|
|
140
|
+
registry.rels.filter((rel) => reading.used.has(rel.rId)),
|
|
141
|
+
);
|
|
163
142
|
return rId;
|
|
164
143
|
};
|
|
165
144
|
|
|
@@ -167,13 +146,12 @@ export let furnish = (page, marking, relate = () => "", ctx = EMPTY_CTX) => {
|
|
|
167
146
|
// reads it as "the first page takes the first-page parts" -- so a band with
|
|
168
147
|
// no `first` reference shows *nothing* on page one once the other band has
|
|
169
148
|
// 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
|
-
|
|
172
|
-
/** @type {(kind: string, pair: string[], titlePg: boolean) => string} */
|
|
149
|
+
// one; the steady one names the part it already wrote.
|
|
150
|
+
/** @type {(kind: string, pair: any[], titlePg: boolean) => string} */
|
|
173
151
|
let references = (kind, [first, later], titlePg) => {
|
|
174
|
-
/** @type {(type: string,
|
|
175
|
-
let ref = (type,
|
|
176
|
-
`<w:${kind}Reference w:type="${type}" r:id="${partFor(kind,
|
|
152
|
+
/** @type {(type: string, reading: any) => string} */
|
|
153
|
+
let ref = (type, reading) =>
|
|
154
|
+
`<w:${kind}Reference w:type="${type}" r:id="${partFor(kind, reading)}"/>`;
|
|
177
155
|
return (titlePg ? ref("first", first) : "") + ref("default", later);
|
|
178
156
|
};
|
|
179
157
|
|
|
@@ -186,7 +164,12 @@ export let furnish = (page, marking, relate = () => "", ctx = EMPTY_CTX) => {
|
|
|
186
164
|
* @type {(sequence: string) => { refs: string, titlePg: boolean }}
|
|
187
165
|
*/
|
|
188
166
|
let refsFor = (sequence) => {
|
|
189
|
-
let band = {
|
|
167
|
+
let band = {
|
|
168
|
+
...ctx,
|
|
169
|
+
sequence,
|
|
170
|
+
picture: recording(ctx.picture),
|
|
171
|
+
link: recording(ctx.link),
|
|
172
|
+
};
|
|
190
173
|
let lead = marking ? paragraph("", run(marking, MARKING_LOOK)) : "";
|
|
191
174
|
let bands = [
|
|
192
175
|
{ kind: "header", closure: page?.header, lead: "" },
|
|
@@ -195,9 +178,12 @@ export let furnish = (page, marking, relate = () => "", ctx = EMPTY_CTX) => {
|
|
|
195
178
|
.filter((band) => band.closure || band.lead)
|
|
196
179
|
.map(({ kind, closure, lead: own }) => ({
|
|
197
180
|
kind,
|
|
198
|
-
pair: PROBE.map((at) =>
|
|
181
|
+
pair: PROBE.map((at) => {
|
|
182
|
+
used = new Set();
|
|
183
|
+
return { xml: reading(closure, at, own, band), used };
|
|
184
|
+
}),
|
|
199
185
|
}));
|
|
200
|
-
let titlePg = bands.some(({ pair }) => pair[0] !== pair[1]);
|
|
186
|
+
let titlePg = bands.some(({ pair }) => pair[0].xml !== pair[1].xml);
|
|
201
187
|
return {
|
|
202
188
|
refs: bands.map(({ kind, pair }) => references(kind, pair, titlePg)).join(""),
|
|
203
189
|
titlePg,
|
|
@@ -207,8 +193,14 @@ export let furnish = (page, marking, relate = () => "", ctx = EMPTY_CTX) => {
|
|
|
207
193
|
return { parts, refsFor };
|
|
208
194
|
};
|
|
209
195
|
|
|
210
|
-
/** A
|
|
211
|
-
|
|
196
|
+
/** A document nothing has furnished yet: the value a render carries until its
|
|
197
|
+
* opening event says what its page bands are. A stream walked from part-way
|
|
198
|
+
* through never replaces it, and still describes a page. */
|
|
199
|
+
/** @type {{ parts: Record<string, string>, refsFor: (sequence: string) => { refs: string, titlePg: boolean } }} */
|
|
200
|
+
export const NO_FURNITURE = {
|
|
201
|
+
parts: {},
|
|
202
|
+
refsFor: () => ({ refs: "", titlePg: false }),
|
|
203
|
+
};
|
|
212
204
|
|
|
213
205
|
/** The namespaces a document referencing furniture declares. */
|
|
214
206
|
export const DOCUMENT_NS = W + " " + R;
|
package/lib/index.d.ts
CHANGED
|
@@ -34,3 +34,27 @@ export interface DocxOptions {
|
|
|
34
34
|
* bytes described in SCHEMA.md ("The DOCX target").
|
|
35
35
|
*/
|
|
36
36
|
export function docx(options?: DocxOptions): Target<"docx", Promise<Uint8Array>>;
|
|
37
|
+
|
|
38
|
+
/** What a target makes of a declaration: CONTEXT.md's fate vocabulary. */
|
|
39
|
+
export type Fate = "resolved" | "approximated" | "withdrawn" | "unread";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* What this target makes of each declaration SCHEMA.md's support matrix
|
|
43
|
+
* names. Hand-written and gated against that matrix rather than generated
|
|
44
|
+
* from it. It carries no version of its own: the package that ships it is the
|
|
45
|
+
* version.
|
|
46
|
+
*
|
|
47
|
+
* Keyed by the schema name an author writes — `valign`, `break`, `page.margin`
|
|
48
|
+
* — so a host, a `required` marking and a problem message share one
|
|
49
|
+
* vocabulary. Two matrix rows share the bare word `width`, so each is
|
|
50
|
+
* qualified by what carries it: `column.width` and `slot.width`.
|
|
51
|
+
*
|
|
52
|
+
* It covers the declarations every official target states one fate for. An
|
|
53
|
+
* absent key means the matrix states no single fate for that row — a row that
|
|
54
|
+
* summarises more than one declaration, or that describes a behaviour — never
|
|
55
|
+
* that this target has no answer.
|
|
56
|
+
*/
|
|
57
|
+
export const capabilities: {
|
|
58
|
+
readonly target: "docx";
|
|
59
|
+
readonly declarations: Readonly<Record<string, Fate>>;
|
|
60
|
+
};
|
package/lib/index.js
CHANGED
|
@@ -6,23 +6,19 @@
|
|
|
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 { collapse, hostMeta, hostOptions, splits, walk } from "quario";
|
|
15
13
|
import { bodyOf } from "./body.js";
|
|
16
|
-
import { DOCUMENT_NS, furnish } from "./furniture.js";
|
|
14
|
+
import { DOCUMENT_NS, furnish, NO_FURNITURE } from "./furniture.js";
|
|
17
15
|
import { STYLES } from "./stylepart.js";
|
|
18
16
|
import { DOCUMENT_PAGES, SECTION_PAGES } from "./text.js";
|
|
19
17
|
import { geometry, pageBox, twips } from "./page.js";
|
|
20
18
|
import { pack } from "./pack.js";
|
|
19
|
+
import { overrides, packageRels, relationships } from "./rels.js";
|
|
21
20
|
import { W, XML, esc } from "./xml.js";
|
|
22
21
|
|
|
23
|
-
const RELS = 'xmlns="http://schemas.openxmlformats.org/package/2006/relationships"';
|
|
24
|
-
const OFFICE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
25
|
-
const PACKAGE = "http://schemas.openxmlformats.org/package/2006/relationships";
|
|
26
22
|
const WML = "application/vnd.openxmlformats-officedocument.wordprocessingml";
|
|
27
23
|
|
|
28
24
|
// The distance from the paper's edge to a header or a footer, in twips: Word's
|
|
@@ -77,9 +73,9 @@ let core = (meta) =>
|
|
|
77
73
|
* refs: string, titlePg: boolean }} Section */
|
|
78
74
|
|
|
79
75
|
// The parts every document relates to, in relationship order. A furniture part
|
|
80
|
-
// numbers from after them, and
|
|
81
|
-
//
|
|
82
|
-
// one and not the other.
|
|
76
|
+
// numbers from after them, and `rels.js` maps the whole list to both the
|
|
77
|
+
// content types and the relationships, so a part cannot reach the package
|
|
78
|
+
// through one and not the other.
|
|
83
79
|
// A picture's bytes are typed by extension rather than by part name, the way
|
|
84
80
|
// `rels` and `xml` already are: one `Default` covers every image of that kind.
|
|
85
81
|
const IMAGE_TYPES = [
|
|
@@ -88,20 +84,10 @@ const IMAGE_TYPES = [
|
|
|
88
84
|
];
|
|
89
85
|
|
|
90
86
|
const FIXED = [
|
|
91
|
-
{
|
|
92
|
-
{
|
|
87
|
+
{ target: "settings.xml", kind: "settings", rId: "rId1" },
|
|
88
|
+
{ target: "styles.xml", kind: "styles", rId: "rId2" },
|
|
93
89
|
];
|
|
94
90
|
|
|
95
|
-
// The content type of one furniture part, and the relationship that reaches
|
|
96
|
-
// it. Both are keyed off the same `{ path, kind, rId }` the registry hands
|
|
97
|
-
// back, so a part cannot reach the package through one and not the other.
|
|
98
|
-
/** @type {(part: { path: string, kind: string }) => string} */
|
|
99
|
-
let override = ({ path, kind }) =>
|
|
100
|
-
`<Override PartName="/${path}" ContentType="${WML}.${kind}+xml"/>`;
|
|
101
|
-
/** @type {(part: { target: string, kind: string, rId: string }) => string} */
|
|
102
|
-
let related = ({ target, kind, rId }) =>
|
|
103
|
-
`<Relationship Id="${rId}" Type="${OFFICE}/${kind}" Target="${target}"/>`;
|
|
104
|
-
|
|
105
91
|
/**
|
|
106
92
|
* The parts of the package. The floor is what Word opens: the content types,
|
|
107
93
|
* the package relationships and `document.xml` — plus `settings.xml`, which is
|
|
@@ -118,7 +104,7 @@ let parts = (body, meta, furniture, rels) => ({
|
|
|
118
104
|
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' +
|
|
119
105
|
'<Default Extension="xml" ContentType="application/xml"/>' +
|
|
120
106
|
`<Override PartName="/word/document.xml" ContentType="${WML}.document.main+xml"/>` +
|
|
121
|
-
rels
|
|
107
|
+
overrides(rels) +
|
|
122
108
|
IMAGE_TYPES.filter(([ext]) => rels.some((r) => r.target.endsWith("." + ext)))
|
|
123
109
|
.map(([ext, type]) => `<Default Extension="${ext}" ContentType="image/${type}"/>`)
|
|
124
110
|
.join("") +
|
|
@@ -126,17 +112,9 @@ let parts = (body, meta, furniture, rels) => ({
|
|
|
126
112
|
? '<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>'
|
|
127
113
|
: "") +
|
|
128
114
|
"</Types>",
|
|
129
|
-
"_rels/.rels":
|
|
130
|
-
XML +
|
|
131
|
-
`<Relationships ${RELS}>` +
|
|
132
|
-
`<Relationship Id="rId1" Type="${OFFICE}/officeDocument" Target="word/document.xml"/>` +
|
|
133
|
-
(meta
|
|
134
|
-
? `<Relationship Id="rId2" Type="${PACKAGE}/metadata/core-properties" Target="docProps/core.xml"/>`
|
|
135
|
-
: "") +
|
|
136
|
-
"</Relationships>",
|
|
115
|
+
"_rels/.rels": packageRels(meta),
|
|
137
116
|
"word/document.xml": XML + `<w:document ${DOCUMENT_NS}><w:body>${body}</w:body></w:document>`,
|
|
138
|
-
"word/_rels/document.xml.rels":
|
|
139
|
-
XML + `<Relationships ${RELS}>` + rels.map(related).join("") + "</Relationships>",
|
|
117
|
+
"word/_rels/document.xml.rels": relationships(rels),
|
|
140
118
|
"word/settings.xml":
|
|
141
119
|
XML +
|
|
142
120
|
`<w:settings ${W}><w:compat>` +
|
|
@@ -153,8 +131,6 @@ let parts = (body, meta, furniture, rels) => ({
|
|
|
153
131
|
* bytes described in SCHEMA.md ("The DOCX target"). Options are taken and
|
|
154
132
|
* validated at the factory call.
|
|
155
133
|
*
|
|
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
134
|
* @import { DocxOptions } from './index.d.ts'
|
|
159
135
|
*
|
|
160
136
|
* @param {DocxOptions} [options] Host controls (see SCHEMA.md, "The DOCX target").
|
|
@@ -176,33 +152,45 @@ export function docx(options) {
|
|
|
176
152
|
// The sections are written after the walk, so the opening event is taken
|
|
177
153
|
// in its handler rather than peeked ahead of the driver: the margin a
|
|
178
154
|
// 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.
|
|
155
|
+
// second event is pulled, and the stream reaches `walk` unwrapped.
|
|
182
156
|
let geo = geometry(page);
|
|
183
157
|
// Every relationship the document carries, in one place: the parts it
|
|
184
158
|
// always has, then whatever the furniture and the body ask for. One
|
|
185
159
|
// allocator, because a page band and the body both draw pictures and two
|
|
186
160
|
// would hand out the same number twice.
|
|
187
|
-
/** @type {
|
|
161
|
+
/** @type {import('./rels.js').Rel[]} */
|
|
188
162
|
let rels = [...FIXED];
|
|
189
163
|
/** @type {Record<string, Uint8Array>} */
|
|
190
164
|
let media = {};
|
|
191
165
|
/** @type {(kind: string, target: string) => string} */
|
|
192
166
|
let relate = (kind, target) => {
|
|
193
167
|
let rId = "rId" + (rels.length + 1);
|
|
194
|
-
rels.push({
|
|
168
|
+
rels.push({ target, kind, rId });
|
|
195
169
|
return rId;
|
|
196
170
|
};
|
|
197
|
-
//
|
|
171
|
+
// The one registry, handed to whatever writes a part: the furniture needs
|
|
172
|
+
// the records and not only the ids, because a part's own relationships are
|
|
173
|
+
// the ones its markup refers to.
|
|
174
|
+
let registry = { rels, relate };
|
|
175
|
+
// One part per distinct picture, keyed on the bytes themselves: the
|
|
198
176
|
// 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.
|
|
177
|
+
// so the part stays identical and the furniture's dedup still collapses
|
|
178
|
+
// the sections. A `source` computing fresh bytes per call writes a part
|
|
179
|
+
// per call, which is wasteful and still correct.
|
|
204
180
|
/** @type {Map<Uint8Array, string>} */
|
|
205
181
|
let drawn = new Map();
|
|
182
|
+
// One relationship per distinct URL, for the same reason: a report linking
|
|
183
|
+
// one place from every row would otherwise relate to it once per row.
|
|
184
|
+
/** @type {Map<string, string>} */
|
|
185
|
+
let linked = new Map();
|
|
186
|
+
/** @type {(url: string) => string} */
|
|
187
|
+
let link = (url) => {
|
|
188
|
+
let seen = linked.get(url);
|
|
189
|
+
if (seen) return seen;
|
|
190
|
+
let rId = relate("hyperlink", url);
|
|
191
|
+
linked.set(url, rId);
|
|
192
|
+
return rId;
|
|
193
|
+
};
|
|
206
194
|
/** @type {(event: any) => string} */
|
|
207
195
|
let picture = (event) => {
|
|
208
196
|
let seen = drawn.get(event.bytes);
|
|
@@ -213,7 +201,7 @@ export function docx(options) {
|
|
|
213
201
|
drawn.set(event.bytes, rId);
|
|
214
202
|
return rId;
|
|
215
203
|
};
|
|
216
|
-
let furniture =
|
|
204
|
+
let furniture = NO_FURNITURE;
|
|
217
205
|
|
|
218
206
|
/** @type {(kind: any) => Section} */
|
|
219
207
|
let section = (kind) => ({
|
|
@@ -221,20 +209,21 @@ export function docx(options) {
|
|
|
221
209
|
...furniture.refsFor(kind.reset ? SECTION_PAGES : DOCUMENT_PAGES),
|
|
222
210
|
});
|
|
223
211
|
let body = bodyOf(section);
|
|
224
|
-
await walk(stream(data), {
|
|
212
|
+
await walk(collapse(splits(stream(data))), {
|
|
225
213
|
...body.handlers,
|
|
226
214
|
"report-start": (event) => {
|
|
227
215
|
geo = geometry(page, event);
|
|
228
216
|
// What a render carries that the events do not, settled once and handed
|
|
229
217
|
// to both writers: a fifth instance field must be read here and nowhere
|
|
230
|
-
// else (`
|
|
218
|
+
// else (`content.js`, `Ctx`).
|
|
231
219
|
let ctx = {
|
|
232
220
|
base: event.style || null,
|
|
233
221
|
intl: { locale: event.locale, currency: event.currency, timeZone: event.timeZone },
|
|
234
222
|
width: geo.width - 2 * geo.margin,
|
|
235
223
|
picture,
|
|
224
|
+
link,
|
|
236
225
|
};
|
|
237
|
-
furniture = furnish(event.page, event.marking,
|
|
226
|
+
furniture = furnish(event.page, event.marking ?? "", registry, ctx);
|
|
238
227
|
body.start(ctx, event);
|
|
239
228
|
},
|
|
240
229
|
});
|
|
@@ -254,3 +243,53 @@ export function docx(options) {
|
|
|
254
243
|
};
|
|
255
244
|
return { name: "docx", compile };
|
|
256
245
|
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* What this target makes of each declaration the support matrix names: one of
|
|
249
|
+
* the [fates](../../../CONTEXT.md#declaration-and-resolution) — `resolved`,
|
|
250
|
+
* `approximated`, `withdrawn` or `unread` — so a host can ask before it
|
|
251
|
+
* renders rather than reading the prose.
|
|
252
|
+
*
|
|
253
|
+
* **Hand-written, and gated rather than generated.** SCHEMA.md's matrix is the
|
|
254
|
+
* description a person maintains and this is the data a host reads; the repo's
|
|
255
|
+
* `test/capabilities.test.js` holds the two equal, which is the same bargain
|
|
256
|
+
* ADR 0014 makes for the band-role table. It carries no version of its own:
|
|
257
|
+
* the package that ships it is the version, and a second copy could only drift
|
|
258
|
+
* from it.
|
|
259
|
+
*
|
|
260
|
+
* Keyed by the **schema name** an author writes, so a host, a `required`
|
|
261
|
+
* marking and a problem message all index by one vocabulary. Two rows share
|
|
262
|
+
* the bare word `width`, so each is qualified by what carries it.
|
|
263
|
+
*
|
|
264
|
+
* It covers the declarations every official target states one fate for. A
|
|
265
|
+
* matrix row that summarises more than one declaration, or that describes a
|
|
266
|
+
* behaviour rather than a declaration, states no single fate and is absent
|
|
267
|
+
* here — an absent key means the spec does not answer, never that this target
|
|
268
|
+
* has no answer.
|
|
269
|
+
*/
|
|
270
|
+
export const capabilities = Object.freeze({
|
|
271
|
+
target: "docx",
|
|
272
|
+
declarations: Object.freeze({
|
|
273
|
+
"page.header": "resolved",
|
|
274
|
+
"page.footer": "resolved",
|
|
275
|
+
break: "resolved",
|
|
276
|
+
reset: "resolved",
|
|
277
|
+
"page.margin": "resolved",
|
|
278
|
+
"header.height": "withdrawn",
|
|
279
|
+
groups: "resolved",
|
|
280
|
+
label: "approximated",
|
|
281
|
+
collapsed: "resolved",
|
|
282
|
+
"column.width": "approximated",
|
|
283
|
+
"slot.width": "resolved",
|
|
284
|
+
span: "resolved",
|
|
285
|
+
uppercase: "resolved",
|
|
286
|
+
href: "resolved",
|
|
287
|
+
spaceBefore: "resolved",
|
|
288
|
+
spaceAfter: "resolved",
|
|
289
|
+
format: "resolved",
|
|
290
|
+
family: "resolved",
|
|
291
|
+
valign: "resolved",
|
|
292
|
+
runs: "resolved",
|
|
293
|
+
alt: "resolved",
|
|
294
|
+
}),
|
|
295
|
+
});
|
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
|
*/
|