postext 1.22.1 → 1.22.3

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.
Files changed (65) hide show
  1. package/dist/__tests__/exports.test.js +4 -0
  2. package/dist/__tests__/exports.test.js.map +1 -1
  3. package/dist/comics/lettering/__tests__/panelCache.test.d.ts +2 -0
  4. package/dist/comics/lettering/__tests__/panelCache.test.d.ts.map +1 -0
  5. package/dist/comics/lettering/__tests__/panelCache.test.js +43 -0
  6. package/dist/comics/lettering/__tests__/panelCache.test.js.map +1 -0
  7. package/dist/comics/lettering/letter.d.ts.map +1 -1
  8. package/dist/comics/lettering/letter.js +73 -0
  9. package/dist/comics/lettering/letter.js.map +1 -1
  10. package/dist/comics/lettering/shape-text.d.ts.map +1 -1
  11. package/dist/comics/lettering/shape-text.js +64 -16
  12. package/dist/comics/lettering/shape-text.js.map +1 -1
  13. package/dist/fonts/usage.d.ts +46 -0
  14. package/dist/fonts/usage.d.ts.map +1 -0
  15. package/dist/fonts/usage.js +205 -0
  16. package/dist/fonts/usage.js.map +1 -0
  17. package/dist/index.d.ts +2 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/index.js.map +1 -1
  21. package/dist/word/analyze.d.ts +33 -0
  22. package/dist/word/analyze.d.ts.map +1 -0
  23. package/dist/word/analyze.js +167 -0
  24. package/dist/word/analyze.js.map +1 -0
  25. package/dist/word/docxRead.d.ts +10 -0
  26. package/dist/word/docxRead.d.ts.map +1 -0
  27. package/dist/word/docxRead.js +584 -0
  28. package/dist/word/docxRead.js.map +1 -0
  29. package/dist/word/index.d.ts +14 -0
  30. package/dist/word/index.d.ts.map +1 -0
  31. package/dist/word/index.js +12 -0
  32. package/dist/word/index.js.map +1 -0
  33. package/dist/word/inline.d.ts +48 -0
  34. package/dist/word/inline.d.ts.map +1 -0
  35. package/dist/word/inline.js +606 -0
  36. package/dist/word/inline.js.map +1 -0
  37. package/dist/word/model.d.ts +141 -0
  38. package/dist/word/model.d.ts.map +1 -0
  39. package/dist/word/model.js +23 -0
  40. package/dist/word/model.js.map +1 -0
  41. package/dist/word/omml.d.ts +4 -0
  42. package/dist/word/omml.d.ts.map +1 -0
  43. package/dist/word/omml.js +139 -0
  44. package/dist/word/omml.js.map +1 -0
  45. package/dist/word/template.d.ts +111 -0
  46. package/dist/word/template.d.ts.map +1 -0
  47. package/dist/word/template.js +255 -0
  48. package/dist/word/template.js.map +1 -0
  49. package/dist/word/toDocx.d.ts +28 -0
  50. package/dist/word/toDocx.d.ts.map +1 -0
  51. package/dist/word/toDocx.js +684 -0
  52. package/dist/word/toDocx.js.map +1 -0
  53. package/dist/word/toMarkdown.d.ts +53 -0
  54. package/dist/word/toMarkdown.d.ts.map +1 -0
  55. package/dist/word/toMarkdown.js +632 -0
  56. package/dist/word/toMarkdown.js.map +1 -0
  57. package/dist/word/word.test.d.ts +2 -0
  58. package/dist/word/word.test.d.ts.map +1 -0
  59. package/dist/word/word.test.js +302 -0
  60. package/dist/word/word.test.js.map +1 -0
  61. package/dist/word/xml.d.ts +30 -0
  62. package/dist/word/xml.d.ts.map +1 -0
  63. package/dist/word/xml.js +127 -0
  64. package/dist/word/xml.js.map +1 -0
  65. package/package.json +5 -1
@@ -0,0 +1,684 @@
1
+ // Postext chapters → a `.docx` someone can edit in Word and send back. The
2
+ // import template is applied in reverse: headings, paragraph styles,
3
+ // callouts, quotes, lists, chips and footnotes become Word styles, numbering
4
+ // and notes; what Word cannot say (fences, directives, `:ref`, maths, front
5
+ // matter) is kept verbatim in the *Postext Markup* styles. The template
6
+ // travels inside the file, so the re-import maps every style back.
7
+ import { strToU8, zipSync } from 'fflate';
8
+ import { parseInline } from './inline';
9
+ import { calloutStylesOf, chipStylesOf, CHAPTER_STYLE, MARKUP_CHAR_STYLE, MARKUP_STYLE, } from './template';
10
+ import { TEMPLATE_XML_NS, TEMPLATE_XML_ROOT } from './docxRead';
11
+ import { escapeAttr, escapeXml } from './xml';
12
+ export const DEFAULT_STYLE_NAMES = {
13
+ callout: 'Callout: __name__',
14
+ calloutTitle: 'Callout: __name__: title',
15
+ chip: 'Chip: __name__',
16
+ };
17
+ const HEADING_RE = /^(#{1,6})\s+(.+)$/;
18
+ const FENCE_RE = /^:::\s*([a-z][a-z0-9-]*)\s*(?:\{([^}]*)\})?\s*$/;
19
+ const CLOSE_RE = /^:::\s*$/;
20
+ const CONTAINERS = new Set(['callout', 'paragraphs', 'part', 'columns', 'paper']);
21
+ const RAW_BODY = new Set(['references', 'verse', 'page', 'strip']);
22
+ const TASK_RE = /^(\s*)([-*+])\s+(\[[ xX]\]\s+.*)$/;
23
+ const ORDERED_RE = /^(\s*)([0-9]+)([.)])\s+(.*)$/;
24
+ const BULLET_RE = /^(\s*)([-*+])\s+(.*)$/;
25
+ const NOTE_DEF_RE = /^\[\^([\p{L}\p{N}_.:-]+)\]:[ \t]*/u;
26
+ const RESOURCE_RE = /^::resource\s*\{id="[^"]+"\}\s*$/;
27
+ const MATH_SINGLE_RE = /^\s*\$\$[\s\S]+?\$\$\s*$/;
28
+ const MATH_FENCE_RE = /^\s*\$\$\s*$/;
29
+ /** `key="value"` / `key=value` / flag pairs of an attribute blob. */
30
+ function attrPairs(blob) {
31
+ const out = [];
32
+ const re = /([A-Za-z_][\w-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"']+)))?/g;
33
+ let m;
34
+ while ((m = re.exec(blob)))
35
+ out.push([m[1], m[2] ?? m[3] ?? m[4] ?? true]);
36
+ return out;
37
+ }
38
+ function isBlank(line) {
39
+ return line.trim() === '';
40
+ }
41
+ /** Does a paragraph run end at this line? (blank, heading, list, quote,
42
+ * fence, maths, resource, note definition) */
43
+ function startsBlock(trimmed, raw) {
44
+ return HEADING_RE.test(trimmed) || trimmed.startsWith('>') || FENCE_RE.test(trimmed) || CLOSE_RE.test(trimmed)
45
+ || TASK_RE.test(raw) || ORDERED_RE.test(raw) || BULLET_RE.test(raw) || RESOURCE_RE.test(trimmed)
46
+ || MATH_SINGLE_RE.test(trimmed) || MATH_FENCE_RE.test(trimmed) || NOTE_DEF_RE.test(trimmed);
47
+ }
48
+ function parseChapter(markdown, listRunBase) {
49
+ const lines = markdown.replace(/\r\n?/g, '\n').split('\n');
50
+ const blocks = [];
51
+ const notes = new Map();
52
+ let i = 0;
53
+ // Front matter: verbatim.
54
+ if (lines[0]?.trim() === '---') {
55
+ const end = lines.findIndex((l, k) => k > 0 && l.trim() === '---');
56
+ if (end > 0) {
57
+ for (let k = 0; k <= end; k++)
58
+ blocks.push({ kind: 'markup', text: lines[k] });
59
+ i = end + 1;
60
+ }
61
+ }
62
+ /** Open containers: explicit ones (fences kept verbatim) and the one
63
+ * implicit `:::paragraphs` group a Word style stands for. */
64
+ const stack = [];
65
+ const role = () => {
66
+ for (let k = stack.length - 1; k >= 0; k--)
67
+ if (stack[k].role)
68
+ return stack[k].role;
69
+ return { kind: 'body' };
70
+ };
71
+ let listStack = [];
72
+ let listRun = -1;
73
+ let lastImplicitStyle = null;
74
+ let lastImplicitEnd = -2;
75
+ const endList = () => {
76
+ listStack = [];
77
+ listRun = -1;
78
+ };
79
+ // Two verbatim blocks come back joined line to line; a blank line
80
+ // between them in the source travels as an empty markup paragraph.
81
+ let blankBefore = false;
82
+ const pushBlock = (b) => {
83
+ if (b.kind === 'markup' && blankBefore && blocks[blocks.length - 1]?.kind === 'markup')
84
+ blocks.push({ kind: 'markup', text: '' });
85
+ blankBefore = false;
86
+ blocks.push(b);
87
+ };
88
+ while (i < lines.length) {
89
+ const raw = lines[i];
90
+ const t = raw.trim();
91
+ if (isBlank(raw)) {
92
+ blankBefore = true;
93
+ i++;
94
+ continue;
95
+ }
96
+ const fence = FENCE_RE.exec(t);
97
+ if (fence) {
98
+ endList();
99
+ const name = fence[1];
100
+ const attrs = attrPairs(fence[2] ?? '');
101
+ if (RAW_BODY.has(name)) {
102
+ pushBlock({ kind: 'markup', text: raw });
103
+ i++;
104
+ while (i < lines.length && !CLOSE_RE.test(lines[i].trim()))
105
+ blocks.push({ kind: 'markup', text: lines[i++] });
106
+ if (i < lines.length)
107
+ blocks.push({ kind: 'markup', text: lines[i++] });
108
+ continue;
109
+ }
110
+ if (!CONTAINERS.has(name)) {
111
+ pushBlock({ kind: 'markup', text: raw });
112
+ i++;
113
+ continue;
114
+ }
115
+ const style = attrs.find(([k]) => k === 'style')?.[1];
116
+ if (name === 'paragraphs' && stack.length === 0 && attrs.length === 1 && typeof style === 'string' && style) {
117
+ // A Word paragraph style can stand for the fence when the group
118
+ // holds only paragraphs and does not touch another group of the
119
+ // same style (the two would merge on the way back).
120
+ let depth = 1;
121
+ let simple = true;
122
+ let k = i + 1;
123
+ for (; k < lines.length; k++) {
124
+ const lt = lines[k].trim();
125
+ if (isBlank(lines[k]))
126
+ continue;
127
+ if (CLOSE_RE.test(lt)) {
128
+ if (--depth === 0)
129
+ break;
130
+ continue;
131
+ }
132
+ if (FENCE_RE.test(lt)) {
133
+ simple = false;
134
+ depth++;
135
+ continue;
136
+ }
137
+ if (startsBlock(lt, lines[k]))
138
+ simple = false;
139
+ }
140
+ const adjacent = lastImplicitStyle === style && lastImplicitEnd === blocks.length;
141
+ if (simple && k < lines.length && !adjacent) {
142
+ stack.push({ name, role: { kind: 'paragraphs', style }, implicit: true });
143
+ i++;
144
+ continue;
145
+ }
146
+ }
147
+ const fenceRole = name === 'callout'
148
+ ? { kind: 'callout', type: attrs.find(([key]) => key === 'type')?.[1] ?? '' }
149
+ : name === 'paragraphs' && typeof style === 'string' && style ? { kind: 'paragraphs', style } : null;
150
+ stack.push({ name, role: fenceRole, implicit: false });
151
+ pushBlock({ kind: 'markup', text: raw });
152
+ i++;
153
+ continue;
154
+ }
155
+ if (CLOSE_RE.test(t)) {
156
+ endList();
157
+ const top = stack.pop();
158
+ if (top?.implicit) {
159
+ lastImplicitStyle = top.role?.kind === 'paragraphs' ? top.role.style : null;
160
+ lastImplicitEnd = blocks.length;
161
+ }
162
+ else
163
+ pushBlock({ kind: 'markup', text: raw });
164
+ i++;
165
+ continue;
166
+ }
167
+ if (RESOURCE_RE.test(t) || MATH_SINGLE_RE.test(t)) {
168
+ endList();
169
+ pushBlock({ kind: 'markup', text: raw });
170
+ i++;
171
+ continue;
172
+ }
173
+ if (MATH_FENCE_RE.test(t)) {
174
+ endList();
175
+ pushBlock({ kind: 'markup', text: raw });
176
+ i++;
177
+ while (i < lines.length && !MATH_FENCE_RE.test(lines[i].trim()))
178
+ blocks.push({ kind: 'markup', text: lines[i++] });
179
+ if (i < lines.length)
180
+ blocks.push({ kind: 'markup', text: lines[i++] });
181
+ continue;
182
+ }
183
+ const heading = HEADING_RE.exec(t);
184
+ if (heading) {
185
+ endList();
186
+ let text = heading[2];
187
+ let style;
188
+ const attrs = /(\s+)\{([^{}]*)\}\s*$/.exec(text);
189
+ if (attrs) {
190
+ const pairs = attrPairs(attrs[2]);
191
+ const s = pairs.find(([k]) => k === 'style');
192
+ // Only a blob the grammar reads whole is attributes.
193
+ const reconstructed = attrs[2].trim();
194
+ if (s && typeof s[1] === 'string' && /^(?:[A-Za-z_][\w-]*(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"']+))?\s*)*$/.test(reconstructed)) {
195
+ style = s[1];
196
+ const rest = reconstructed.replace(/(^|\s)style\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"']+)/, '').trim();
197
+ text = text.slice(0, attrs.index) + (rest ? `${attrs[1]}{${rest}}` : '');
198
+ }
199
+ }
200
+ pushBlock({ kind: 'heading', level: heading[1].length, ...(style ? { style } : {}), text });
201
+ i++;
202
+ continue;
203
+ }
204
+ if (t.startsWith('>')) {
205
+ endList();
206
+ const parts = [];
207
+ while (i < lines.length && lines[i].trim().startsWith('>'))
208
+ parts.push(lines[i++].trim().replace(/^>\s?/, ''));
209
+ pushBlock({ kind: 'para', text: parts.join(' ').trim(), role: { kind: 'quote' } });
210
+ continue;
211
+ }
212
+ const task = TASK_RE.exec(raw);
213
+ const ordered = task ? null : ORDERED_RE.exec(raw);
214
+ const bullet = task || ordered ? null : BULLET_RE.exec(raw);
215
+ const item = task ?? ordered ?? bullet;
216
+ if (item) {
217
+ const col = item[1].replace(/\t/g, ' ').length;
218
+ while (listStack.length && listStack[listStack.length - 1] + 2 > col)
219
+ listStack.pop();
220
+ listStack.push(col);
221
+ if (listRun < 0)
222
+ listRun = listRunBase.n++;
223
+ const depth = Math.min(5, listStack.length);
224
+ pushBlock({
225
+ kind: 'list',
226
+ text: (task ? task[3] : ordered ? ordered[4] : bullet[3]).trim(),
227
+ depth,
228
+ ordered: !!ordered,
229
+ number: ordered ? Number(ordered[2]) : 0,
230
+ role: role(),
231
+ run: listRun,
232
+ });
233
+ i++;
234
+ continue;
235
+ }
236
+ // A paragraph (or a note definition): lines up to the next block.
237
+ endList();
238
+ const parts = [t];
239
+ i++;
240
+ while (i < lines.length && !isBlank(lines[i]) && !startsBlock(lines[i].trim(), lines[i]))
241
+ parts.push(lines[i++].trim());
242
+ const text = parts.join(' ');
243
+ const def = NOTE_DEF_RE.exec(text);
244
+ if (def) {
245
+ const id = def[1];
246
+ if (!notes.has(id))
247
+ notes.set(id, text.slice(def[0].length));
248
+ pushBlock({ kind: 'markup', text });
249
+ continue;
250
+ }
251
+ pushBlock({ kind: 'para', text, role: role() });
252
+ }
253
+ return { blocks, notes };
254
+ }
255
+ const sameTarget = (a, b) => JSON.stringify(a) === JSON.stringify(b);
256
+ const BUILTIN_HEADING = (level) => `heading ${level}`;
257
+ function halfPoints(d, base = 10) {
258
+ if (!d)
259
+ return undefined;
260
+ const pt = d.unit === 'pt' ? d.value : d.unit === 'px' ? d.value * 0.75 : d.unit === 'mm' ? d.value * 2.8346 : d.unit === 'cm' ? d.value * 28.346 : d.unit === 'in' ? d.value * 72 : d.value * base;
261
+ return pt > 4 && pt < 200 ? Math.round(pt * 2) : undefined;
262
+ }
263
+ function twips(d, base = 10) {
264
+ const hp = halfPoints(d, base);
265
+ return hp !== undefined ? hp * 10 : undefined;
266
+ }
267
+ const hex = (c) => (c?.hex && /^#[0-9a-fA-F]{6}$/.test(c.hex) ? c.hex.slice(1).toUpperCase() : undefined);
268
+ class Styles {
269
+ template;
270
+ config;
271
+ names;
272
+ defs = new Map();
273
+ byName = new Map();
274
+ paragraphs = {};
275
+ characters = {};
276
+ constructor(template, config, names) {
277
+ this.template = template;
278
+ this.config = config;
279
+ this.names = names;
280
+ const body = config.bodyText;
281
+ const bodySize = halfPoints(body?.fontSize) ?? 22;
282
+ this.define({ id: 'Normal', name: 'Normal', kind: 'paragraph', builtin: true, pPr: '<w:spacing w:after="120" w:line="276" w:lineRule="auto"/>', rPr: `<w:sz w:val="${bodySize}"/>` });
283
+ const sizes = [40, 32, 28, 26, 24, 22];
284
+ for (let l = 1; l <= 6; l++) {
285
+ this.define({ id: `Heading${l}`, name: BUILTIN_HEADING(l), kind: 'paragraph', builtin: true, basedOn: 'Normal', pPr: `<w:keepNext/><w:spacing w:before="${l === 1 ? 480 : 240}" w:after="120"/><w:outlineLvl w:val="${l - 1}"/>`, rPr: `<w:b/><w:sz w:val="${sizes[l - 1]}"/>` });
286
+ }
287
+ this.define({ id: 'Quote', name: 'Quote', kind: 'paragraph', builtin: true, basedOn: 'Normal', pPr: '<w:ind w:left="720" w:right="720"/>', rPr: '<w:i/><w:color w:val="595959"/>' });
288
+ this.define({ id: 'ListParagraph', name: 'List Paragraph', kind: 'paragraph', builtin: true, basedOn: 'Normal', pPr: '<w:ind w:left="720"/><w:contextualSpacing/>' });
289
+ this.define({ id: 'FootnoteText', name: 'footnote text', kind: 'paragraph', builtin: true, basedOn: 'Normal', pPr: '<w:spacing w:after="0" w:line="240" w:lineRule="auto"/>', rPr: '<w:sz w:val="18"/>' });
290
+ this.define({ id: 'FootnoteReference', name: 'footnote reference', kind: 'character', builtin: true, rPr: '<w:vertAlign w:val="superscript"/>' });
291
+ this.define({ id: 'Hyperlink', name: 'Hyperlink', kind: 'character', builtin: true, rPr: '<w:color w:val="0563C1"/><w:u w:val="single"/>' });
292
+ const mono = '<w:rFonts w:ascii="Consolas" w:hAnsi="Consolas" w:cs="Consolas"/><w:color w:val="7F7F7F"/><w:sz w:val="18"/>';
293
+ this.define({ id: 'PostextMarkup', name: MARKUP_STYLE, kind: 'paragraph', basedOn: 'Normal', pPr: '<w:shd w:val="clear" w:color="auto" w:fill="F2F2F2"/><w:spacing w:after="0"/>', rPr: mono });
294
+ this.define({ id: 'PostextMarkupInline', name: MARKUP_CHAR_STYLE, kind: 'character', rPr: `${mono}<w:shd w:val="clear" w:color="auto" w:fill="F2F2F2"/>` });
295
+ this.define({ id: 'PostextChapter', name: CHAPTER_STYLE, kind: 'paragraph', basedOn: 'Normal', pPr: '<w:pageBreakBefore/><w:pBdr><w:bottom w:val="single" w:sz="6" w:space="4" w:color="A6A6A6"/></w:pBdr><w:spacing w:after="240"/>', rPr: '<w:caps/><w:color w:val="7F7F7F"/><w:sz w:val="20"/>' });
296
+ this.paragraphs[MARKUP_STYLE] = { kind: 'markup' };
297
+ this.paragraphs[CHAPTER_STYLE] = { kind: 'chapter' };
298
+ this.characters[MARKUP_CHAR_STYLE] = { kind: 'markup' };
299
+ }
300
+ define(def) {
301
+ const existing = this.byName.get(def.name.toLowerCase());
302
+ if (existing)
303
+ return existing;
304
+ let id = def.id.replace(/[^A-Za-z0-9]/g, '') || 'Style';
305
+ for (let n = 2; this.defs.has(id); n++)
306
+ id = `${def.id.replace(/[^A-Za-z0-9]/g, '') || 'Style'}${n}`;
307
+ const d = { ...def, id };
308
+ this.defs.set(id, d);
309
+ this.byName.set(d.name.toLowerCase(), d);
310
+ return d;
311
+ }
312
+ /** The template's own name for a target, when it has one. */
313
+ templateName(map, target) {
314
+ for (const [name, t] of Object.entries(map))
315
+ if (sameTarget(t, target))
316
+ return name;
317
+ return undefined;
318
+ }
319
+ freeName(base, kind, target, map) {
320
+ let name = base;
321
+ for (let n = 2;; n++) {
322
+ const taken = this.byName.get(name.toLowerCase());
323
+ if (!taken)
324
+ return name;
325
+ const meaning = map[taken.name];
326
+ if (taken.kind === kind && (!meaning || sameTarget(meaning, target)) && !(taken.builtin && !meaning))
327
+ return name;
328
+ name = `${base} (${n})`;
329
+ }
330
+ }
331
+ /** A paragraph style for a target, defined on first use. */
332
+ paragraph(target, fallbackName, look) {
333
+ // A name that already means something else (a Postext style called
334
+ // `Quote`, one heading style used at two levels) gets a number.
335
+ const name = this.freeName(this.templateName(this.template.paragraphs, target) ?? fallbackName(), 'paragraph', target, this.paragraphs);
336
+ const def = this.define({ id: name, name, kind: 'paragraph', ...look() });
337
+ this.paragraphs[def.name] = target;
338
+ return def.id;
339
+ }
340
+ character(target, fallbackName, look) {
341
+ const name = this.freeName(this.templateName(this.template.characters, target) ?? fallbackName(), 'character', target, this.characters);
342
+ const def = this.define({ id: name, name, kind: 'character', ...look() });
343
+ this.characters[def.name] = target;
344
+ return def.id;
345
+ }
346
+ heading(level, style) {
347
+ if (!style) {
348
+ const custom = this.templateName(this.template.paragraphs, { kind: 'heading', level });
349
+ if (!custom || custom.toLowerCase() === BUILTIN_HEADING(level)) {
350
+ this.paragraphs[BUILTIN_HEADING(level)] = { kind: 'heading', level };
351
+ return `Heading${level}`;
352
+ }
353
+ }
354
+ const hs = style ? this.config.headingStyles?.find((s) => s.id === style) : undefined;
355
+ return this.paragraph({ kind: 'heading', level, ...(style ? { style } : {}) }, () => (hs ? hs.name ?? hs.id : style ?? BUILTIN_HEADING(level)), () => ({ basedOn: `Heading${level}` }));
356
+ }
357
+ paragraphsStyle(style) {
358
+ const ps = this.config.paragraphStyles?.find((s) => s.id === style);
359
+ return this.paragraph({ kind: 'paragraphs', style }, () => ps?.name ?? style, () => {
360
+ let rPr = '';
361
+ let pPr = '';
362
+ if (ps?.fontFamily)
363
+ rPr += `<w:rFonts w:ascii="${escapeAttr(ps.fontFamily)}" w:hAnsi="${escapeAttr(ps.fontFamily)}"/>`;
364
+ if (ps?.italic)
365
+ rPr += '<w:i/>';
366
+ if (ps?.smallCaps)
367
+ rPr += '<w:smallCaps/>';
368
+ const c = hex(ps?.color);
369
+ if (c)
370
+ rPr += `<w:color w:val="${c}"/>`;
371
+ const sz = halfPoints(ps?.fontSize);
372
+ if (sz)
373
+ rPr += `<w:sz w:val="${sz}"/>`;
374
+ const jc = ps?.textAlign === 'center' ? 'center' : ps?.textAlign === 'right' || ps?.textAlign === 'end' ? 'right' : ps?.textAlign === 'justify' ? 'both' : ps?.textAlign ? 'left' : undefined;
375
+ // Schema order: ind before jc.
376
+ const ind = twips(ps?.indent);
377
+ if (ind)
378
+ pPr += `<w:ind w:left="${ind}"/>`;
379
+ if (jc)
380
+ pPr += `<w:jc w:val="${jc}"/>`;
381
+ return { basedOn: 'Normal', pPr, rPr };
382
+ });
383
+ }
384
+ callout(type, title = false) {
385
+ const cs = calloutStylesOf(this.config).find((s) => s.id === type);
386
+ const label = cs?.name ?? type;
387
+ const fill = (cs?.backgroundEnabled !== false ? hex(cs?.background) : undefined) ?? 'EEF3F8';
388
+ return this.paragraph(title ? { kind: 'calloutTitle', type } : { kind: 'callout', type }, () => (title ? this.names.calloutTitle : this.names.callout).replace('__name__', label), () => ({
389
+ basedOn: 'Normal',
390
+ pPr: `<w:pBdr><w:left w:val="single" w:sz="18" w:space="8" w:color="8EA9C1"/></w:pBdr><w:shd w:val="clear" w:color="auto" w:fill="${fill}"/><w:ind w:left="284" w:right="284"/>`,
391
+ rPr: title ? '<w:b/>' : '',
392
+ }));
393
+ }
394
+ quote() {
395
+ const custom = this.templateName(this.template.paragraphs, { kind: 'quote' });
396
+ if (!custom || custom.toLowerCase() === 'quote') {
397
+ this.paragraphs.Quote = { kind: 'quote' };
398
+ return 'Quote';
399
+ }
400
+ return this.paragraph({ kind: 'quote' }, () => custom, () => ({ basedOn: 'Quote' }));
401
+ }
402
+ body() {
403
+ this.paragraphs.Normal = { kind: 'body' };
404
+ return 'Normal';
405
+ }
406
+ chip(style) {
407
+ const cs = chipStylesOf(this.config).find((s) => s.id === style);
408
+ const fill = hex(cs?.background) ?? 'E8EEF7';
409
+ return this.character({ kind: 'chip', style }, () => this.names.chip.replace('__name__', cs?.name ?? style), () => ({
410
+ rPr: `<w:shd w:val="clear" w:color="auto" w:fill="${fill}"/>`,
411
+ }));
412
+ }
413
+ xml() {
414
+ let out = '';
415
+ for (const d of this.defs.values()) {
416
+ const type = d.kind;
417
+ const isDefault = d.id === 'Normal' ? ' w:default="1"' : '';
418
+ out += `<w:style w:type="${type}" w:styleId="${d.id}"${isDefault}${d.builtin ? '' : ' w:customStyle="1"'}>`;
419
+ out += `<w:name w:val="${escapeAttr(d.name)}"/>`;
420
+ if (d.basedOn)
421
+ out += `<w:basedOn w:val="${d.basedOn}"/>`;
422
+ if (type === 'paragraph')
423
+ out += '<w:next w:val="Normal"/>';
424
+ out += '<w:qFormat/>';
425
+ if (d.pPr && type === 'paragraph')
426
+ out += `<w:pPr>${d.pPr}</w:pPr>`;
427
+ if (d.rPr)
428
+ out += `<w:rPr>${d.rPr}</w:rPr>`;
429
+ out += '</w:style>';
430
+ }
431
+ return out;
432
+ }
433
+ }
434
+ // ---------------------------------------------------------------------------
435
+ // WordprocessingML
436
+ // ---------------------------------------------------------------------------
437
+ const W_NS = 'xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"';
438
+ function textXml(text) {
439
+ // Tabs and newlines inside a verbatim run.
440
+ return text.split(/(\t|\n)/).map((piece) => {
441
+ if (piece === '\t')
442
+ return '<w:tab/>';
443
+ if (piece === '\n')
444
+ return '<w:br/>';
445
+ return piece ? `<w:t xml:space="preserve">${escapeXml(piece)}</w:t>` : '';
446
+ }).join('');
447
+ }
448
+ function runsXml(runs, ctx) {
449
+ let out = '';
450
+ let i = 0;
451
+ while (i < runs.length) {
452
+ const r = runs[i];
453
+ if (r.href !== undefined) {
454
+ let j = i;
455
+ let inner = '';
456
+ while (j < runs.length && runs[j].href === r.href) {
457
+ inner += runXml({ ...runs[j], href: undefined }, ctx, 'Hyperlink');
458
+ j++;
459
+ }
460
+ out += `<w:hyperlink r:id="${ctx.linkRel(r.href)}" w:history="1">${inner}</w:hyperlink>`;
461
+ i = j;
462
+ continue;
463
+ }
464
+ out += runXml(r, ctx);
465
+ i++;
466
+ }
467
+ return out;
468
+ }
469
+ function runXml(r, ctx, charStyle) {
470
+ if (r.note !== undefined) {
471
+ const id = ctx.noteIds.get(r.note);
472
+ if (id === undefined)
473
+ return `<w:r><w:rPr><w:rStyle w:val="PostextMarkupInline"/></w:rPr>${textXml(`[^${r.note}]`)}</w:r>`;
474
+ return `<w:r><w:rPr><w:rStyle w:val="FootnoteReference"/></w:rPr><w:footnoteReference w:id="${id}"/></w:r>`;
475
+ }
476
+ if (!r.text)
477
+ return '';
478
+ let rPr = '';
479
+ const style = r.raw ? 'PostextMarkupInline' : r.chip !== undefined ? ctx.styles.chip(r.chip) : charStyle;
480
+ if (style)
481
+ rPr += `<w:rStyle w:val="${style}"/>`;
482
+ if (r.bold)
483
+ rPr += '<w:b/>';
484
+ if (r.italic)
485
+ rPr += '<w:i/>';
486
+ if (r.smallCaps)
487
+ rPr += '<w:smallCaps/>';
488
+ if (r.script)
489
+ rPr += `<w:vertAlign w:val="${r.script === 'sup' ? 'superscript' : 'subscript'}"/>`;
490
+ return `<w:r>${rPr ? `<w:rPr>${rPr}</w:rPr>` : ''}${textXml(r.text)}</w:r>`;
491
+ }
492
+ function paragraphXml(styleId, content, extraPPr = '') {
493
+ return `<w:p><w:pPr><w:pStyle w:val="${styleId}"/>${extraPPr}</w:pPr>${content}</w:p>`;
494
+ }
495
+ /** Bullets on one abstract list, numbers on another; each run of ordered
496
+ * items gets its own `w:num` with its first number as the start. */
497
+ class Numbering {
498
+ nums = [{ abstract: 0 }];
499
+ open = new Map();
500
+ bullet() {
501
+ return 1;
502
+ }
503
+ ordered(run, depth, number) {
504
+ const key = `${run}:${depth}`;
505
+ const cur = this.open.get(key);
506
+ if (cur && cur.next === number) {
507
+ cur.next++;
508
+ return cur.numId;
509
+ }
510
+ this.nums.push({ abstract: 1, ilvl: depth - 1, start: number });
511
+ const numId = this.nums.length;
512
+ this.open.set(key, { numId, next: number + 1 });
513
+ // A shallower item ends the deeper runs under it.
514
+ for (const k of [...this.open.keys()]) {
515
+ const [r, d] = k.split(':').map(Number);
516
+ if (r === run && d > depth)
517
+ this.open.delete(k);
518
+ }
519
+ return numId;
520
+ }
521
+ /** A shallower item in a run closes the numbering below it. */
522
+ touch(run, depth) {
523
+ for (const k of [...this.open.keys()]) {
524
+ const [r, d] = k.split(':').map(Number);
525
+ if (r === run && d > depth)
526
+ this.open.delete(k);
527
+ }
528
+ }
529
+ xml() {
530
+ const bullets = ['•', '◦', '▪', '•', '◦', '▪', '•', '◦', '▪'];
531
+ const level = (l, fmt, text) => `<w:lvl w:ilvl="${l}"><w:start w:val="1"/><w:numFmt w:val="${fmt}"/><w:lvlText w:val="${escapeAttr(text)}"/><w:lvlJc w:val="left"/><w:pPr><w:ind w:left="${720 * (l + 1)}" w:hanging="360"/></w:pPr></w:lvl>`;
532
+ let out = `<w:abstractNum w:abstractNumId="0"><w:multiLevelType w:val="hybridMultilevel"/>${bullets.map((b, l) => level(l, 'bullet', b)).join('')}</w:abstractNum>`;
533
+ out += `<w:abstractNum w:abstractNumId="1"><w:multiLevelType w:val="multilevel"/>${Array.from({ length: 9 }, (_, l) => level(l, 'decimal', `%${l + 1}.`)).join('')}</w:abstractNum>`;
534
+ this.nums.forEach((n, k) => {
535
+ out += `<w:num w:numId="${k + 1}"><w:abstractNumId w:val="${n.abstract}"/>`;
536
+ if (n.ilvl !== undefined && n.start !== undefined)
537
+ out += `<w:lvlOverride w:ilvl="${n.ilvl}"><w:startOverride w:val="${n.start}"/></w:lvlOverride>`;
538
+ out += '</w:num>';
539
+ });
540
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:numbering ${W_NS}>${out}</w:numbering>`;
541
+ }
542
+ }
543
+ function roleStyle(role, styles) {
544
+ switch (role.kind) {
545
+ case 'paragraphs':
546
+ return styles.paragraphsStyle(role.style);
547
+ case 'callout':
548
+ return role.type ? styles.callout(role.type) : styles.body();
549
+ case 'quote':
550
+ return styles.quote();
551
+ default:
552
+ return styles.body();
553
+ }
554
+ }
555
+ /** Build the `.docx` bytes for chapters. */
556
+ export function postextToDocx(chapters, settings) {
557
+ const styles = new Styles(settings.template, settings.config, settings.names ?? DEFAULT_STYLE_NAMES);
558
+ const numbering = new Numbering();
559
+ const links = new Map();
560
+ const linkRel = (url) => {
561
+ let id = links.get(url);
562
+ if (!id) {
563
+ id = `rIdL${links.size + 1}`;
564
+ links.set(url, id);
565
+ }
566
+ return id;
567
+ };
568
+ const footnotes = [];
569
+ let body = '';
570
+ const listRunBase = { n: 0 };
571
+ chapters.forEach((chapter) => {
572
+ if (settings.book)
573
+ body += paragraphXml('PostextChapter', `<w:r>${textXml(chapter.title)}</w:r>`);
574
+ const { blocks, notes } = parseChapter(chapter.markdown, listRunBase);
575
+ const noteIds = new Map();
576
+ const pending = new Set();
577
+ const ctx = { styles, noteIds, linkRel };
578
+ const inline = (text, heading = false) => parseInline(text, {
579
+ heading,
580
+ notes: heading ? new Set() : new Set(notes.keys()),
581
+ takeNote: (id) => {
582
+ if (noteIds.has(id) || pending.has(id))
583
+ return false;
584
+ pending.add(id);
585
+ return true;
586
+ },
587
+ });
588
+ const citeNotes = (runs) => {
589
+ for (const r of runs) {
590
+ if (r.note === undefined || noteIds.has(r.note))
591
+ continue;
592
+ const wordId = footnotes.length + 1;
593
+ noteIds.set(r.note, wordId);
594
+ const noteRuns = parseInline(notes.get(r.note) ?? '', {});
595
+ footnotes.push(`<w:footnote w:id="${wordId}"><w:p><w:pPr><w:pStyle w:val="FootnoteText"/></w:pPr><w:r><w:rPr><w:rStyle w:val="FootnoteReference"/></w:rPr><w:footnoteRef/></w:r><w:r><w:t xml:space="preserve"> </w:t></w:r>${runsXml(noteRuns, ctx)}</w:p></w:footnote>`);
596
+ }
597
+ };
598
+ // Definitions of notes some marker cites leave the flow (they live in
599
+ // the Word note); the others stay verbatim.
600
+ const cited = new Set();
601
+ for (const b of blocks) {
602
+ if (b.kind === 'para' || b.kind === 'list')
603
+ for (const m of b.text.matchAll(/\[\^([\p{L}\p{N}_.:-]+)\](?!:)/gu))
604
+ cited.add(m[1]);
605
+ }
606
+ for (const b of blocks) {
607
+ switch (b.kind) {
608
+ case 'markup': {
609
+ const def = NOTE_DEF_RE.exec(b.text);
610
+ if (def && cited.has(def[1]) && notes.get(def[1]) === b.text.slice(def[0].length))
611
+ break;
612
+ body += paragraphXml(styles.paragraph({ kind: 'markup' }, () => MARKUP_STYLE, () => ({})), b.text ? `<w:r>${textXml(b.text)}</w:r>` : '');
613
+ break;
614
+ }
615
+ case 'heading': {
616
+ // Trailing attributes (`{toc="false"}`) travel as markup.
617
+ const attrs = /\s+\{[^{}]*\}\s*$/.exec(b.text);
618
+ const runs = attrs
619
+ ? [...inline(b.text.slice(0, attrs.index), true), { text: attrs[0].trimEnd(), raw: true }]
620
+ : inline(b.text, true);
621
+ body += paragraphXml(styles.heading(b.level, b.style), runsXml(runs, ctx));
622
+ break;
623
+ }
624
+ case 'para': {
625
+ const runs = inline(b.text);
626
+ citeNotes(runs);
627
+ body += paragraphXml(roleStyle(b.role, styles), runsXml(runs, ctx));
628
+ break;
629
+ }
630
+ case 'list': {
631
+ const runs = inline(b.text);
632
+ citeNotes(runs);
633
+ numbering.touch(b.run, b.depth);
634
+ const numId = b.ordered ? numbering.ordered(b.run, b.depth, b.number) : numbering.bullet();
635
+ const style = b.role.kind === 'body' ? 'ListParagraph' : roleStyle(b.role, styles);
636
+ body += paragraphXml(style, runsXml(runs, ctx), `<w:numPr><w:ilvl w:val="${b.depth - 1}"/><w:numId w:val="${numId}"/></w:numPr>`);
637
+ break;
638
+ }
639
+ default:
640
+ break;
641
+ }
642
+ }
643
+ });
644
+ if (!body)
645
+ body = paragraphXml('Normal', '');
646
+ const template = {
647
+ ...settings.template,
648
+ paragraphs: { ...settings.template.paragraphs, ...styles.paragraphs },
649
+ characters: { ...settings.template.characters, ...styles.characters },
650
+ };
651
+ const sect = '<w:sectPr><w:footnotePr><w:numFmt w:val="decimal"/></w:footnotePr><w:pgSz w:w="11906" w:h="16838"/><w:pgMar w:top="1418" w:right="1418" w:bottom="1418" w:left="1418" w:header="709" w:footer="709" w:gutter="0"/></w:sectPr>';
652
+ const documentXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document ${W_NS}><w:body>${body}${sect}</w:body></w:document>`;
653
+ const lang = settings.config.locale ?? 'en';
654
+ const stylesXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:styles ${W_NS}><w:docDefaults><w:rPrDefault><w:rPr><w:rFonts w:ascii="Georgia" w:hAnsi="Georgia" w:eastAsia="Georgia" w:cs="Times New Roman"/><w:sz w:val="22"/><w:lang w:val="${escapeAttr(lang)}"/></w:rPr></w:rPrDefault><w:pPrDefault/></w:docDefaults>${styles.xml()}</w:styles>`;
655
+ const footnotesXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:footnotes ${W_NS}><w:footnote w:type="separator" w:id="-1"><w:p><w:pPr><w:spacing w:after="0" w:line="240" w:lineRule="auto"/></w:pPr><w:r><w:separator/></w:r></w:p></w:footnote><w:footnote w:type="continuationSeparator" w:id="0"><w:p><w:pPr><w:spacing w:after="0" w:line="240" w:lineRule="auto"/></w:pPr><w:r><w:continuationSeparator/></w:r></w:p></w:footnote>${footnotes.join('')}</w:footnotes>`;
656
+ const settingsXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:settings ${W_NS}><w:defaultTabStop w:val="709"/><w:footnotePr><w:footnote w:id="-1"/><w:footnote w:id="0"/></w:footnotePr><w:compat><w:compatSetting w:name="compatibilityMode" w:uri="http://schemas.microsoft.com/office/word" w:val="15"/></w:compat></w:settings>`;
657
+ const linkRels = [...links].map(([url, id]) => `<Relationship Id="${id}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" Target="${escapeAttr(url)}" TargetMode="External"/>`).join('');
658
+ const docRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rIdStyles" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/><Relationship Id="rIdNumbering" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" Target="numbering.xml"/><Relationship Id="rIdFootnotes" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes" Target="footnotes.xml"/><Relationship Id="rIdSettings" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" Target="settings.xml"/><Relationship Id="rIdTemplate" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml" Target="../customXml/item1.xml"/>${linkRels}</Relationships>`;
659
+ const now = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
660
+ const core = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">${settings.title ? `<dc:title>${escapeXml(settings.title)}</dc:title>` : ''}${settings.author ? `<dc:creator>${escapeXml(settings.author)}</dc:creator>` : ''}<dcterms:created xsi:type="dcterms:W3CDTF">${now}</dcterms:created><dcterms:modified xsi:type="dcterms:W3CDTF">${now}</dcterms:modified></cp:coreProperties>`;
661
+ const app = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"><Application>Postext</Application></Properties>';
662
+ const templateJson = JSON.stringify({ version: 1, ...template, id: '', updatedAt: 0 });
663
+ const item = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><${TEMPLATE_XML_ROOT} xmlns="${TEMPLATE_XML_NS}">${escapeXml(templateJson)}</${TEMPLATE_XML_ROOT}>`;
664
+ const itemProps = `<?xml version="1.0" encoding="UTF-8" standalone="no"?><ds:datastoreItem ds:itemID="{6E0D2B5A-3F1C-4C8B-9A2D-5F0E7A1B9C30}" xmlns:ds="http://schemas.openxmlformats.org/officeDocument/2006/customXml"><ds:schemaRefs><ds:schemaRef ds:uri="${TEMPLATE_XML_NS}"/></ds:schemaRefs></ds:datastoreItem>`;
665
+ const itemRels = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXmlProps" Target="itemProps1.xml"/></Relationships>';
666
+ const contentTypes = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/><Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/><Override PartName="/word/numbering.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"/><Override PartName="/word/footnotes.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml"/><Override PartName="/word/settings.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml"/><Override PartName="/customXml/itemProps1.xml" ContentType="application/vnd.openxmlformats-officedocument.customXmlProperties+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/></Types>';
667
+ const pkgRels = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/><Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/></Relationships>';
668
+ return zipSync({
669
+ '[Content_Types].xml': strToU8(contentTypes),
670
+ '_rels/.rels': strToU8(pkgRels),
671
+ 'docProps/core.xml': strToU8(core),
672
+ 'docProps/app.xml': strToU8(app),
673
+ 'word/document.xml': strToU8(documentXml),
674
+ 'word/styles.xml': strToU8(stylesXml),
675
+ 'word/numbering.xml': strToU8(numbering.xml()),
676
+ 'word/footnotes.xml': strToU8(footnotesXml),
677
+ 'word/settings.xml': strToU8(settingsXml),
678
+ 'word/_rels/document.xml.rels': strToU8(docRels),
679
+ 'customXml/item1.xml': strToU8(item),
680
+ 'customXml/itemProps1.xml': strToU8(itemProps),
681
+ 'customXml/_rels/item1.xml.rels': strToU8(itemRels),
682
+ }, { level: 6 });
683
+ }
684
+ //# sourceMappingURL=toDocx.js.map