@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/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/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
|
|
@@ -25,11 +24,13 @@
|
|
|
25
24
|
*/
|
|
26
25
|
import { W, XML } from "./xml.js";
|
|
27
26
|
|
|
28
|
-
// The base. Arial is the name the sans generic maps to;
|
|
29
|
-
// half-points; 1.4 line spacing is `line` in 240ths
|
|
30
|
-
// 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).
|
|
31
31
|
const FAMILY = "Arial";
|
|
32
|
-
const
|
|
32
|
+
export const BASE = 10;
|
|
33
|
+
const SIZE = BASE * 2;
|
|
33
34
|
const LINE = 336;
|
|
34
35
|
|
|
35
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,7 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quario/docx",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.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": {
|
|
@@ -26,12 +33,13 @@
|
|
|
26
33
|
"access": "public"
|
|
27
34
|
},
|
|
28
35
|
"scripts": {
|
|
29
|
-
"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",
|
|
30
38
|
"size": "size-limit",
|
|
31
|
-
"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",
|
|
32
40
|
"test:browser": "node test/browser/setup.js",
|
|
33
41
|
"test:types": "tsc && attw --pack . --profile esm-only",
|
|
34
|
-
"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",
|
|
35
43
|
"prepack": "node -e \"require('fs').copyFileSync('../../LICENSE','LICENSE')\"",
|
|
36
44
|
"postpack": "node -e \"require('fs').rmSync('LICENSE',{force:true})\""
|
|
37
45
|
},
|
|
@@ -41,12 +49,12 @@
|
|
|
41
49
|
"devDependencies": {
|
|
42
50
|
"@arethetypeswrong/cli": "^0.18.3",
|
|
43
51
|
"@size-limit/preset-small-lib": "^13.0.3",
|
|
44
|
-
"quario": "^0.
|
|
52
|
+
"quario": "^0.11.0",
|
|
45
53
|
"size-limit": "^13.0.3",
|
|
46
54
|
"typescript": "^7.0.2"
|
|
47
55
|
},
|
|
48
56
|
"peerDependencies": {
|
|
49
|
-
"quario": "^0.
|
|
57
|
+
"quario": "^0.11.0"
|
|
50
58
|
},
|
|
51
59
|
"size-limit": [
|
|
52
60
|
{
|