@godcreator/word-report 0.5.1 → 0.6.1
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/cli.mjs +2 -2
- package/dist/core.mjs +1 -1
- package/dist/{export-CGvF3Wz-.mjs → export-BrDdJ_E-.mjs} +2 -2
- package/dist/{mdx-B1V34o8x.mjs → mdx-DUOW1JEE.mjs} +45 -20
- package/dist/node.mjs +2 -2
- package/dist/{report-files-DyPBOCgh.mjs → report-files-CpZA-0-K.mjs} +1 -1
- package/dist/site-server.mjs +2 -2
- package/engine/src/docreport/compose.py +172 -37
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as reportExport, r as checkReports } from "./export-
|
|
3
|
-
import { o as reportEntryIn, s as reportEntryOf } from "./report-files-
|
|
2
|
+
import { n as reportExport, r as checkReports } from "./export-BrDdJ_E-.mjs";
|
|
3
|
+
import { o as reportEntryIn, s as reportEntryOf } from "./report-files-CpZA-0-K.mjs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { realpathSync } from "node:fs";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
package/dist/core.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { i as validateDocument, n as safeRelativePath, o as parseReportJson, s as ReportContentError, t as assembleDocument } from "./assemble-BEt9_xsB.mjs";
|
|
2
|
-
import { a as mdxReportInputsOf, i as compileMdxReport, n as MDX_REFERENCES_NAME, o as GENERATED_ID_PREFIX, r as assembleMdxReport, t as MDX_ENTRY_NAME } from "./mdx-
|
|
2
|
+
import { a as mdxReportInputsOf, i as compileMdxReport, n as MDX_REFERENCES_NAME, o as GENERATED_ID_PREFIX, r as assembleMdxReport, t as MDX_ENTRY_NAME } from "./mdx-DUOW1JEE.mjs";
|
|
3
3
|
export { GENERATED_ID_PREFIX, MDX_ENTRY_NAME, MDX_REFERENCES_NAME, ReportContentError, assembleDocument, assembleMdxReport, compileMdxReport, mdxReportInputsOf, parseReportJson, safeRelativePath, validateDocument };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as walk, i as validateDocument, n as safeRelativePath, o as parseReportJson, s as ReportContentError, t as assembleDocument } from "./assemble-BEt9_xsB.mjs";
|
|
2
|
-
import { a as mdxReportInputsOf, r as assembleMdxReport, t as MDX_ENTRY_NAME } from "./mdx-
|
|
3
|
-
import { l as reportsDirName, t as JSON_ENTRY_NAME } from "./report-files-
|
|
2
|
+
import { a as mdxReportInputsOf, r as assembleMdxReport, t as MDX_ENTRY_NAME } from "./mdx-DUOW1JEE.mjs";
|
|
3
|
+
import { l as reportsDirName, t as JSON_ENTRY_NAME } from "./report-files-CpZA-0-K.mjs";
|
|
4
4
|
import { access, mkdir, open, readFile, readdir, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -298,12 +298,13 @@ function collectElement(context, element, marks, out) {
|
|
|
298
298
|
return;
|
|
299
299
|
}
|
|
300
300
|
case "WordPlainMath": {
|
|
301
|
-
const attributes = attributesOf(context, element, {
|
|
302
|
-
const
|
|
301
|
+
const attributes = attributesOf(context, element, { word: "required" });
|
|
302
|
+
const latex = plainMathFormulaOf(context, element);
|
|
303
|
+
const plainText = plainMathRunsOf(context, element, attributes["word"]);
|
|
303
304
|
out.push(located(context, context.ids.request({
|
|
304
305
|
type: "math",
|
|
305
306
|
id: "",
|
|
306
|
-
latex
|
|
307
|
+
latex,
|
|
307
308
|
plainText
|
|
308
309
|
}, "m"), element));
|
|
309
310
|
return;
|
|
@@ -323,27 +324,51 @@ function unknownComponentMessage(name, allowed) {
|
|
|
323
324
|
if (/^[A-Z]/.test(name) && !name.startsWith("Word")) return `报告里的组件都以 Word 开头,<${name}> 不认:${allowed}`;
|
|
324
325
|
return `认不出组件 <${name}>:${allowed}`;
|
|
325
326
|
}
|
|
326
|
-
|
|
327
|
+
/** <WordPlainMath> 标签里的公式:只能是一个 $…$ */
|
|
328
|
+
function plainMathFormulaOf(context, element) {
|
|
329
|
+
const children = element.children.filter((child) => !isCommentOnlyExpression(child));
|
|
330
|
+
const only = children.length === 1 ? children[0] : void 0;
|
|
331
|
+
if (only?.type !== "inlineMath") failAt(context, element, "<WordPlainMath word=\"…\"> 的标签里只写一个行内公式 $…$,别的都不放");
|
|
332
|
+
return only.value;
|
|
333
|
+
}
|
|
334
|
+
const PLAIN_MATH_MARK_ORDER = [
|
|
327
335
|
"em",
|
|
328
336
|
"sub",
|
|
329
337
|
"sup"
|
|
330
338
|
];
|
|
331
|
-
/**
|
|
332
|
-
function plainMathRunsOf(context, element) {
|
|
333
|
-
const usage = "<WordPlainMath
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
339
|
+
/** word 属性:Word 里写的文字,按正文的规则解析,只收文字、*斜体*、<sub>、<sup>;相邻同样式的文字合并 */
|
|
340
|
+
function plainMathRunsOf(context, element, word) {
|
|
341
|
+
const usage = "<WordPlainMath> 的 word 属性写 Word 用的文字,只能用文字、*斜体*、<sub>、<sup>";
|
|
342
|
+
if (/[\r\n]/.test(word)) failAt(context, element, `${usage},不能换行`);
|
|
343
|
+
let root;
|
|
344
|
+
try {
|
|
345
|
+
root = parseMdx(word, "");
|
|
346
|
+
} catch (error) {
|
|
347
|
+
return failAt(context, element, `<WordPlainMath> 的 word 属性解析不了(位置从属性开头算):${error.message}`);
|
|
348
|
+
}
|
|
349
|
+
const blocks = root.children;
|
|
350
|
+
if (blocks.length !== 1 || blocks[0].type !== "paragraph") failAt(context, element, `${usage},这里${blocks.length ? "不是一段文字" : "是空的"}`);
|
|
351
|
+
const runs = [];
|
|
352
|
+
const collect = (nodes, marks) => {
|
|
353
|
+
for (const node of nodes) if (node.type === "text") {
|
|
354
|
+
const sorted = PLAIN_MATH_MARK_ORDER.filter((mark) => marks.has(mark));
|
|
355
|
+
const previous = runs.at(-1);
|
|
356
|
+
if (previous && (previous.marks ?? []).join() === sorted.join()) previous.text += node.value;
|
|
357
|
+
else runs.push(sorted.length ? {
|
|
358
|
+
text: node.value,
|
|
359
|
+
marks: sorted
|
|
360
|
+
} : { text: node.value });
|
|
361
|
+
} else if (node.type === "emphasis") collect(node.children, /* @__PURE__ */ new Set([...marks, "em"]));
|
|
362
|
+
else if (node.type === "mdxJsxTextElement" && (node.name === "sub" || node.name === "sup")) {
|
|
363
|
+
if (node.attributes.length) failAt(context, element, `${usage},<${node.name}> 不带属性`);
|
|
364
|
+
const script = node.name;
|
|
365
|
+
if (marks.has(script === "sub" ? "sup" : "sub")) failAt(context, element, "<WordPlainMath> 的 word 属性里同一段文字不能既是下标又是上标");
|
|
366
|
+
collect(node.children, /* @__PURE__ */ new Set([...marks, script]));
|
|
367
|
+
} else failAt(context, element, `${usage},这里出现了 ${node.type === "mdxJsxTextElement" ? `<${node.name}>` : node.type}`);
|
|
368
|
+
};
|
|
369
|
+
collect(blocks[0].children, /* @__PURE__ */ new Set());
|
|
370
|
+
if (!runs.some((run) => run.text)) failAt(context, element, `${usage},这里是空的`);
|
|
371
|
+
return runs.filter((run) => run.text);
|
|
347
372
|
}
|
|
348
373
|
function linkTextOf(context, link) {
|
|
349
374
|
const inlines = [];
|
package/dist/node.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { i as readReport, n as reportExport, r as checkReports, t as reportEntry } from "./export-
|
|
2
|
-
import { a as readReportFiles, c as reportIdsOf, i as readMdxReportFiles, l as reportsDirName, n as REPORT_ID_PATTERN, o as reportEntryIn, r as isMdxEntry, s as reportEntryOf, t as JSON_ENTRY_NAME } from "./report-files-
|
|
1
|
+
import { i as readReport, n as reportExport, r as checkReports, t as reportEntry } from "./export-BrDdJ_E-.mjs";
|
|
2
|
+
import { a as readReportFiles, c as reportIdsOf, i as readMdxReportFiles, l as reportsDirName, n as REPORT_ID_PATTERN, o as reportEntryIn, r as isMdxEntry, s as reportEntryOf, t as JSON_ENTRY_NAME } from "./report-files-CpZA-0-K.mjs";
|
|
3
3
|
export { JSON_ENTRY_NAME, REPORT_ID_PATTERN, checkReports, isMdxEntry, readMdxReportFiles, readReport, readReportFiles, reportEntry, reportEntryIn, reportEntryOf, reportExport, reportIdsOf, reportsDirName };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { i as validateDocument, n as safeRelativePath, o as parseReportJson, s as ReportContentError } from "./assemble-BEt9_xsB.mjs";
|
|
2
|
-
import { a as mdxReportInputsOf, t as MDX_ENTRY_NAME } from "./mdx-
|
|
2
|
+
import { a as mdxReportInputsOf, t as MDX_ENTRY_NAME } from "./mdx-DUOW1JEE.mjs";
|
|
3
3
|
import { readFile, readdir, realpath, stat } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
//#region src/node/report-files.ts
|
package/dist/site-server.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as assembleDocument } from "./assemble-BEt9_xsB.mjs";
|
|
2
|
-
import { a as mdxReportInputsOf, r as assembleMdxReport, t as MDX_ENTRY_NAME } from "./mdx-
|
|
3
|
-
import { a as readReportFiles, c as reportIdsOf, i as readMdxReportFiles, n as REPORT_ID_PATTERN, o as reportEntryIn, r as isMdxEntry, t as JSON_ENTRY_NAME } from "./report-files-
|
|
2
|
+
import { a as mdxReportInputsOf, r as assembleMdxReport, t as MDX_ENTRY_NAME } from "./mdx-DUOW1JEE.mjs";
|
|
3
|
+
import { a as readReportFiles, c as reportIdsOf, i as readMdxReportFiles, n as REPORT_ID_PATTERN, o as reportEntryIn, r as isMdxEntry, t as JSON_ENTRY_NAME } from "./report-files-CpZA-0-K.mjs";
|
|
4
4
|
import { n as reportHrefOf } from "./page-data-DLPoVgXy.mjs";
|
|
5
5
|
import { readFile, realpath, stat } from "node:fs/promises";
|
|
6
6
|
import path from "node:path";
|
|
@@ -10,6 +10,8 @@ import re
|
|
|
10
10
|
from docreport.plain_math import INLINE_MATH_STYLE
|
|
11
11
|
from docreport.word import utf16_length, find_text, MarkerFinder
|
|
12
12
|
|
|
13
|
+
BODY_MARKER = re.compile(r'@(?:FIELD|NUMSTART|NUMEND|LINK|NOTE)\d+@')
|
|
14
|
+
|
|
13
15
|
|
|
14
16
|
def mm(value):
|
|
15
17
|
"""Convert millimetres to Word points."""
|
|
@@ -227,30 +229,74 @@ class ContentComposer(Composer):
|
|
|
227
229
|
self.appendix_numbering = None
|
|
228
230
|
self.appendix_count = 0
|
|
229
231
|
self._inline_math_style = None
|
|
232
|
+
# Each COM call crosses into the Word process; style objects, the bookmark collection
|
|
233
|
+
# and the start of the paragraph just written are fetched once and reused.
|
|
234
|
+
self._style_objects = {}
|
|
235
|
+
self._bookmark_collection = doc.Bookmarks
|
|
236
|
+
self._last_range, self._last_start = None, None
|
|
237
|
+
self._token_positions = {}
|
|
238
|
+
|
|
239
|
+
def style_object(self, name):
|
|
240
|
+
style = self._style_objects.get(name)
|
|
241
|
+
if style is None:
|
|
242
|
+
style = self._style_objects[name] = self.doc.Styles.Item(name)
|
|
243
|
+
return style
|
|
244
|
+
|
|
245
|
+
def in_main_story(self):
|
|
246
|
+
"""Body paragraphs and body table cells; a footnote container is another story."""
|
|
247
|
+
return self.container is None or self.cell
|
|
248
|
+
|
|
249
|
+
def range_at(self, start, end):
|
|
250
|
+
if self.in_main_story():
|
|
251
|
+
return self.doc.Range(start, end)
|
|
252
|
+
rng = self.container.Range.Duplicate
|
|
253
|
+
rng.SetRange(start, end)
|
|
254
|
+
return rng
|
|
230
255
|
|
|
231
|
-
def
|
|
232
|
-
|
|
256
|
+
def insertion_point(self):
|
|
257
|
+
if self.container is None:
|
|
258
|
+
position = self.doc.Content.End - 1
|
|
259
|
+
return self.doc.Range(position, position), position
|
|
260
|
+
rng = self.container.Range.Duplicate
|
|
233
261
|
# Word exposes the cell's CR/BEL terminator as one Range position.
|
|
234
262
|
position = rng.End - 1
|
|
235
263
|
rng.SetRange(position, position)
|
|
236
|
-
return rng
|
|
264
|
+
return rng, position
|
|
265
|
+
|
|
266
|
+
def point(self):
|
|
267
|
+
return self.insertion_point()[0]
|
|
237
268
|
|
|
238
269
|
def paragraph(self, text, style=None):
|
|
239
|
-
rng = self.
|
|
240
|
-
|
|
270
|
+
rng, start = self.insertion_point()
|
|
271
|
+
# InsertBefore expands the range over the inserted text and paragraph mark.
|
|
241
272
|
rng.InsertBefore(text + '\r')
|
|
242
|
-
rng.
|
|
243
|
-
|
|
273
|
+
rng.Style = self.style_object(self.style if style is None else style)
|
|
274
|
+
self._last_range, self._last_start = rng, start
|
|
275
|
+
self.record_tokens(text, start)
|
|
244
276
|
return rng
|
|
245
277
|
|
|
278
|
+
def record_tokens(self, text, start):
|
|
279
|
+
"""Body positions of field, number, note and link markers, so finish() needs no Find.
|
|
280
|
+
|
|
281
|
+
Content is only appended after a recorded marker, so its position holds until
|
|
282
|
+
build() replaces markers; notes and links shift later positions, fields go back to front.
|
|
283
|
+
"""
|
|
284
|
+
if not self.in_main_story():
|
|
285
|
+
return
|
|
286
|
+
for match in BODY_MARKER.finditer(text):
|
|
287
|
+
self._token_positions[match.group()] = start + utf16_length(text[:match.start()])
|
|
288
|
+
|
|
246
289
|
def part(self, rng, offset, length):
|
|
290
|
+
if rng is self._last_range:
|
|
291
|
+
return self.range_at(self._last_start + offset, self._last_start + offset + length)
|
|
247
292
|
result = rng.Duplicate
|
|
248
|
-
|
|
293
|
+
start = rng.Start
|
|
294
|
+
result.SetRange(start + offset, start + offset + length)
|
|
249
295
|
return result
|
|
250
296
|
|
|
251
297
|
def mark(self, identifier, rng, suffix=''):
|
|
252
298
|
from .content import bookmark
|
|
253
|
-
self.
|
|
299
|
+
self._bookmark_collection.Add(bookmark(identifier, suffix), rng)
|
|
254
300
|
|
|
255
301
|
def math_token(self, node, inline):
|
|
256
302
|
from .content import bookmark
|
|
@@ -315,21 +361,29 @@ class ContentComposer(Composer):
|
|
|
315
361
|
def rich_paragraph(self, nodes, style=None, prefix='', suffix=''):
|
|
316
362
|
text, spans = self.inline(nodes)
|
|
317
363
|
rng = self.paragraph(prefix + text + suffix, style)
|
|
318
|
-
self.
|
|
364
|
+
self.apply_marks_at(self._last_start + utf16_length(prefix), spans)
|
|
319
365
|
return rng
|
|
320
366
|
|
|
321
367
|
def apply_marks(self, rng, offset, spans):
|
|
368
|
+
start = self._last_start if rng is self._last_range else rng.Start
|
|
369
|
+
self.apply_marks_at(start + offset, spans)
|
|
370
|
+
|
|
371
|
+
def apply_marks_at(self, base, spans):
|
|
322
372
|
for start, length, marks in spans:
|
|
323
373
|
if not marks:
|
|
324
374
|
continue
|
|
325
|
-
run = self.
|
|
375
|
+
run = self.range_at(base + start, base + start + length)
|
|
326
376
|
for mark in marks:
|
|
327
377
|
if mark == 'code':
|
|
328
378
|
run.Font.Name = 'Consolas'
|
|
329
379
|
elif mark == 'math':
|
|
330
380
|
run.Style = self.inline_math_style()
|
|
381
|
+
elif mark == 'strong':
|
|
382
|
+
run.Bold = True
|
|
383
|
+
elif mark == 'em':
|
|
384
|
+
run.Italic = True
|
|
331
385
|
else:
|
|
332
|
-
setattr(run.Font, {'
|
|
386
|
+
setattr(run.Font, {'sup': 'Superscript', 'sub': 'Subscript'}[mark], True)
|
|
333
387
|
|
|
334
388
|
def inline_math_style(self):
|
|
335
389
|
"""Character style of inline formulas written as text; created in the document, the DOTX stays unchanged."""
|
|
@@ -343,11 +397,13 @@ class ContentComposer(Composer):
|
|
|
343
397
|
"""Write a one-paragraph cell in one assignment; the general path inserts then trims a paragraph."""
|
|
344
398
|
text, spans = self.inline(node['content'])
|
|
345
399
|
rng = cell.Range
|
|
400
|
+
start = rng.Start
|
|
346
401
|
rng.Text = text
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
self.
|
|
350
|
-
self.
|
|
402
|
+
self.record_tokens(text, start)
|
|
403
|
+
target = self.range_at(start, start + utf16_length(text))
|
|
404
|
+
target.Style = self.style_object(self.layout.get(node['id'], {}).get('style') or self.style)
|
|
405
|
+
self.apply_marks_at(start, spans)
|
|
406
|
+
self.mark(node['id'], target, '_b')
|
|
351
407
|
|
|
352
408
|
def chapter_sequence(self, kind, bookmark):
|
|
353
409
|
"""「章号-序号」 for an equation, figure or table in the current top-level chapter."""
|
|
@@ -494,7 +550,7 @@ class ContentComposer(Composer):
|
|
|
494
550
|
table.AllowAutoFit = False
|
|
495
551
|
table.Borders.Enable = 1
|
|
496
552
|
table.Rows.AllowBreakAcrossPages = 0
|
|
497
|
-
table.Range.Style = self.
|
|
553
|
+
table.Range.Style = self.style_object('Table Text')
|
|
498
554
|
table.TopPadding = table.BottomPadding = 3
|
|
499
555
|
table.LeftPadding = table.RightPadding = 4
|
|
500
556
|
layout = self.layout.get(node['id'], {})
|
|
@@ -512,14 +568,19 @@ class ContentComposer(Composer):
|
|
|
512
568
|
table.Rows.Item(r).Shading.BackgroundPatternColor = 0xEFEFEF
|
|
513
569
|
if layout.get('minRowHeightPt'):
|
|
514
570
|
table.Rows.HeightRule, table.Rows.Height = 1, layout['minRowHeightPt']
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
571
|
+
if any(rs > 1 or cs > 1 for _, _, rs, cs, _ in placements):
|
|
572
|
+
# Merging renumbers cells; ranges taken before merging still find each placed cell.
|
|
573
|
+
cells = {(r, c): table.Cell(r + 1, c + 1).Range.Duplicate
|
|
574
|
+
for r in range(len(node['rows'])) for c in range(len(node['columns']))}
|
|
575
|
+
for r, c, rs, cs, cell in reversed(placements):
|
|
576
|
+
if rs > 1 or cs > 1:
|
|
577
|
+
cells[r, c].Cells.Item(1).Merge(cells[r + rs - 1, c + cs - 1].Cells.Item(1))
|
|
578
|
+
cell_at = lambda r, c: cells[r, c].Cells.Item(1)
|
|
579
|
+
else:
|
|
580
|
+
cell_at = lambda r, c: table.Cell(r + 1, c + 1)
|
|
520
581
|
saved = self.container, self.cell, self.style, self.width
|
|
521
582
|
for r, c, rs, cs, cell in placements:
|
|
522
|
-
self.container =
|
|
583
|
+
self.container = cell_at(r, c)
|
|
523
584
|
self.cell = True
|
|
524
585
|
self.style = 'Table Header' if r < node['headerRows'] else 'Table Text'
|
|
525
586
|
self.width = sum(widths[c:c + cs]) - 8
|
|
@@ -608,6 +669,92 @@ class ContentComposer(Composer):
|
|
|
608
669
|
else:
|
|
609
670
|
raise ValueError(f'Unsupported block: {kind}')
|
|
610
671
|
|
|
672
|
+
def replace_body_marker(self, token, replace):
|
|
673
|
+
"""Replace a note or link marker; markers recorded after it move by the change in body length."""
|
|
674
|
+
position = self._token_positions.pop(token, None)
|
|
675
|
+
if position is None:
|
|
676
|
+
replace(find_text(self.doc, token))
|
|
677
|
+
return
|
|
678
|
+
before = self.doc.Content.End
|
|
679
|
+
replace(self.doc.Range(position, position + utf16_length(token)))
|
|
680
|
+
delta = self.doc.Content.End - before
|
|
681
|
+
if delta:
|
|
682
|
+
for marker, recorded in self._token_positions.items():
|
|
683
|
+
if recorded > position:
|
|
684
|
+
self._token_positions[marker] = recorded + delta
|
|
685
|
+
|
|
686
|
+
def compose_note(self, target, identifier):
|
|
687
|
+
target.Text = ''
|
|
688
|
+
note = self.doc.Footnotes.Add(Range=target, Text='')
|
|
689
|
+
saved = self.container, self.cell, self.style, self.width
|
|
690
|
+
self.container, self.cell, self.style = note, False, -30
|
|
691
|
+
self.blocks(self.context['index'][identifier]['blocks'])
|
|
692
|
+
self.trim_container()
|
|
693
|
+
self.container, self.cell, self.style, self.width = saved
|
|
694
|
+
|
|
695
|
+
def add_link(self, target, text, url):
|
|
696
|
+
# Word can retain a nonempty anchor's placeholder even when
|
|
697
|
+
# TextToDisplay is supplied. Replace the range explicitly before
|
|
698
|
+
# adding the hyperlink so the saved document contains source text.
|
|
699
|
+
target.Text = text
|
|
700
|
+
self.doc.Hyperlinks.Add(target, url)
|
|
701
|
+
|
|
702
|
+
def add_field(self, token, rng, code, bookmark):
|
|
703
|
+
field = self.doc.Fields.Add(rng, -1, code, False)
|
|
704
|
+
font = field.Code.Font
|
|
705
|
+
font.Bold, font.Italic = 0, 0
|
|
706
|
+
if bookmark:
|
|
707
|
+
self._bookmark_collection.Add(bookmark, field.Result)
|
|
708
|
+
|
|
709
|
+
def finish(self):
|
|
710
|
+
"""Body markers go back to front, so each replacement leaves earlier positions unchanged.
|
|
711
|
+
|
|
712
|
+
A collapsed or adjacent Word range does not follow a field inserted right before it,
|
|
713
|
+
so the end of a 「章号-序号」 bookmark is tracked from the change in body length.
|
|
714
|
+
Markers inside footnotes are not recorded and are found by searching.
|
|
715
|
+
"""
|
|
716
|
+
if self._finished:
|
|
717
|
+
raise RuntimeError('Composer.finish() may only be called once.')
|
|
718
|
+
doc = self.doc
|
|
719
|
+
fields = {token: (code, bookmark) for token, code, bookmark in self._pending_fields}
|
|
720
|
+
openings = {begin: (end, bookmark) for begin, end, bookmark in self._pending_numbers}
|
|
721
|
+
closings = {end for _, end, _ in self._pending_numbers}
|
|
722
|
+
open_number = None
|
|
723
|
+
for position, token in sorted(((p, t) for t, p in self._token_positions.items()), reverse=True):
|
|
724
|
+
rng = doc.Range(position, position + utf16_length(token))
|
|
725
|
+
if token in fields:
|
|
726
|
+
if open_number is None:
|
|
727
|
+
self.add_field(token, rng, *fields.pop(token))
|
|
728
|
+
continue
|
|
729
|
+
before = doc.Content.End
|
|
730
|
+
self.add_field(token, rng, *fields.pop(token))
|
|
731
|
+
open_number[1] += doc.Content.End - before
|
|
732
|
+
elif token in closings:
|
|
733
|
+
if open_number is not None:
|
|
734
|
+
raise RuntimeError(f'Number marker nested in another number: {token}')
|
|
735
|
+
rng.Delete()
|
|
736
|
+
open_number = [token, position]
|
|
737
|
+
elif token in openings:
|
|
738
|
+
end_token, bookmark = openings.pop(token)
|
|
739
|
+
if open_number is None or open_number[0] != end_token:
|
|
740
|
+
raise RuntimeError(f'Number marker without its end: {token}')
|
|
741
|
+
rng.Delete()
|
|
742
|
+
self._bookmark_collection.Add(bookmark, doc.Range(position, open_number[1] - utf16_length(token)))
|
|
743
|
+
open_number = None
|
|
744
|
+
else:
|
|
745
|
+
raise RuntimeError(f'Body marker left unhandled: {token}')
|
|
746
|
+
if openings:
|
|
747
|
+
raise RuntimeError(f'Number markers outside the body: {sorted(openings)}')
|
|
748
|
+
finder = MarkerFinder(doc)
|
|
749
|
+
for token, code, bookmark in self._pending_fields:
|
|
750
|
+
if token in fields:
|
|
751
|
+
self.add_field(token, finder.find(token), *fields.pop(token))
|
|
752
|
+
for table in doc.Tables:
|
|
753
|
+
following = doc.Range(table.Range.End, table.Range.End).Paragraphs.First.Format
|
|
754
|
+
following.SpaceBefore = max(6, following.SpaceBefore)
|
|
755
|
+
self._finished = True
|
|
756
|
+
return self.pending_math
|
|
757
|
+
|
|
611
758
|
def build(self):
|
|
612
759
|
metadata = self.content['metadata']
|
|
613
760
|
values = dict(reportTitle=metadata['title'], projectName='', author='、'.join(metadata.get('authors', [])),
|
|
@@ -623,21 +770,9 @@ class ContentComposer(Composer):
|
|
|
623
770
|
self.doc.Bookmarks.Add('ReportBodyStart', self.doc.Range(start, start))
|
|
624
771
|
self.blocks(self.content['blocks'])
|
|
625
772
|
for token, identifier in self.note_tokens:
|
|
626
|
-
target
|
|
627
|
-
target.Text = ''
|
|
628
|
-
note = self.doc.Footnotes.Add(Range=target, Text='')
|
|
629
|
-
saved = self.container, self.cell, self.style, self.width
|
|
630
|
-
self.container, self.cell, self.style = note, False, -30
|
|
631
|
-
self.blocks(self.context['index'][identifier]['blocks'])
|
|
632
|
-
self.trim_container()
|
|
633
|
-
self.container, self.cell, self.style, self.width = saved
|
|
773
|
+
self.replace_body_marker(token, lambda target: self.compose_note(target, identifier))
|
|
634
774
|
for token, text, url in self.links:
|
|
635
|
-
target
|
|
636
|
-
# Word can retain a nonempty anchor's placeholder even when
|
|
637
|
-
# TextToDisplay is supplied. Replace the range explicitly before
|
|
638
|
-
# adding the hyperlink so the saved document contains source text.
|
|
639
|
-
target.Text = text
|
|
640
|
-
self.doc.Hyperlinks.Add(target, url)
|
|
775
|
+
self.replace_body_marker(token, lambda target: self.add_link(target, text, url))
|
|
641
776
|
pending = self.finish()
|
|
642
777
|
for index, section in enumerate(self.doc.Sections, 1):
|
|
643
778
|
if index > 1:
|