postext 1.22.1 → 1.22.2
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/dist/__tests__/exports.test.js +4 -0
- package/dist/__tests__/exports.test.js.map +1 -1
- package/dist/comics/lettering/__tests__/panelCache.test.d.ts +2 -0
- package/dist/comics/lettering/__tests__/panelCache.test.d.ts.map +1 -0
- package/dist/comics/lettering/__tests__/panelCache.test.js +43 -0
- package/dist/comics/lettering/__tests__/panelCache.test.js.map +1 -0
- package/dist/comics/lettering/letter.d.ts.map +1 -1
- package/dist/comics/lettering/letter.js +73 -0
- package/dist/comics/lettering/letter.js.map +1 -1
- package/dist/comics/lettering/shape-text.d.ts.map +1 -1
- package/dist/comics/lettering/shape-text.js +64 -16
- package/dist/comics/lettering/shape-text.js.map +1 -1
- package/dist/fonts/usage.d.ts +46 -0
- package/dist/fonts/usage.d.ts.map +1 -0
- package/dist/fonts/usage.js +205 -0
- package/dist/fonts/usage.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/word/analyze.d.ts +33 -0
- package/dist/word/analyze.d.ts.map +1 -0
- package/dist/word/analyze.js +167 -0
- package/dist/word/analyze.js.map +1 -0
- package/dist/word/docxRead.d.ts +10 -0
- package/dist/word/docxRead.d.ts.map +1 -0
- package/dist/word/docxRead.js +584 -0
- package/dist/word/docxRead.js.map +1 -0
- package/dist/word/index.d.ts +14 -0
- package/dist/word/index.d.ts.map +1 -0
- package/dist/word/index.js +12 -0
- package/dist/word/index.js.map +1 -0
- package/dist/word/inline.d.ts +48 -0
- package/dist/word/inline.d.ts.map +1 -0
- package/dist/word/inline.js +606 -0
- package/dist/word/inline.js.map +1 -0
- package/dist/word/model.d.ts +141 -0
- package/dist/word/model.d.ts.map +1 -0
- package/dist/word/model.js +23 -0
- package/dist/word/model.js.map +1 -0
- package/dist/word/omml.d.ts +4 -0
- package/dist/word/omml.d.ts.map +1 -0
- package/dist/word/omml.js +139 -0
- package/dist/word/omml.js.map +1 -0
- package/dist/word/template.d.ts +111 -0
- package/dist/word/template.d.ts.map +1 -0
- package/dist/word/template.js +255 -0
- package/dist/word/template.js.map +1 -0
- package/dist/word/toDocx.d.ts +28 -0
- package/dist/word/toDocx.d.ts.map +1 -0
- package/dist/word/toDocx.js +684 -0
- package/dist/word/toDocx.js.map +1 -0
- package/dist/word/toMarkdown.d.ts +53 -0
- package/dist/word/toMarkdown.d.ts.map +1 -0
- package/dist/word/toMarkdown.js +632 -0
- package/dist/word/toMarkdown.js.map +1 -0
- package/dist/word/word.test.d.ts +2 -0
- package/dist/word/word.test.d.ts.map +1 -0
- package/dist/word/word.test.js +302 -0
- package/dist/word/word.test.js.map +1 -0
- package/dist/word/xml.d.ts +30 -0
- package/dist/word/xml.d.ts.map +1 -0
- package/dist/word/xml.js +127 -0
- package/dist/word/xml.js.map +1 -0
- package/package.json +5 -1
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
// Word model → Postext chapters. Each paragraph style becomes what the
|
|
2
|
+
// template says (heading, paragraph style, callout, quote, caption…), runs
|
|
3
|
+
// become inline marks, Word lists become Postext lists, notes become
|
|
4
|
+
// footnotes, pictures and tables become resources placed where they were.
|
|
5
|
+
import { guardLineStart, renderInline, WORD_JOINER } from './inline';
|
|
6
|
+
import { guessCharacterTarget, guessParagraphTarget, lookup, } from './template';
|
|
7
|
+
/** The target of a paragraph style: the template's, else a guess. */
|
|
8
|
+
export function paragraphTargetOf(doc, styleId, template, config) {
|
|
9
|
+
const id = styleId || doc.defaultParagraphStyle;
|
|
10
|
+
const style = doc.styles.get(id);
|
|
11
|
+
const name = style?.name ?? (id || 'Normal');
|
|
12
|
+
return lookup(template.paragraphs, name) ?? guessParagraphTarget(name, config, doc, style);
|
|
13
|
+
}
|
|
14
|
+
/** The target of a character style: the template's, else a guess. */
|
|
15
|
+
export function characterTargetOf(doc, name, template, config) {
|
|
16
|
+
const style = [...doc.styles.values()].find((s) => s.type === 'character' && s.name === name);
|
|
17
|
+
return lookup(template.characters, name) ?? guessCharacterTarget(name, config, doc, style);
|
|
18
|
+
}
|
|
19
|
+
const CAPTION_LABEL_RE = /^\s*(?:fig(?:ure|ura|\.)?|figs?\.|table|tabla|tab\.|taula|tableau|tabelle|tabella|plate|lámina|làmina|chart|gráfico|map|mapa|image|imagen|imatge|ilustración|il·lustració|illustration|esquema|diagram|diagrama|图|表|図|شكل|جدول)\s*[0-9IVXLC٠-٩]+(?:[.\-–][0-9٠-٩]+)*\s*[.:\-–—]?\s*/iu;
|
|
20
|
+
/** A caption without its typed label (Postext numbers resources). */
|
|
21
|
+
export function stripCaptionLabel(text) {
|
|
22
|
+
return text.replace(CAPTION_LABEL_RE, '');
|
|
23
|
+
}
|
|
24
|
+
const attrValue = (v) => `"${v.replace(/"/g, '”').replace(/[{}]/g, '').replace(/\s+/g, ' ').trim()}"`;
|
|
25
|
+
function slugify(text, fallback) {
|
|
26
|
+
const s = text.normalize('NFKD').replace(/[̀-ͯ]/g, '').toLowerCase()
|
|
27
|
+
.replace(/\.[a-z0-9]{2,4}$/, '')
|
|
28
|
+
.replace(/[^\p{L}\p{N}]+/gu, '-').replace(/^-+|-+$/g, '')
|
|
29
|
+
.split('-').slice(0, 4).join('-').slice(0, 40).replace(/-+$/, '');
|
|
30
|
+
return s || fallback;
|
|
31
|
+
}
|
|
32
|
+
function joinChunks(chunks) {
|
|
33
|
+
let out = '';
|
|
34
|
+
chunks.forEach((c, i) => {
|
|
35
|
+
if (i > 0) {
|
|
36
|
+
const prev = chunks[i - 1];
|
|
37
|
+
out += (prev.kind === c.kind && c.kind !== 'block') ? '\n' : '\n\n';
|
|
38
|
+
}
|
|
39
|
+
out += c.text;
|
|
40
|
+
});
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/** Container fences a markup paragraph opens (`:::callout{…}`); a bare
|
|
44
|
+
* `:::` closes the innermost. Inside one, paragraph styles mapped to
|
|
45
|
+
* callouts and paragraph styles do not open boxes of their own. */
|
|
46
|
+
const FENCE_OPEN_RE = /^:::\s*(callout|paragraphs|part|columns|paper|verse|references|page|strip)\b/;
|
|
47
|
+
const FENCE_CLOSE_RE = /^:::\s*$/;
|
|
48
|
+
class Converter {
|
|
49
|
+
doc;
|
|
50
|
+
s;
|
|
51
|
+
chapters = [];
|
|
52
|
+
chunks = [];
|
|
53
|
+
notes = [];
|
|
54
|
+
title;
|
|
55
|
+
group = null;
|
|
56
|
+
explicitDepth = 0;
|
|
57
|
+
list = [];
|
|
58
|
+
counters = new Map();
|
|
59
|
+
noteSeq = 0;
|
|
60
|
+
pendingCaption;
|
|
61
|
+
lastResource = null;
|
|
62
|
+
usedIds;
|
|
63
|
+
hasChapterMarkers;
|
|
64
|
+
pictures = [];
|
|
65
|
+
tables = [];
|
|
66
|
+
skippedPictures = [];
|
|
67
|
+
droppedHeadingNotes = 0;
|
|
68
|
+
/** Tidy style boundaries (spaces between two bold words, a style change
|
|
69
|
+
* inside a word) in documents from Word; a Sandbox export keeps its runs
|
|
70
|
+
* exactly as they were written. */
|
|
71
|
+
tidy;
|
|
72
|
+
constructor(doc, s) {
|
|
73
|
+
this.doc = doc;
|
|
74
|
+
this.s = s;
|
|
75
|
+
this.tidy = doc.embeddedTemplate === undefined;
|
|
76
|
+
this.usedIds = new Set(s.existingIds);
|
|
77
|
+
this.hasChapterMarkers = doc.blocks.some((b) => b.type === 'paragraph' && this.target(b).kind === 'chapter');
|
|
78
|
+
}
|
|
79
|
+
target(p) {
|
|
80
|
+
return paragraphTargetOf(this.doc, p.styleId, this.s.template, this.s.config);
|
|
81
|
+
}
|
|
82
|
+
uniqueId(base) {
|
|
83
|
+
// `table-1` taken: `table-2`, not `table-1-2`.
|
|
84
|
+
const numbered = /^(.*-)(\d+)$/.exec(base);
|
|
85
|
+
let id = base;
|
|
86
|
+
for (let n = 2; this.usedIds.has(id); n++)
|
|
87
|
+
id = numbered ? `${numbered[1]}${Number(numbered[2]) + n - 1}` : `${base}-${n}`;
|
|
88
|
+
this.usedIds.add(id);
|
|
89
|
+
return id;
|
|
90
|
+
}
|
|
91
|
+
render(runs) {
|
|
92
|
+
return renderInline(runs, { tidy: this.tidy }).trim();
|
|
93
|
+
}
|
|
94
|
+
// -- output ---------------------------------------------------------------
|
|
95
|
+
out(chunk) {
|
|
96
|
+
if (chunk.kind !== 'list')
|
|
97
|
+
this.list = [];
|
|
98
|
+
(this.group ? this.group.chunks : this.chunks).push(chunk);
|
|
99
|
+
}
|
|
100
|
+
closeGroup() {
|
|
101
|
+
const g = this.group;
|
|
102
|
+
if (!g)
|
|
103
|
+
return;
|
|
104
|
+
this.group = null;
|
|
105
|
+
if (g.chunks.length === 0 && !g.title)
|
|
106
|
+
return;
|
|
107
|
+
const attrs = g.kind === 'callout'
|
|
108
|
+
? `{type=${attrValue(g.key)}${g.title ? ` title=${attrValue(g.title)}` : ''}}`
|
|
109
|
+
: `{style=${attrValue(g.key)}}`;
|
|
110
|
+
const inner = joinChunks(g.chunks);
|
|
111
|
+
this.chunks.push({ kind: 'block', text: `:::${g.kind}${attrs}${inner ? `\n${inner}` : ''}\n:::` });
|
|
112
|
+
this.list = [];
|
|
113
|
+
}
|
|
114
|
+
openGroup(kind, key) {
|
|
115
|
+
if (this.group && this.group.kind === kind && this.group.key === key)
|
|
116
|
+
return;
|
|
117
|
+
this.closeGroup();
|
|
118
|
+
this.group = { kind, key, chunks: [] };
|
|
119
|
+
}
|
|
120
|
+
flushChapter(nextTitle) {
|
|
121
|
+
this.closeGroup();
|
|
122
|
+
this.flushCaption();
|
|
123
|
+
const body = joinChunks(this.chunks);
|
|
124
|
+
const notes = this.notes.join('\n\n');
|
|
125
|
+
const markdown = [body, notes].filter(Boolean).join('\n\n');
|
|
126
|
+
if (markdown.trim() || this.title !== undefined) {
|
|
127
|
+
this.chapters.push({ title: this.title ?? this.s.untitledChapter, markdown: markdown ? markdown + '\n' : '' });
|
|
128
|
+
}
|
|
129
|
+
this.chunks = [];
|
|
130
|
+
this.notes = [];
|
|
131
|
+
this.list = [];
|
|
132
|
+
this.title = nextTitle;
|
|
133
|
+
}
|
|
134
|
+
flushCaption() {
|
|
135
|
+
if (this.pendingCaption === undefined)
|
|
136
|
+
return;
|
|
137
|
+
const text = this.pendingCaption;
|
|
138
|
+
this.pendingCaption = undefined;
|
|
139
|
+
this.out({ kind: 'block', text: guardLineStart(text) });
|
|
140
|
+
}
|
|
141
|
+
// -- runs -----------------------------------------------------------------
|
|
142
|
+
charTarget(name) {
|
|
143
|
+
return characterTargetOf(this.doc, name, this.s.template, this.s.config);
|
|
144
|
+
}
|
|
145
|
+
/** Runs → inline runs, split at line breaks when `split` (verse typed
|
|
146
|
+
* with Shift+Enter) and at page breaks. Notes are numbered here. */
|
|
147
|
+
inlineRuns(runs, opts) {
|
|
148
|
+
const lines = [[]];
|
|
149
|
+
const images = [];
|
|
150
|
+
const displayMath = [];
|
|
151
|
+
let pageBreak = false;
|
|
152
|
+
const keepDirect = this.s.template.options.directFormatting === 'keep';
|
|
153
|
+
const cur = () => lines[lines.length - 1];
|
|
154
|
+
for (const r of runs) {
|
|
155
|
+
switch (r.type) {
|
|
156
|
+
case 'text': {
|
|
157
|
+
if (r.hidden)
|
|
158
|
+
break;
|
|
159
|
+
const t = this.textRun(r, keepDirect, !!opts.heading);
|
|
160
|
+
if (t)
|
|
161
|
+
cur().push(t);
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
case 'tab':
|
|
165
|
+
cur().push({ text: ' ' });
|
|
166
|
+
break;
|
|
167
|
+
case 'break':
|
|
168
|
+
if (r.kind === 'page')
|
|
169
|
+
pageBreak = true;
|
|
170
|
+
if (r.kind === 'line' && opts.heading)
|
|
171
|
+
cur().push({ text: ' \\\\ ', raw: true });
|
|
172
|
+
else if (r.kind === 'line' && opts.splitLines)
|
|
173
|
+
lines.push([]);
|
|
174
|
+
else if (r.kind === 'line' && opts.inCell && !opts.inNote)
|
|
175
|
+
cur().push({ text: '\n', raw: true });
|
|
176
|
+
else
|
|
177
|
+
cur().push({ text: ' ' });
|
|
178
|
+
break;
|
|
179
|
+
case 'note': {
|
|
180
|
+
if (opts.heading || opts.inCell || opts.inNote) {
|
|
181
|
+
this.droppedHeadingNotes++;
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
const blocks = (r.kind === 'footnote' ? this.doc.footnotes : this.doc.endnotes).get(r.id);
|
|
185
|
+
if (!blocks)
|
|
186
|
+
break;
|
|
187
|
+
const id = String(++this.noteSeq);
|
|
188
|
+
const text = blocks
|
|
189
|
+
.filter((b) => b.type === 'paragraph')
|
|
190
|
+
.map((b) => this.renderRuns(b.runs, { inNote: true }))
|
|
191
|
+
.filter(Boolean)
|
|
192
|
+
.join(' ');
|
|
193
|
+
this.notes.push(`[^${id}]: ${text}`);
|
|
194
|
+
cur().push({ text: '', note: id });
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
case 'math':
|
|
198
|
+
if (r.display && !opts.inCell && !opts.inNote)
|
|
199
|
+
displayMath.push(r.tex);
|
|
200
|
+
else
|
|
201
|
+
cur().push({ text: `$${r.tex}$`, raw: true });
|
|
202
|
+
break;
|
|
203
|
+
case 'image':
|
|
204
|
+
images.push(r);
|
|
205
|
+
break;
|
|
206
|
+
case 'index': {
|
|
207
|
+
if (opts.inCell)
|
|
208
|
+
break;
|
|
209
|
+
const term = r.term.map((t) => t.replace(/!/g, '')).join('!');
|
|
210
|
+
let a = `term=${attrValue(term)}`;
|
|
211
|
+
if (r.main)
|
|
212
|
+
a += ' main';
|
|
213
|
+
if (r.see)
|
|
214
|
+
a += ` see=${attrValue(r.see)}`;
|
|
215
|
+
cur().push({ text: `:index{${a}}`, raw: true });
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return { lines, images, pageBreak, displayMath };
|
|
221
|
+
}
|
|
222
|
+
textRun(r, keepDirect, heading) {
|
|
223
|
+
const t = { text: r.text.replace(/[]/g, '') };
|
|
224
|
+
if (!t.text)
|
|
225
|
+
return null;
|
|
226
|
+
if (r.href !== undefined && !heading)
|
|
227
|
+
t.href = r.href;
|
|
228
|
+
if (keepDirect) {
|
|
229
|
+
if (r.bold)
|
|
230
|
+
t.bold = true;
|
|
231
|
+
if (r.italic)
|
|
232
|
+
t.italic = true;
|
|
233
|
+
if (r.smallCaps)
|
|
234
|
+
t.smallCaps = true;
|
|
235
|
+
if (r.script)
|
|
236
|
+
t.script = r.script;
|
|
237
|
+
}
|
|
238
|
+
if (r.charStyle) {
|
|
239
|
+
const target = this.charTarget(r.charStyle);
|
|
240
|
+
switch (target.kind) {
|
|
241
|
+
case 'drop':
|
|
242
|
+
return null;
|
|
243
|
+
case 'markup': {
|
|
244
|
+
// Verbatim, inside the emphasis and link around it.
|
|
245
|
+
const raw = { text: r.text, raw: true };
|
|
246
|
+
if (t.bold && r.bold !== false)
|
|
247
|
+
raw.bold = true;
|
|
248
|
+
if (t.italic && r.italic !== false)
|
|
249
|
+
raw.italic = true;
|
|
250
|
+
if (t.smallCaps)
|
|
251
|
+
raw.smallCaps = true;
|
|
252
|
+
if (t.href !== undefined)
|
|
253
|
+
raw.href = t.href;
|
|
254
|
+
return raw;
|
|
255
|
+
}
|
|
256
|
+
case 'bold':
|
|
257
|
+
if (r.bold !== false)
|
|
258
|
+
t.bold = true;
|
|
259
|
+
break;
|
|
260
|
+
case 'italic':
|
|
261
|
+
if (r.italic !== false)
|
|
262
|
+
t.italic = true;
|
|
263
|
+
break;
|
|
264
|
+
case 'boldItalic':
|
|
265
|
+
if (r.bold !== false)
|
|
266
|
+
t.bold = true;
|
|
267
|
+
if (r.italic !== false)
|
|
268
|
+
t.italic = true;
|
|
269
|
+
break;
|
|
270
|
+
case 'smallCaps':
|
|
271
|
+
if (r.smallCaps !== false)
|
|
272
|
+
t.smallCaps = true;
|
|
273
|
+
break;
|
|
274
|
+
case 'sup':
|
|
275
|
+
case 'sub':
|
|
276
|
+
t.script = target.kind;
|
|
277
|
+
break;
|
|
278
|
+
case 'chip':
|
|
279
|
+
if (!heading)
|
|
280
|
+
t.chip = target.style;
|
|
281
|
+
break;
|
|
282
|
+
default:
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (r.bold === false)
|
|
287
|
+
delete t.bold;
|
|
288
|
+
if (r.italic === false)
|
|
289
|
+
delete t.italic;
|
|
290
|
+
return t;
|
|
291
|
+
}
|
|
292
|
+
renderRuns(runs, opts = {}) {
|
|
293
|
+
const { lines } = this.inlineRuns(runs, { inNote: !!opts.inNote });
|
|
294
|
+
return lines.map((l) => this.render(l)).filter(Boolean).join(' ');
|
|
295
|
+
}
|
|
296
|
+
// -- blocks ---------------------------------------------------------------
|
|
297
|
+
emitImages(images) {
|
|
298
|
+
if (!this.s.template.options.images)
|
|
299
|
+
return;
|
|
300
|
+
for (const img of images) {
|
|
301
|
+
const media = this.doc.media.get(img.rId);
|
|
302
|
+
if (!media)
|
|
303
|
+
continue;
|
|
304
|
+
if (!/^image\/(png|jpeg|gif|webp|svg\+xml)$/.test(media.contentType)) {
|
|
305
|
+
this.skippedPictures.push(media.path.split('/').pop() ?? media.path);
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const base = slugify(img.alt ?? '', '') || slugify(img.name ?? '', '') || 'figure';
|
|
309
|
+
const id = this.uniqueId(/^(picture|imagen|image|figure)\b/i.test(base) || !/[a-z]/.test(base) ? `fig-${this.pictures.length + 1}` : base);
|
|
310
|
+
const pic = { id, media, ...(img.name ? { name: img.name } : {}), ...(img.alt ? { alt: img.alt } : {}) };
|
|
311
|
+
if (this.pendingCaption !== undefined) {
|
|
312
|
+
pic.caption = this.pendingCaption;
|
|
313
|
+
this.pendingCaption = undefined;
|
|
314
|
+
}
|
|
315
|
+
this.pictures.push(pic);
|
|
316
|
+
this.lastResource = pic;
|
|
317
|
+
this.out({ kind: 'block', text: `::resource{id="${id}"}` });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
emitMarkup(p) {
|
|
321
|
+
let text = '';
|
|
322
|
+
for (const r of p.runs) {
|
|
323
|
+
if (r.type === 'text' && !r.hidden)
|
|
324
|
+
text += r.text;
|
|
325
|
+
else if (r.type === 'tab')
|
|
326
|
+
text += '\t';
|
|
327
|
+
else if (r.type === 'break' && r.kind === 'line')
|
|
328
|
+
text += '\n';
|
|
329
|
+
}
|
|
330
|
+
this.closeGroup();
|
|
331
|
+
this.flushCaption();
|
|
332
|
+
for (const line of text.split('\n')) {
|
|
333
|
+
const t = line.replace(/\s+$/, '');
|
|
334
|
+
if (FENCE_OPEN_RE.test(t.trim()))
|
|
335
|
+
this.explicitDepth++;
|
|
336
|
+
else if (FENCE_CLOSE_RE.test(t.trim()) && this.explicitDepth > 0)
|
|
337
|
+
this.explicitDepth--;
|
|
338
|
+
}
|
|
339
|
+
this.out({ kind: 'markup', text: text.replace(/\s+$/, '') });
|
|
340
|
+
}
|
|
341
|
+
listMarker(p) {
|
|
342
|
+
const { numId, ilvl } = p.list;
|
|
343
|
+
const num = this.doc.numbering.nums.get(numId);
|
|
344
|
+
const level = num ? this.doc.numbering.abstracts.get(num.abstractId)?.get(ilvl) : undefined;
|
|
345
|
+
const ordered = !!level && level.format !== 'bullet' && level.format !== 'none';
|
|
346
|
+
let marker = '- ';
|
|
347
|
+
if (ordered) {
|
|
348
|
+
const k = `${numId}:${ilvl}`;
|
|
349
|
+
const start = num?.starts.get(ilvl) ?? level?.start ?? 1;
|
|
350
|
+
const n = this.counters.has(k) ? this.counters.get(k) + 1 : start;
|
|
351
|
+
this.counters.set(k, n);
|
|
352
|
+
for (const key of [...this.counters.keys()]) {
|
|
353
|
+
const [id, l] = key.split(':');
|
|
354
|
+
if (id === numId && Number(l) > ilvl)
|
|
355
|
+
this.counters.delete(key);
|
|
356
|
+
}
|
|
357
|
+
marker = `${n}. `;
|
|
358
|
+
}
|
|
359
|
+
while (this.list.length && this.list[this.list.length - 1].ilvl >= ilvl)
|
|
360
|
+
this.list.pop();
|
|
361
|
+
const parent = this.list[this.list.length - 1];
|
|
362
|
+
const indent = parent ? parent.indent + parent.width : 0;
|
|
363
|
+
this.list.push({ ilvl, indent, width: marker.length });
|
|
364
|
+
return { indent: ' '.repeat(indent), marker };
|
|
365
|
+
}
|
|
366
|
+
isManualHeading(p, text) {
|
|
367
|
+
if (!this.s.template.options.manualHeadings || p.list)
|
|
368
|
+
return false;
|
|
369
|
+
const runs = p.runs.filter((r) => r.type === 'text' && r.text.trim() !== '');
|
|
370
|
+
if (!runs.length)
|
|
371
|
+
return false;
|
|
372
|
+
const t = text.trim();
|
|
373
|
+
if (t.length > 120 || /[.:;,]$/.test(t))
|
|
374
|
+
return false;
|
|
375
|
+
return runs.every((r) => r.bold || (r.charStyle && ['bold', 'boldItalic'].includes(this.charTarget(r.charStyle).kind)));
|
|
376
|
+
}
|
|
377
|
+
paragraph(p) {
|
|
378
|
+
let target = this.target(p);
|
|
379
|
+
const plain = p.runs.map((r) => (r.type === 'text' && !r.hidden ? r.text : r.type === 'tab' || r.type === 'break' ? ' ' : '')).join('').replace(/\s+/g, ' ').trim();
|
|
380
|
+
if (target.kind === 'drop')
|
|
381
|
+
return;
|
|
382
|
+
if (target.kind === 'markup') {
|
|
383
|
+
this.emitMarkup(p);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (target.kind === 'chapter') {
|
|
387
|
+
if (this.s.chapters === 'split')
|
|
388
|
+
this.flushChapter(plain || this.s.untitledChapter);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (p.pageBreakBefore && this.s.template.options.pageBreaks === 'keep' && !(target.kind === 'heading' && target.level === 1)) {
|
|
392
|
+
this.closeGroup();
|
|
393
|
+
this.out({ kind: 'block', text: ':::pagebreak' });
|
|
394
|
+
}
|
|
395
|
+
let manualHeading = false;
|
|
396
|
+
if (target.kind === 'body' && this.isManualHeading(p, plain)) {
|
|
397
|
+
target = { kind: 'heading', level: this.s.template.options.manualHeadings };
|
|
398
|
+
manualHeading = true;
|
|
399
|
+
}
|
|
400
|
+
if (this.explicitDepth > 0 && (target.kind === 'callout' || target.kind === 'paragraphs' || target.kind === 'calloutTitle')) {
|
|
401
|
+
target = target.kind === 'calloutTitle' ? target : { kind: 'body' };
|
|
402
|
+
}
|
|
403
|
+
if (target.kind === 'heading') {
|
|
404
|
+
const level = Math.min(6, Math.max(1, target.level));
|
|
405
|
+
if (level === 1 && this.s.chapters === 'split' && !this.hasChapterMarkers)
|
|
406
|
+
this.flushChapter(plain || this.s.untitledChapter);
|
|
407
|
+
else {
|
|
408
|
+
this.closeGroup();
|
|
409
|
+
this.flushCaption();
|
|
410
|
+
}
|
|
411
|
+
const { lines } = this.inlineRuns(p.runs, { heading: true });
|
|
412
|
+
// A heading typed in bold by hand: the bold was its only sign.
|
|
413
|
+
let text = this.render(manualHeading ? lines.flat().map((x) => ({ ...x, bold: undefined })) : lines.flat());
|
|
414
|
+
if (!text)
|
|
415
|
+
return;
|
|
416
|
+
if (target.style) {
|
|
417
|
+
const attrs = /\s\{([^{}]*)\}$/.exec(text);
|
|
418
|
+
if (attrs && !/\bstyle\s*=/.test(attrs[1]))
|
|
419
|
+
text = text.slice(0, attrs.index) + ` {style=${attrValue(target.style)} ${attrs[1].trim()}}`;
|
|
420
|
+
else if (!attrs)
|
|
421
|
+
text += ` {style=${attrValue(target.style)}}`;
|
|
422
|
+
}
|
|
423
|
+
else if (text.endsWith('}') && !/\s\{[^{}]*\}$/.test(text))
|
|
424
|
+
text += WORD_JOINER;
|
|
425
|
+
this.out({ kind: 'block', text: `${'#'.repeat(level)} ${text}` });
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const splitLines = this.s.template.options.lineBreaks === 'paragraph' && !p.list;
|
|
429
|
+
const { lines, images, pageBreak, displayMath } = this.inlineRuns(p.runs, { splitLines });
|
|
430
|
+
const rendered = lines.map((l) => this.render(l)).filter(Boolean);
|
|
431
|
+
if (target.kind === 'caption') {
|
|
432
|
+
const text = stripCaptionLabel(rendered.join(' '));
|
|
433
|
+
if (images.length === 0 && text) {
|
|
434
|
+
if (this.lastResource && !this.lastResource.caption && this.lastChunkIsResource()) {
|
|
435
|
+
this.lastResource.caption = text;
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
this.flushCaption();
|
|
439
|
+
this.pendingCaption = text;
|
|
440
|
+
}
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (target.kind === 'calloutTitle') {
|
|
445
|
+
if (this.explicitDepth > 0) {
|
|
446
|
+
// Inside a fence the box is already open: the title is a paragraph.
|
|
447
|
+
target = { kind: 'body' };
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
this.closeGroup();
|
|
451
|
+
this.flushCaption();
|
|
452
|
+
// A callout title is plain text.
|
|
453
|
+
this.group = { kind: 'callout', key: target.type, title: plain || undefined, chunks: [] };
|
|
454
|
+
this.emitImages(images);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (target.kind === 'callout') {
|
|
459
|
+
if (!(this.group?.kind === 'callout' && this.group.key === target.type))
|
|
460
|
+
this.openGroup('callout', target.type);
|
|
461
|
+
}
|
|
462
|
+
else if (target.kind === 'paragraphs') {
|
|
463
|
+
this.openGroup('paragraphs', target.style);
|
|
464
|
+
}
|
|
465
|
+
else if (this.group) {
|
|
466
|
+
this.closeGroup();
|
|
467
|
+
}
|
|
468
|
+
this.flushCaption();
|
|
469
|
+
if (rendered.length) {
|
|
470
|
+
if (p.list) {
|
|
471
|
+
const { indent, marker } = this.listMarker(p);
|
|
472
|
+
this.out({ kind: 'list', text: indent + marker + rendered.join(' ') });
|
|
473
|
+
}
|
|
474
|
+
else if (target.kind === 'quote') {
|
|
475
|
+
for (const line of rendered)
|
|
476
|
+
this.out({ kind: 'block', text: `> ${line}` });
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
for (const line of rendered)
|
|
480
|
+
this.out({ kind: 'block', text: guardLineStart(line) });
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
for (const tex of displayMath)
|
|
484
|
+
this.out({ kind: 'block', text: `$$${tex}$$` });
|
|
485
|
+
this.emitImages(images);
|
|
486
|
+
if (pageBreak && this.s.template.options.pageBreaks === 'keep') {
|
|
487
|
+
this.closeGroup();
|
|
488
|
+
this.out({ kind: 'block', text: ':::pagebreak' });
|
|
489
|
+
}
|
|
490
|
+
for (const box of p.textBoxes ?? [])
|
|
491
|
+
this.block(box);
|
|
492
|
+
}
|
|
493
|
+
lastChunkIsResource() {
|
|
494
|
+
const list = this.group ? this.group.chunks : this.chunks;
|
|
495
|
+
const last = list[list.length - 1];
|
|
496
|
+
return !!last && last.text.startsWith('::resource{');
|
|
497
|
+
}
|
|
498
|
+
table(t) {
|
|
499
|
+
if (!this.s.template.options.tables) {
|
|
500
|
+
for (const row of t.rows)
|
|
501
|
+
for (const cell of row)
|
|
502
|
+
for (const b of cell.blocks)
|
|
503
|
+
this.block(b);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
this.closeGroup();
|
|
507
|
+
const model = tableModel(t, (p) => this.cellText(p));
|
|
508
|
+
const id = this.uniqueId(`table-${this.tables.length + 1}`);
|
|
509
|
+
const table = { id, model };
|
|
510
|
+
if (this.pendingCaption !== undefined) {
|
|
511
|
+
table.caption = this.pendingCaption;
|
|
512
|
+
this.pendingCaption = undefined;
|
|
513
|
+
}
|
|
514
|
+
this.tables.push(table);
|
|
515
|
+
this.lastResource = table;
|
|
516
|
+
this.out({ kind: 'block', text: `::resource{id="${id}"}` });
|
|
517
|
+
}
|
|
518
|
+
cellText(blocks) {
|
|
519
|
+
const lines = [];
|
|
520
|
+
let depthBase = null;
|
|
521
|
+
for (const b of blocks) {
|
|
522
|
+
if (b.type !== 'paragraph')
|
|
523
|
+
continue;
|
|
524
|
+
const { lines: ls } = this.inlineRuns(b.runs, { inCell: true });
|
|
525
|
+
const text = ls.map((l) => renderInline(l, { collapseSpaces: false, tidy: this.tidy }).replace(/[ \t]+/g, ' ').trim()).join(' ');
|
|
526
|
+
if (!text)
|
|
527
|
+
continue;
|
|
528
|
+
if (b.list) {
|
|
529
|
+
const num = this.doc.numbering.nums.get(b.list.numId);
|
|
530
|
+
const level = num ? this.doc.numbering.abstracts.get(num.abstractId)?.get(b.list.ilvl) : undefined;
|
|
531
|
+
if (depthBase === null)
|
|
532
|
+
depthBase = b.list.ilvl;
|
|
533
|
+
const ordered = !!level && level.format !== 'bullet';
|
|
534
|
+
const k = `cell:${b.list.numId}:${b.list.ilvl}`;
|
|
535
|
+
const n = (this.counters.get(k) ?? (level?.start ?? 1) - 1) + 1;
|
|
536
|
+
this.counters.set(k, n);
|
|
537
|
+
lines.push(' '.repeat(Math.max(0, b.list.ilvl - depthBase)) + (ordered ? `${n}. ` : '- ') + text);
|
|
538
|
+
}
|
|
539
|
+
else
|
|
540
|
+
lines.push(text);
|
|
541
|
+
}
|
|
542
|
+
return lines.join('\n');
|
|
543
|
+
}
|
|
544
|
+
block(b) {
|
|
545
|
+
if (b.type === 'table')
|
|
546
|
+
this.table(b);
|
|
547
|
+
else
|
|
548
|
+
this.paragraph(b);
|
|
549
|
+
}
|
|
550
|
+
run() {
|
|
551
|
+
for (const b of this.doc.blocks)
|
|
552
|
+
this.block(b);
|
|
553
|
+
this.flushChapter();
|
|
554
|
+
if (this.chapters.length === 0)
|
|
555
|
+
this.chapters.push({ title: this.s.untitledChapter, markdown: '' });
|
|
556
|
+
return {
|
|
557
|
+
chapters: this.chapters,
|
|
558
|
+
pictures: this.pictures,
|
|
559
|
+
tables: this.tables,
|
|
560
|
+
skippedPictures: this.skippedPictures,
|
|
561
|
+
droppedHeadingNotes: this.droppedHeadingNotes,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
/** A Word table as a Postext table model: merged cells as spans (covered
|
|
566
|
+
* cells point at the cell covering them), the header rows, the column
|
|
567
|
+
* widths as weights and the cell fills. */
|
|
568
|
+
export function tableModel(t, text) {
|
|
569
|
+
// Lay the Word cells on the grid (a gridSpan takes several columns).
|
|
570
|
+
const grid = [];
|
|
571
|
+
let columns = 0;
|
|
572
|
+
t.rows.forEach((row, ri) => {
|
|
573
|
+
const out = [];
|
|
574
|
+
let col = 0;
|
|
575
|
+
for (const c of row) {
|
|
576
|
+
const cell = { content: c.vMerge === 'continue' ? '' : text(c.blocks) };
|
|
577
|
+
if (c.colSpan > 1)
|
|
578
|
+
cell.colSpan = c.colSpan;
|
|
579
|
+
if (c.shading)
|
|
580
|
+
cell.background = { hex: c.shading, model: 'hex' };
|
|
581
|
+
if (ri < t.headerRows)
|
|
582
|
+
cell.isHeader = true;
|
|
583
|
+
out.push({ cell, vMerge: c.vMerge });
|
|
584
|
+
for (let k = 1; k < c.colSpan; k++)
|
|
585
|
+
out.push({ cell: { content: '', hiddenBy: { row: ri, col } } });
|
|
586
|
+
col += c.colSpan;
|
|
587
|
+
}
|
|
588
|
+
columns = Math.max(columns, col);
|
|
589
|
+
grid.push(out);
|
|
590
|
+
});
|
|
591
|
+
// Vertical merges: the restart cell spans the continue cells under it.
|
|
592
|
+
for (let ri = 0; ri < grid.length; ri++) {
|
|
593
|
+
grid[ri].forEach((g, ci) => {
|
|
594
|
+
if (g.vMerge !== 'restart')
|
|
595
|
+
return;
|
|
596
|
+
let span = 1;
|
|
597
|
+
for (let rj = ri + 1; rj < grid.length; rj++) {
|
|
598
|
+
const below = grid[rj][ci];
|
|
599
|
+
if (below?.vMerge !== 'continue')
|
|
600
|
+
break;
|
|
601
|
+
span++;
|
|
602
|
+
below.cell = { content: '', hiddenBy: { row: ri, col: ci } };
|
|
603
|
+
for (let k = 1; k < (g.cell.colSpan ?? 1); k++) {
|
|
604
|
+
const side = grid[rj][ci + k];
|
|
605
|
+
if (side)
|
|
606
|
+
side.cell = { content: '', hiddenBy: { row: ri, col: ci } };
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (span > 1)
|
|
610
|
+
g.cell.rowSpan = span;
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
const rows = grid.map((r) => {
|
|
614
|
+
const cells = r.map((g) => g.cell);
|
|
615
|
+
while (cells.length < columns)
|
|
616
|
+
cells.push({ content: '' });
|
|
617
|
+
return cells;
|
|
618
|
+
});
|
|
619
|
+
const model = { rows };
|
|
620
|
+
if (t.headerRows > 0)
|
|
621
|
+
model.headerRowCount = t.headerRows;
|
|
622
|
+
if (t.widths && t.widths.length === columns) {
|
|
623
|
+
const min = Math.min(...t.widths);
|
|
624
|
+
model.columnWidths = t.widths.map((w) => Math.round((w / min) * 100) / 100);
|
|
625
|
+
}
|
|
626
|
+
return model;
|
|
627
|
+
}
|
|
628
|
+
/** Convert a read Word document to Postext chapters and resources. */
|
|
629
|
+
export function wordToPostext(doc, settings) {
|
|
630
|
+
return new Converter(doc, settings).run();
|
|
631
|
+
}
|
|
632
|
+
//# sourceMappingURL=toMarkdown.js.map
|