@quario/docx 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/lib/body.js +57 -24
- package/lib/content.js +7 -6
- package/lib/furniture.js +60 -22
- package/lib/index.d.ts +24 -0
- package/lib/index.js +83 -36
- package/lib/rels.js +66 -0
- package/lib/stylepart.js +6 -4
- package/lib/table.js +8 -7
- package/lib/text.js +65 -13
- package/package.json +7 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @quario/docx
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- **A `capabilities` descriptor.** The target exports what it makes of each declaration the spec's support matrix names — `resolved`, `approximated`, `withdrawn` or `unread` — beside its factory, so a host can ask before it renders rather than reading the prose. It is keyed by the schema name an author writes (`valign`, `break`, `page.margin`, and `column.width` / `slot.width` where one word carries two declarations), hand-written and gated against that matrix. It carries no version of its own: the package that ships it is the version.
|
|
8
|
+
- **A run that carries an `href` is a Word hyperlink.** A URL becomes a relationship of the part the run lands in, marked as leaving the package, and one relationship serves every run pointing at the same place. An `href` beginning `#` becomes a `w:anchor`, and the first instance of the group carrying that `label` writes the bookmark it names, beside the heading that item already takes. A run whose text is empty writes no link and relates to nothing. The support matrix recorded this target as withdrawing `href`, and it now resolves it.
|
|
9
|
+
|
|
10
|
+
Two things a page band's own part had wrong are fixed with it. A header or footer now carries its **own relationships part**, so a picture or a link inside a band resolves in the part that refers to it rather than only in `document.xml`'s. And `[Content_Types].xml` no longer writes an `Override` for a relationship that names no WordprocessingML part. That entry typed a picture's bytes as a document part, which said nothing true about them.
|
|
11
|
+
|
|
12
|
+
- **`negative` and `zero` reach the runs.** The target presents through the engine's `format()`, so a cell declaring either reads the same in Word as on the page.
|
|
13
|
+
- **A collapsed group does not draw its content.** An instance whose group declares `collapsed` renders as its header and footer, with the rows between them left out — a page has nowhere to put a row a reader could open. Aggregates are unchanged, so a footer still totals them.
|
|
14
|
+
- **The report header scales with the report default.** A report-header run is written at 1.4× the report default's size rather than a fixed 14, rounded to Word's half-points. A document declaring no size keeps its bytes, because 1.4× the base 10 is the same 14. A document declaring `"size": 20` now renders its report header at 28 where it rendered at 14, smaller than its own body.
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- Updated dependencies
|
|
19
|
+
- quario@0.11.0
|
|
20
|
+
|
|
3
21
|
## 0.2.0
|
|
4
22
|
|
|
5
23
|
### Minor Changes
|
package/lib/body.js
CHANGED
|
@@ -34,12 +34,11 @@
|
|
|
34
34
|
* written per run), the report default, the band-role default, then the node's
|
|
35
35
|
* own. A `false` in an inner layer is a declaration and wins.
|
|
36
36
|
*/
|
|
37
|
-
import { headingAt } from "./stylepart.js";
|
|
37
|
+
import { BASE, headingAt } from "./stylepart.js";
|
|
38
38
|
import { paraProps, under } from "./style.js";
|
|
39
39
|
import { grid, record, tbl } from "./table.js";
|
|
40
40
|
import { content, splitBlock } from "./content.js";
|
|
41
|
-
import {
|
|
42
|
-
import { paragraph } from "./text.js";
|
|
41
|
+
import { bookmark, paragraph } from "./text.js";
|
|
43
42
|
|
|
44
43
|
// The page break a paragraph wears, spelled once: `push` folds it into a
|
|
45
44
|
// paragraph's own properties, and `carrier` gives it one where no paragraph is
|
|
@@ -52,10 +51,21 @@ const BREAK = "<w:pageBreakBefore/>";
|
|
|
52
51
|
// agreement; it is not shared code, and that is the ADR's point.
|
|
53
52
|
/** @type {Record<string, any>} */
|
|
54
53
|
const ROLES = {
|
|
55
|
-
"report-header": { bold: true,
|
|
54
|
+
"report-header": { bold: true, scale: 1.4 },
|
|
56
55
|
"group-header": { bold: true },
|
|
57
56
|
};
|
|
58
57
|
|
|
58
|
+
// A role's default as a style block: `scale` is a factor over the report
|
|
59
|
+
// default's size, or over the base the style part writes when the report
|
|
60
|
+
// declared none, so a larger document keeps its headline larger.
|
|
61
|
+
/** @type {(role: string, base: any) => any} */
|
|
62
|
+
const roleStyle = (role, base) => {
|
|
63
|
+
let { scale, ...rest } = ROLES[role];
|
|
64
|
+
// The engine admits only a positive number, so absent is the one other case.
|
|
65
|
+
let size = base?.size || BASE;
|
|
66
|
+
return scale ? { ...rest, size: Math.round(scale * size * 100) / 100 } : rest;
|
|
67
|
+
};
|
|
68
|
+
|
|
59
69
|
// The cell padding this target supplies on a side the author did not name: 6 pt
|
|
60
70
|
// across, 2 pt down, the layout's own numbers. A named `0` beats it, which is
|
|
61
71
|
// what makes the declaration a declaration (`docs/adr/0040`).
|
|
@@ -91,16 +101,26 @@ export let bodyOf = (section) => {
|
|
|
91
101
|
// paragraph carries one `spacing` element and the author may have declared
|
|
92
102
|
// it -- so the gap layers under their own `spaceBefore`, which then wins.
|
|
93
103
|
let owed = { broken: false, gapped: false };
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
104
|
+
// What a group instance's *first* header item is still owed, set when the
|
|
105
|
+
// instance opens and taken by whichever item claims it: the heading its depth
|
|
106
|
+
// earns, and the bookmark its `label` is the destination of. One record rather
|
|
107
|
+
// than a depth, a flag and a name: they only ever move together.
|
|
108
|
+
/** @type {{ props: string, mark: { id: number, name: string } | null }} */
|
|
109
|
+
let owes = { props: "", mark: null };
|
|
110
|
+
// The labels a destination has already been written for, and how many there
|
|
111
|
+
// are. A label two instances share is one destination and the first wins
|
|
112
|
+
// (SCHEMA.md, "Style declarations"), and Word's bookmark id is an integer the
|
|
113
|
+
// document uses once.
|
|
114
|
+
/** @type {Set<string>} */
|
|
115
|
+
let labelled = new Set();
|
|
98
116
|
/** What a render carries that the events do not: the report default, the
|
|
99
117
|
* instance's `format` configuration, the text column and the picture
|
|
100
118
|
* registry. Settled once, on the opening event, and read-only after —
|
|
101
119
|
* `content.js`'s `Ctx`, so the two writers describe a render one way.
|
|
120
|
+
* Nothing reads it before then, which is why the picture registry the real
|
|
121
|
+
* one carries is absent here rather than stubbed.
|
|
102
122
|
* @type {import("./content.js").Ctx} */
|
|
103
|
-
let ctx =
|
|
123
|
+
let ctx = /** @type {any} */ ({ base: null, intl: null, width: 0 });
|
|
104
124
|
/** The table being filled, if any: its grid, and the rows written so far. */
|
|
105
125
|
/** @type {{ widths: number[], rows: string } | null} */
|
|
106
126
|
let table = null;
|
|
@@ -160,7 +180,7 @@ export let bodyOf = (section) => {
|
|
|
160
180
|
/** @type {(event: any) => any} */
|
|
161
181
|
let styleOf = (event) =>
|
|
162
182
|
under(
|
|
163
|
-
under(ctx.base, Object.hasOwn(ROLES, event.role) ?
|
|
183
|
+
under(ctx.base, Object.hasOwn(ROLES, event.role) ? roleStyle(event.role, ctx.base) : null),
|
|
164
184
|
event.style,
|
|
165
185
|
);
|
|
166
186
|
|
|
@@ -191,7 +211,7 @@ export let bodyOf = (section) => {
|
|
|
191
211
|
* @type {(event: any) => void}
|
|
192
212
|
*/
|
|
193
213
|
let group = (event) => {
|
|
194
|
-
|
|
214
|
+
owes = { props: `<w:pStyle w:val="${headingAt(event.depth)}"/>`, mark: marking(event.label) };
|
|
195
215
|
// A structural half-line before every instance, dropped where the document
|
|
196
216
|
// has not started: groups read as blocks without authored margins.
|
|
197
217
|
// oxlint-disable-next-line no-unused-expressions
|
|
@@ -237,17 +257,27 @@ export let bodyOf = (section) => {
|
|
|
237
257
|
if (frame) closeInstance(frame);
|
|
238
258
|
};
|
|
239
259
|
|
|
260
|
+
/** The destination an instance's label is, or none where it declared none or
|
|
261
|
+
* an earlier instance already carries it.
|
|
262
|
+
* @type {(label: any) => { id: number, name: string } | null} */
|
|
263
|
+
let marking = (label) => {
|
|
264
|
+
if (!label || labelled.has(label)) return null;
|
|
265
|
+
labelled.add(label);
|
|
266
|
+
return { id: labelled.size, name: label };
|
|
267
|
+
};
|
|
268
|
+
|
|
240
269
|
/**
|
|
241
|
-
*
|
|
242
|
-
* owed, on a group instance's *first* header item, and nothing anywhere
|
|
270
|
+
* What an item's role earns from its instance: the heading and the bookmark
|
|
271
|
+
* it is owed, on a group instance's *first* header item, and nothing anywhere
|
|
272
|
+
* else.
|
|
243
273
|
*
|
|
244
|
-
* @type {(role: string) => string}
|
|
274
|
+
* @type {(role: string) => { props: string, mark: { id: number, name: string } | null }}
|
|
245
275
|
*/
|
|
246
|
-
let
|
|
247
|
-
if (role !== "group-header") return "";
|
|
248
|
-
let
|
|
249
|
-
|
|
250
|
-
return
|
|
276
|
+
let claim = (role) => {
|
|
277
|
+
if (role !== "group-header") return { props: "", mark: null };
|
|
278
|
+
let owed = owes;
|
|
279
|
+
owes = { props: "", mark: null };
|
|
280
|
+
return owed;
|
|
251
281
|
};
|
|
252
282
|
|
|
253
283
|
/** How wide the body is where it is being written: the text column, divided
|
|
@@ -261,7 +291,8 @@ export let bodyOf = (section) => {
|
|
|
261
291
|
/** @type {(event: any) => void} */
|
|
262
292
|
let itemOf = (event) => {
|
|
263
293
|
let style = styleOf(event);
|
|
264
|
-
|
|
294
|
+
let { props, mark } = claim(event.role);
|
|
295
|
+
push(props, style, bookmark(mark, content(event, style, columnWidth(), ctx)));
|
|
265
296
|
};
|
|
266
297
|
|
|
267
298
|
/** @type {(event: any) => void} */
|
|
@@ -269,10 +300,12 @@ export let bodyOf = (section) => {
|
|
|
269
300
|
block(splitBlock(event, columnWidth(), styleOf(event), ctx));
|
|
270
301
|
};
|
|
271
302
|
|
|
272
|
-
/** What a table's cells are written with. `intl`
|
|
273
|
-
* event, so
|
|
274
|
-
*
|
|
275
|
-
|
|
303
|
+
/** What a table's cells are written with. `intl` and the link registry are
|
|
304
|
+
* settled on the opening event, so they are read here rather than captured
|
|
305
|
+
* at compile.
|
|
306
|
+
* @type {() => { pad: Record<string, number>, intl: any,
|
|
307
|
+
* link: (url: string) => string }} */
|
|
308
|
+
let ruled = () => ({ pad: PAD, intl: ctx.intl, link: ctx.link });
|
|
276
309
|
|
|
277
310
|
/** @type {(event: any) => void} */
|
|
278
311
|
let tableStart = (event) => {
|
package/lib/content.js
CHANGED
|
@@ -12,13 +12,14 @@ import { looking, runs } from "./text.js";
|
|
|
12
12
|
/**
|
|
13
13
|
* What a render carries that the events do not: the report default every band
|
|
14
14
|
* item wears under its own, the instance's `format` configuration, the text
|
|
15
|
-
* column a picture is capped at, the
|
|
16
|
-
* in, and -- for a page band only -- which field gives
|
|
17
|
-
* sequence its section numbers. Both writers hand one of
|
|
18
|
-
* describe a render one way.
|
|
15
|
+
* column a picture is capped at, the registries a picture's bytes and a link's
|
|
16
|
+
* URL become relationships in, and -- for a page band only -- which field gives
|
|
17
|
+
* the length of the sequence its section numbers. Both writers hand one of
|
|
18
|
+
* these down, so they describe a render one way.
|
|
19
19
|
*
|
|
20
20
|
* @typedef {{ base: any, intl: any, width: number,
|
|
21
|
-
* picture: (event: any) => string,
|
|
21
|
+
* picture: (event: any) => string, link: (url: string) => string,
|
|
22
|
+
* sequence?: string }} Ctx
|
|
22
23
|
*/
|
|
23
24
|
|
|
24
25
|
/**
|
|
@@ -29,7 +30,7 @@ import { looking, runs } from "./text.js";
|
|
|
29
30
|
* @type {(event: any, style: any, column: number, ctx: Ctx) => string}
|
|
30
31
|
*/
|
|
31
32
|
export let content = (event, style, column, ctx) => {
|
|
32
|
-
if (event.type !== "image") return runs(event.tokens, looking(style, ctx
|
|
33
|
+
if (event.type !== "image") return runs(event.tokens, looking(style, ctx));
|
|
33
34
|
let rId = ctx.picture(event);
|
|
34
35
|
return drawing(event, idOf(rId), rId, describe(event), sizeOf(event, column));
|
|
35
36
|
};
|
package/lib/furniture.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { splits } from "quario";
|
|
23
23
|
import { content, splitBlock } from "./content.js";
|
|
24
24
|
import { paraProps, under } from "./style.js";
|
|
25
|
+
import { relationships } from "./rels.js";
|
|
25
26
|
import { paragraph, run } from "./text.js";
|
|
26
27
|
import { R, W, XML } from "./xml.js";
|
|
27
28
|
|
|
@@ -46,7 +47,7 @@ const MARKING_LOOK = '<w:color w:val="808080"/><w:sz w:val="14"/>';
|
|
|
46
47
|
*/
|
|
47
48
|
let banded = (events, ctx) => {
|
|
48
49
|
let out = "";
|
|
49
|
-
for (let e of splits(events)) out += STEPS[e.type]
|
|
50
|
+
for (let e of splits(events)) out += STEPS[e.type](e, ctx);
|
|
50
51
|
return out;
|
|
51
52
|
};
|
|
52
53
|
|
|
@@ -89,14 +90,20 @@ let reading = (closure, at, lead, ctx) => lead + (closure ? banded(closure(at),
|
|
|
89
90
|
* `reset: "page"` all number themselves the same way, so a document of two
|
|
90
91
|
* hundred invoices carries one footer part.
|
|
91
92
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
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.
|
|
98
105
|
*/
|
|
99
|
-
export let furnish = (page, marking,
|
|
106
|
+
export let furnish = (page, marking, registry, ctx) => {
|
|
100
107
|
/** @type {Record<string, string>} */
|
|
101
108
|
let parts = {};
|
|
102
109
|
/** @type {Map<string, string>} */
|
|
@@ -104,17 +111,34 @@ export let furnish = (page, marking, relate = () => "", ctx = EMPTY_CTX) => {
|
|
|
104
111
|
/** @type {Record<string, number>} */
|
|
105
112
|
let counts = { header: 0, footer: 0 };
|
|
106
113
|
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
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) => {
|
|
110
130
|
let tag = kind === "header" ? "hdr" : "ftr";
|
|
111
|
-
let xml = XML + `<w:${tag} ${W}>` +
|
|
131
|
+
let xml = XML + `<w:${tag} ${W}>` + reading.xml + `</w:${tag}>`;
|
|
112
132
|
let seen = byXml.get(xml);
|
|
113
133
|
if (seen) return seen;
|
|
114
134
|
let target = kind + ++counts[kind] + ".xml";
|
|
115
|
-
let rId = relate(kind, target);
|
|
135
|
+
let rId = registry.relate(kind, target);
|
|
116
136
|
byXml.set(xml, rId);
|
|
117
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
|
+
);
|
|
118
142
|
return rId;
|
|
119
143
|
};
|
|
120
144
|
|
|
@@ -123,11 +147,11 @@ export let furnish = (page, marking, relate = () => "", ctx = EMPTY_CTX) => {
|
|
|
123
147
|
// no `first` reference shows *nothing* on page one once the other band has
|
|
124
148
|
// turned it on. Both therefore name a first-page part whenever either needs
|
|
125
149
|
// one; the steady one names the part it already wrote.
|
|
126
|
-
/** @type {(kind: string, pair:
|
|
150
|
+
/** @type {(kind: string, pair: any[], titlePg: boolean) => string} */
|
|
127
151
|
let references = (kind, [first, later], titlePg) => {
|
|
128
|
-
/** @type {(type: string,
|
|
129
|
-
let ref = (type,
|
|
130
|
-
`<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)}"/>`;
|
|
131
155
|
return (titlePg ? ref("first", first) : "") + ref("default", later);
|
|
132
156
|
};
|
|
133
157
|
|
|
@@ -140,7 +164,12 @@ export let furnish = (page, marking, relate = () => "", ctx = EMPTY_CTX) => {
|
|
|
140
164
|
* @type {(sequence: string) => { refs: string, titlePg: boolean }}
|
|
141
165
|
*/
|
|
142
166
|
let refsFor = (sequence) => {
|
|
143
|
-
let band = {
|
|
167
|
+
let band = {
|
|
168
|
+
...ctx,
|
|
169
|
+
sequence,
|
|
170
|
+
picture: recording(ctx.picture),
|
|
171
|
+
link: recording(ctx.link),
|
|
172
|
+
};
|
|
144
173
|
let lead = marking ? paragraph("", run(marking, MARKING_LOOK)) : "";
|
|
145
174
|
let bands = [
|
|
146
175
|
{ kind: "header", closure: page?.header, lead: "" },
|
|
@@ -149,9 +178,12 @@ export let furnish = (page, marking, relate = () => "", ctx = EMPTY_CTX) => {
|
|
|
149
178
|
.filter((band) => band.closure || band.lead)
|
|
150
179
|
.map(({ kind, closure, lead: own }) => ({
|
|
151
180
|
kind,
|
|
152
|
-
pair: PROBE.map((at) =>
|
|
181
|
+
pair: PROBE.map((at) => {
|
|
182
|
+
used = new Set();
|
|
183
|
+
return { xml: reading(closure, at, own, band), used };
|
|
184
|
+
}),
|
|
153
185
|
}));
|
|
154
|
-
let titlePg = bands.some(({ pair }) => pair[0] !== pair[1]);
|
|
186
|
+
let titlePg = bands.some(({ pair }) => pair[0].xml !== pair[1].xml);
|
|
155
187
|
return {
|
|
156
188
|
refs: bands.map(({ kind, pair }) => references(kind, pair, titlePg)).join(""),
|
|
157
189
|
titlePg,
|
|
@@ -161,8 +193,14 @@ export let furnish = (page, marking, relate = () => "", ctx = EMPTY_CTX) => {
|
|
|
161
193
|
return { parts, refsFor };
|
|
162
194
|
};
|
|
163
195
|
|
|
164
|
-
/** A
|
|
165
|
-
|
|
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
|
+
};
|
|
166
204
|
|
|
167
205
|
/** The namespaces a document referencing furniture declares. */
|
|
168
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
|
@@ -9,18 +9,16 @@
|
|
|
9
9
|
* stream, and the parts a package is made of. The writers live beside it:
|
|
10
10
|
* `body.js`, `furniture.js`, `styles.js` and `text.js`.
|
|
11
11
|
*/
|
|
12
|
-
import { hostMeta, hostOptions, splits, walk } from "quario";
|
|
12
|
+
import { collapse, hostMeta, hostOptions, splits, walk } from "quario";
|
|
13
13
|
import { bodyOf } from "./body.js";
|
|
14
|
-
import { DOCUMENT_NS, furnish } from "./furniture.js";
|
|
14
|
+
import { DOCUMENT_NS, furnish, NO_FURNITURE } from "./furniture.js";
|
|
15
15
|
import { STYLES } from "./stylepart.js";
|
|
16
16
|
import { DOCUMENT_PAGES, SECTION_PAGES } from "./text.js";
|
|
17
17
|
import { geometry, pageBox, twips } from "./page.js";
|
|
18
18
|
import { pack } from "./pack.js";
|
|
19
|
+
import { overrides, packageRels, relationships } from "./rels.js";
|
|
19
20
|
import { W, XML, esc } from "./xml.js";
|
|
20
21
|
|
|
21
|
-
const RELS = 'xmlns="http://schemas.openxmlformats.org/package/2006/relationships"';
|
|
22
|
-
const OFFICE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
23
|
-
const PACKAGE = "http://schemas.openxmlformats.org/package/2006/relationships";
|
|
24
22
|
const WML = "application/vnd.openxmlformats-officedocument.wordprocessingml";
|
|
25
23
|
|
|
26
24
|
// The distance from the paper's edge to a header or a footer, in twips: Word's
|
|
@@ -75,9 +73,9 @@ let core = (meta) =>
|
|
|
75
73
|
* refs: string, titlePg: boolean }} Section */
|
|
76
74
|
|
|
77
75
|
// The parts every document relates to, in relationship order. A furniture part
|
|
78
|
-
// numbers from after them, and
|
|
79
|
-
//
|
|
80
|
-
// 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.
|
|
81
79
|
// A picture's bytes are typed by extension rather than by part name, the way
|
|
82
80
|
// `rels` and `xml` already are: one `Default` covers every image of that kind.
|
|
83
81
|
const IMAGE_TYPES = [
|
|
@@ -86,20 +84,10 @@ const IMAGE_TYPES = [
|
|
|
86
84
|
];
|
|
87
85
|
|
|
88
86
|
const FIXED = [
|
|
89
|
-
{
|
|
90
|
-
{
|
|
87
|
+
{ target: "settings.xml", kind: "settings", rId: "rId1" },
|
|
88
|
+
{ target: "styles.xml", kind: "styles", rId: "rId2" },
|
|
91
89
|
];
|
|
92
90
|
|
|
93
|
-
// The content type of one furniture part, and the relationship that reaches
|
|
94
|
-
// it. Both are keyed off the same `{ path, kind, rId }` the registry hands
|
|
95
|
-
// back, so a part cannot reach the package through one and not the other.
|
|
96
|
-
/** @type {(part: { path: string, kind: string }) => string} */
|
|
97
|
-
let override = ({ path, kind }) =>
|
|
98
|
-
`<Override PartName="/${path}" ContentType="${WML}.${kind}+xml"/>`;
|
|
99
|
-
/** @type {(part: { target: string, kind: string, rId: string }) => string} */
|
|
100
|
-
let related = ({ target, kind, rId }) =>
|
|
101
|
-
`<Relationship Id="${rId}" Type="${OFFICE}/${kind}" Target="${target}"/>`;
|
|
102
|
-
|
|
103
91
|
/**
|
|
104
92
|
* The parts of the package. The floor is what Word opens: the content types,
|
|
105
93
|
* the package relationships and `document.xml` — plus `settings.xml`, which is
|
|
@@ -116,7 +104,7 @@ let parts = (body, meta, furniture, rels) => ({
|
|
|
116
104
|
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>' +
|
|
117
105
|
'<Default Extension="xml" ContentType="application/xml"/>' +
|
|
118
106
|
`<Override PartName="/word/document.xml" ContentType="${WML}.document.main+xml"/>` +
|
|
119
|
-
rels
|
|
107
|
+
overrides(rels) +
|
|
120
108
|
IMAGE_TYPES.filter(([ext]) => rels.some((r) => r.target.endsWith("." + ext)))
|
|
121
109
|
.map(([ext, type]) => `<Default Extension="${ext}" ContentType="image/${type}"/>`)
|
|
122
110
|
.join("") +
|
|
@@ -124,17 +112,9 @@ let parts = (body, meta, furniture, rels) => ({
|
|
|
124
112
|
? '<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>'
|
|
125
113
|
: "") +
|
|
126
114
|
"</Types>",
|
|
127
|
-
"_rels/.rels":
|
|
128
|
-
XML +
|
|
129
|
-
`<Relationships ${RELS}>` +
|
|
130
|
-
`<Relationship Id="rId1" Type="${OFFICE}/officeDocument" Target="word/document.xml"/>` +
|
|
131
|
-
(meta
|
|
132
|
-
? `<Relationship Id="rId2" Type="${PACKAGE}/metadata/core-properties" Target="docProps/core.xml"/>`
|
|
133
|
-
: "") +
|
|
134
|
-
"</Relationships>",
|
|
115
|
+
"_rels/.rels": packageRels(meta),
|
|
135
116
|
"word/document.xml": XML + `<w:document ${DOCUMENT_NS}><w:body>${body}</w:body></w:document>`,
|
|
136
|
-
"word/_rels/document.xml.rels":
|
|
137
|
-
XML + `<Relationships ${RELS}>` + rels.map(related).join("") + "</Relationships>",
|
|
117
|
+
"word/_rels/document.xml.rels": relationships(rels),
|
|
138
118
|
"word/settings.xml":
|
|
139
119
|
XML +
|
|
140
120
|
`<w:settings ${W}><w:compat>` +
|
|
@@ -178,16 +158,20 @@ export function docx(options) {
|
|
|
178
158
|
// always has, then whatever the furniture and the body ask for. One
|
|
179
159
|
// allocator, because a page band and the body both draw pictures and two
|
|
180
160
|
// would hand out the same number twice.
|
|
181
|
-
/** @type {
|
|
161
|
+
/** @type {import('./rels.js').Rel[]} */
|
|
182
162
|
let rels = [...FIXED];
|
|
183
163
|
/** @type {Record<string, Uint8Array>} */
|
|
184
164
|
let media = {};
|
|
185
165
|
/** @type {(kind: string, target: string) => string} */
|
|
186
166
|
let relate = (kind, target) => {
|
|
187
167
|
let rId = "rId" + (rels.length + 1);
|
|
188
|
-
rels.push({
|
|
168
|
+
rels.push({ target, kind, rId });
|
|
189
169
|
return rId;
|
|
190
170
|
};
|
|
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 };
|
|
191
175
|
// One part per distinct picture, keyed on the bytes themselves: the
|
|
192
176
|
// page-band probe reads each band twice and every section reads it again,
|
|
193
177
|
// so the part stays identical and the furniture's dedup still collapses
|
|
@@ -195,6 +179,18 @@ export function docx(options) {
|
|
|
195
179
|
// per call, which is wasteful and still correct.
|
|
196
180
|
/** @type {Map<Uint8Array, string>} */
|
|
197
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
|
+
};
|
|
198
194
|
/** @type {(event: any) => string} */
|
|
199
195
|
let picture = (event) => {
|
|
200
196
|
let seen = drawn.get(event.bytes);
|
|
@@ -205,7 +201,7 @@ export function docx(options) {
|
|
|
205
201
|
drawn.set(event.bytes, rId);
|
|
206
202
|
return rId;
|
|
207
203
|
};
|
|
208
|
-
let furniture =
|
|
204
|
+
let furniture = NO_FURNITURE;
|
|
209
205
|
|
|
210
206
|
/** @type {(kind: any) => Section} */
|
|
211
207
|
let section = (kind) => ({
|
|
@@ -213,7 +209,7 @@ export function docx(options) {
|
|
|
213
209
|
...furniture.refsFor(kind.reset ? SECTION_PAGES : DOCUMENT_PAGES),
|
|
214
210
|
});
|
|
215
211
|
let body = bodyOf(section);
|
|
216
|
-
await walk(splits(stream(data)), {
|
|
212
|
+
await walk(collapse(splits(stream(data))), {
|
|
217
213
|
...body.handlers,
|
|
218
214
|
"report-start": (event) => {
|
|
219
215
|
geo = geometry(page, event);
|
|
@@ -225,8 +221,9 @@ export function docx(options) {
|
|
|
225
221
|
intl: { locale: event.locale, currency: event.currency, timeZone: event.timeZone },
|
|
226
222
|
width: geo.width - 2 * geo.margin,
|
|
227
223
|
picture,
|
|
224
|
+
link,
|
|
228
225
|
};
|
|
229
|
-
furniture = furnish(event.page, event.marking,
|
|
226
|
+
furniture = furnish(event.page, event.marking ?? "", registry, ctx);
|
|
230
227
|
body.start(ctx, event);
|
|
231
228
|
},
|
|
232
229
|
});
|
|
@@ -246,3 +243,53 @@ export function docx(options) {
|
|
|
246
243
|
};
|
|
247
244
|
return { name: "docx", compile };
|
|
248
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/rels.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The package's relationships: what a part may reach, and the id it refers to
|
|
3
|
+
* it by. A relationship is per part, so `document.xml` has its own and so does
|
|
4
|
+
* every header and footer — a picture a page band draws is reachable from the
|
|
5
|
+
* band's part or from nowhere.
|
|
6
|
+
*
|
|
7
|
+
* Two things a relationship says beyond its target. A **hyperlink** names a URL
|
|
8
|
+
* rather than a part, so it says `TargetMode` and the package types no content
|
|
9
|
+
* for it. Every other kind reaches a part this target packs, and of those the
|
|
10
|
+
* WordprocessingML ones each need a content type of their own — a picture's
|
|
11
|
+
* bytes are typed by their extension instead, one entry however many pictures
|
|
12
|
+
* there are.
|
|
13
|
+
*/
|
|
14
|
+
import { XML, esc } from "./xml.js";
|
|
15
|
+
|
|
16
|
+
const RELS = 'xmlns="http://schemas.openxmlformats.org/package/2006/relationships"';
|
|
17
|
+
const OFFICE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
18
|
+
const PACKAGE = "http://schemas.openxmlformats.org/package/2006/relationships";
|
|
19
|
+
const WML = "application/vnd.openxmlformats-officedocument.wordprocessingml";
|
|
20
|
+
|
|
21
|
+
/** The one kind whose target leaves the package. */
|
|
22
|
+
const EXTERNAL = "hyperlink";
|
|
23
|
+
|
|
24
|
+
/** The kinds whose target is a WordprocessingML part of this package. */
|
|
25
|
+
const TYPED = new Set(["settings", "styles", "header", "footer"]);
|
|
26
|
+
|
|
27
|
+
/** @typedef {{ target: string, kind: string, rId: string }} Rel */
|
|
28
|
+
|
|
29
|
+
/** One relationship. The target is a part name this target wrote or a URL the
|
|
30
|
+
* engine admitted, and the escape is the same one every other string goes
|
|
31
|
+
* through on the way out.
|
|
32
|
+
* @type {(rel: Rel) => string} */
|
|
33
|
+
let related = ({ target, kind, rId }) =>
|
|
34
|
+
`<Relationship Id="${rId}" Type="${OFFICE}/${kind}" Target="${esc(target)}"` +
|
|
35
|
+
(kind === EXTERNAL ? ' TargetMode="External"' : "") +
|
|
36
|
+
"/>";
|
|
37
|
+
|
|
38
|
+
/** One relationships part, whether the package's or one part's own.
|
|
39
|
+
* @type {(rels: Rel[]) => string} */
|
|
40
|
+
export let relationships = (rels) =>
|
|
41
|
+
XML + `<Relationships ${RELS}>` + rels.map(related).join("") + "</Relationships>";
|
|
42
|
+
|
|
43
|
+
/** The content types the relationships of one package earn: one per
|
|
44
|
+
* WordprocessingML part they reach.
|
|
45
|
+
* @type {(rels: Rel[]) => string} */
|
|
46
|
+
export let overrides = (rels) =>
|
|
47
|
+
rels
|
|
48
|
+
.filter(({ kind }) => TYPED.has(kind))
|
|
49
|
+
.map(
|
|
50
|
+
({ target, kind }) =>
|
|
51
|
+
`<Override PartName="/word/${target}" ContentType="${WML}.${kind}+xml"/>`,
|
|
52
|
+
)
|
|
53
|
+
.join("");
|
|
54
|
+
|
|
55
|
+
/** The package's own relationships: `document.xml`, and the metadata part a
|
|
56
|
+
* host's `meta` wrote. Its two entries name types from two vocabularies, which
|
|
57
|
+
* is why they are spelled here rather than through `related`.
|
|
58
|
+
* @type {(meta: any) => string} */
|
|
59
|
+
export let packageRels = (meta) =>
|
|
60
|
+
XML +
|
|
61
|
+
`<Relationships ${RELS}>` +
|
|
62
|
+
`<Relationship Id="rId1" Type="${OFFICE}/officeDocument" Target="word/document.xml"/>` +
|
|
63
|
+
(meta
|
|
64
|
+
? `<Relationship Id="rId2" Type="${PACKAGE}/metadata/core-properties" Target="docProps/core.xml"/>`
|
|
65
|
+
: "") +
|
|
66
|
+
"</Relationships>";
|
package/lib/stylepart.js
CHANGED
|
@@ -24,11 +24,13 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { W, XML } from "./xml.js";
|
|
26
26
|
|
|
27
|
-
// The base. Arial is the name the sans generic maps to;
|
|
28
|
-
// half-points; 1.4 line spacing is `line` in 240ths
|
|
29
|
-
// larger run still fits.
|
|
27
|
+
// The base. Arial is the name the sans generic maps to; `BASE` is the type
|
|
28
|
+
// size in points, `sz` in half-points; 1.4 line spacing is `line` in 240ths
|
|
29
|
+
// of a line, `auto` so a larger run still fits. The layout carries the same
|
|
30
|
+
// 10 (docs/adr/0014).
|
|
30
31
|
const FAMILY = "Arial";
|
|
31
|
-
const
|
|
32
|
+
export const BASE = 10;
|
|
33
|
+
const SIZE = BASE * 2;
|
|
32
34
|
const LINE = 336;
|
|
33
35
|
|
|
34
36
|
/** Word caps the outline at nine levels, and so does the deepest style here. */
|
package/lib/table.js
CHANGED
|
@@ -63,7 +63,7 @@ let tc = (cell, width, body, pad) =>
|
|
|
63
63
|
(cell.span > 1 ? `<w:gridSpan w:val="${cell.span}"/>` : "") +
|
|
64
64
|
cellProps(cell.style, pad) +
|
|
65
65
|
"</w:tcPr>" +
|
|
66
|
-
|
|
66
|
+
body +
|
|
67
67
|
"</w:tc>";
|
|
68
68
|
|
|
69
69
|
/**
|
|
@@ -125,7 +125,7 @@ export let splitting = (slots, width, style) => {
|
|
|
125
125
|
return {
|
|
126
126
|
style,
|
|
127
127
|
slot: (own, content) => {
|
|
128
|
-
let column = widths[at++]
|
|
128
|
+
let column = widths[at++];
|
|
129
129
|
// `boxed: false`: the slot's box is the cell's, so the paragraph inside
|
|
130
130
|
// it must not draw a second one.
|
|
131
131
|
cells += tc(
|
|
@@ -142,12 +142,12 @@ export let splitting = (slots, width, style) => {
|
|
|
142
142
|
/**
|
|
143
143
|
* How wide one cell is, in twips: its own grid column, plus every column it
|
|
144
144
|
* [spans](../../../SCHEMA.md#span). `record` walks the cells in order and keeps
|
|
145
|
-
* the column it has reached, since a span consumes more than one.
|
|
145
|
+
* the column it has reached, since a span consumes more than one. It is also
|
|
146
|
+
* where a span becomes a count of at least one, so this one takes it as read.
|
|
146
147
|
*
|
|
147
148
|
* @type {(widths: number[], at: number, span: number) => number}
|
|
148
149
|
*/
|
|
149
|
-
let spanned = (widths, at, span) =>
|
|
150
|
-
widths.slice(at, at + Math.max(1, span || 1)).reduce((sum, w) => sum + w, 0);
|
|
150
|
+
let spanned = (widths, at, span) => widths.slice(at, at + span).reduce((sum, w) => sum + w, 0);
|
|
151
151
|
|
|
152
152
|
/**
|
|
153
153
|
* One row of cells, each wearing what the row layers over it and taking the
|
|
@@ -158,7 +158,8 @@ let spanned = (widths, at, span) =>
|
|
|
158
158
|
* page band binds the `page` anchor, which is why no sequence is named here.
|
|
159
159
|
*
|
|
160
160
|
* @type {(cells: any[], widths: number[], heading: boolean, layer: any,
|
|
161
|
-
* how: { pad: Record<string, number>, intl: any
|
|
161
|
+
* how: { pad: Record<string, number>, intl: any,
|
|
162
|
+
* link: (url: string) => string }) => string}
|
|
162
163
|
*/
|
|
163
164
|
export let record = (cells, widths, heading, layer, how) => {
|
|
164
165
|
let at = 0;
|
|
@@ -171,7 +172,7 @@ export let record = (cells, widths, heading, layer, how) => {
|
|
|
171
172
|
spanned(widths, at, span),
|
|
172
173
|
// `boxed: false`: the cell above draws the box, so the paragraph inside
|
|
173
174
|
// it must not draw a second one.
|
|
174
|
-
paragraph(paraProps(style, false), runs(cell.tokens, looking(style, how
|
|
175
|
+
paragraph(paraProps(style, false), runs(cell.tokens, looking(style, how))),
|
|
175
176
|
how.pad,
|
|
176
177
|
);
|
|
177
178
|
at += span;
|
package/lib/text.js
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { display, format, styledRuns } from "quario";
|
|
20
20
|
import { capitals, runProps } from "./style.js";
|
|
21
|
-
import { esc } from "./xml.js";
|
|
21
|
+
import { R, esc } from "./xml.js";
|
|
22
22
|
|
|
23
23
|
/** One paragraph, its properties already written.
|
|
24
24
|
* @type {(props: string, inner: string) => string} */
|
|
@@ -104,38 +104,90 @@ let shown = (token, look) => format(token.value, look.style, look.intl) ?? displ
|
|
|
104
104
|
/**
|
|
105
105
|
* What a stretch of text is written with: the resolved style it wears, the run
|
|
106
106
|
* properties that style became, whether it is capitalised, the instance's
|
|
107
|
-
* `format` configuration,
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* why nothing below it takes one
|
|
107
|
+
* `format` configuration, which field gives the current sequence's length, and
|
|
108
|
+
* the registry a link's URL becomes a relationship in. Carried together because
|
|
109
|
+
* every one of them is read for every token, and derived once per run rather
|
|
110
|
+
* than per token — `sequence` included, which is why nothing below it takes one
|
|
111
|
+
* as an argument.
|
|
111
112
|
*
|
|
112
113
|
* @typedef {{ style: any, props: string, caps: boolean, intl: any,
|
|
113
|
-
* sequence: string }} Look
|
|
114
|
+
* sequence: string, link: (url: string) => string }} Look
|
|
114
115
|
*/
|
|
115
116
|
|
|
116
|
-
/**
|
|
117
|
-
|
|
117
|
+
/** A `Look` from what the render already carries — `content.js`'s `Ctx`, a
|
|
118
|
+
* table's `how`, or an outer `Look`. A body cell names no sequence, because
|
|
119
|
+
* only a page band binds the `page` anchor.
|
|
120
|
+
* @type {(style: any, of: { intl: any, sequence?: string,
|
|
121
|
+
* link: (url: string) => string }, boxed?: boolean) => Look} */
|
|
122
|
+
export let looking = (style, of, boxed = false) => ({
|
|
118
123
|
style,
|
|
119
124
|
props: runProps(style, boxed),
|
|
120
125
|
caps: capitals(style),
|
|
121
|
-
intl,
|
|
122
|
-
sequence,
|
|
126
|
+
intl: of.intl,
|
|
127
|
+
sequence: of.sequence || DOCUMENT_PAGES,
|
|
128
|
+
link: of.link,
|
|
123
129
|
});
|
|
124
130
|
|
|
131
|
+
/**
|
|
132
|
+
* A run that carries an [`href`](../../../SCHEMA.md#style-declarations), inside
|
|
133
|
+
* its hyperlink. The engine admitted the URL against the host's scheme
|
|
134
|
+
* allowlist before it crossed (`docs/adr/0085`), so this target escapes it like
|
|
135
|
+
* every other string and decides nothing about it.
|
|
136
|
+
*
|
|
137
|
+
* An `href` beginning `#` names a group's `label`, which is the bookmark below;
|
|
138
|
+
* anything else is a URL, and a URL leaves the package, so it is a relationship
|
|
139
|
+
* of the part the run lands in. A run with no text is no link: there would be
|
|
140
|
+
* nothing to click, and the relationship would reach out of the document for
|
|
141
|
+
* nobody.
|
|
142
|
+
*
|
|
143
|
+
* The relationship namespace is declared on the element that uses it, as the
|
|
144
|
+
* picture's `blip` declares it: a page band's part carries no `r` of its own,
|
|
145
|
+
* and a part that opened one only where a link happened to land would be a
|
|
146
|
+
* second rule about where a namespace comes from.
|
|
147
|
+
*
|
|
148
|
+
* @type {(inner: string, look: Look) => string}
|
|
149
|
+
*/
|
|
150
|
+
let linked = (inner, look) => {
|
|
151
|
+
let href = look.style?.href;
|
|
152
|
+
if (!href || !inner) return inner;
|
|
153
|
+
let at = href.startsWith("#")
|
|
154
|
+
? `w:anchor="${esc(href.slice(1))}"`
|
|
155
|
+
: `${R} r:id="${look.link(href)}"`;
|
|
156
|
+
return `<w:hyperlink ${at}>` + inner + "</w:hyperlink>";
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A destination inside the document: the bookmark an instance's
|
|
161
|
+
* [`label`](../../../SCHEMA.md#group-definition) names, around the content of
|
|
162
|
+
* the item that claimed it. The name is the label itself, which is what an
|
|
163
|
+
* `href` of `#label` asks for; the id is Word's own, an integer the document
|
|
164
|
+
* uses once.
|
|
165
|
+
*
|
|
166
|
+
* @type {(mark: { id: number, name: string } | null, inner: string) => string}
|
|
167
|
+
*/
|
|
168
|
+
export let bookmark = (mark, inner) =>
|
|
169
|
+
mark
|
|
170
|
+
? `<w:bookmarkStart w:id="${mark.id}" w:name="${esc(mark.name)}"/>` +
|
|
171
|
+
inner +
|
|
172
|
+
`<w:bookmarkEnd w:id="${mark.id}"/>`
|
|
173
|
+
: inner;
|
|
174
|
+
|
|
125
175
|
/**
|
|
126
176
|
* A cell's tokens as Word runs. The engine's `styledRuns` is the grouping, so
|
|
127
177
|
* a styled run's own resolved style replaces the cell's for its stretch — the
|
|
128
178
|
* two are already composed on the stream — and a tagged token stays a field
|
|
129
|
-
* whatever style it wears.
|
|
179
|
+
* whatever style it wears. A run carrying an `href` sits inside its hyperlink,
|
|
180
|
+
* which is an element of the paragraph rather than a run property.
|
|
130
181
|
*
|
|
131
182
|
* @type {(tokens: any[], look: Look) => string}
|
|
132
183
|
*/
|
|
133
184
|
export let runs = (tokens, look) =>
|
|
134
185
|
styledRuns(tokens)
|
|
135
186
|
.map((styled) => {
|
|
136
|
-
let own = styled.style ? looking(styled.style, look
|
|
137
|
-
|
|
187
|
+
let own = styled.style ? looking(styled.style, look, true) : look;
|
|
188
|
+
let inner = styled.tokens
|
|
138
189
|
.map((token) => fieldOf(token, own.sequence) || run(textOf(token, own), own.props))
|
|
139
190
|
.join("");
|
|
191
|
+
return linked(inner, own);
|
|
140
192
|
})
|
|
141
193
|
.join("");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quario/docx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Tiny, flow-based Word render target for quario. Real Word tables, a navigable outline, live page-number fields.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"csp",
|
|
@@ -33,12 +33,13 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
|
-
"check": "npm run size && npm test
|
|
36
|
+
"check": "npm run size && npm test",
|
|
37
|
+
"coverage:check": "c8 report --src lib/ --temp-directory=../../coverage/tmp --reporter=text --check-coverage --100",
|
|
37
38
|
"size": "size-limit",
|
|
38
|
-
"test": "npm run test:unit && npm run test:types",
|
|
39
|
+
"test": "npm run test:unit && npm run test:browser && npm run test:types && npm run coverage:check",
|
|
39
40
|
"test:browser": "node test/browser/setup.js",
|
|
40
41
|
"test:types": "tsc && attw --pack . --profile esm-only",
|
|
41
|
-
"test:unit": "node --disallow-code-generation-from-strings --test --test-concurrency=1 test/*.test.js",
|
|
42
|
+
"test:unit": "c8 --clean=false --src lib/ --reporter=none --temp-directory=../../coverage/tmp node --disallow-code-generation-from-strings --test --test-concurrency=1 test/*.test.js",
|
|
42
43
|
"prepack": "node -e \"require('fs').copyFileSync('../../LICENSE','LICENSE')\"",
|
|
43
44
|
"postpack": "node -e \"require('fs').rmSync('LICENSE',{force:true})\""
|
|
44
45
|
},
|
|
@@ -48,12 +49,12 @@
|
|
|
48
49
|
"devDependencies": {
|
|
49
50
|
"@arethetypeswrong/cli": "^0.18.3",
|
|
50
51
|
"@size-limit/preset-small-lib": "^13.0.3",
|
|
51
|
-
"quario": "^0.
|
|
52
|
+
"quario": "^0.11.0",
|
|
52
53
|
"size-limit": "^13.0.3",
|
|
53
54
|
"typescript": "^7.0.2"
|
|
54
55
|
},
|
|
55
56
|
"peerDependencies": {
|
|
56
|
-
"quario": "^0.
|
|
57
|
+
"quario": "^0.11.0"
|
|
57
58
|
},
|
|
58
59
|
"size-limit": [
|
|
59
60
|
{
|