@godcreator/word-report 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{assemble-3cx8L2vo.mjs → assemble-D25NLmLx.mjs} +16 -10
- package/dist/cli.mjs +2 -2
- package/dist/core.mjs +1 -1
- package/dist/{export-TbclsIsE.mjs → export-DJQiBPPU.mjs} +2 -2
- package/dist/node.mjs +2 -2
- package/dist/{report-files-DM9Ea6yK.mjs → report-files-CgWzLxH-.mjs} +1 -1
- package/dist/site-server.d.mts +3 -7
- package/dist/site-server.mjs +8 -64
- package/dist/site.mjs +1 -1
- package/engine/src/docreport/compose.py +29 -25
- package/engine/src/docreport/content.py +3 -0
- package/package.json +1 -1
|
@@ -994,12 +994,21 @@ function assembleDocument(entry, files, options = {}) {
|
|
|
994
994
|
}
|
|
995
995
|
const counts = {
|
|
996
996
|
decimal: 0,
|
|
997
|
-
appendix: 0
|
|
998
|
-
figure: 0,
|
|
999
|
-
table: 0
|
|
997
|
+
appendix: 0
|
|
1000
998
|
};
|
|
1001
999
|
let appendixSeen = false;
|
|
1002
|
-
const
|
|
1000
|
+
const chapterCounts = {
|
|
1001
|
+
figure: /* @__PURE__ */ new Map(),
|
|
1002
|
+
table: /* @__PURE__ */ new Map(),
|
|
1003
|
+
equation: /* @__PURE__ */ new Map()
|
|
1004
|
+
};
|
|
1005
|
+
/** 图、编号表格、编号公式各自按所在的顶层章从 1 编,写成「章号-n」;导出 Word 的题注同样取章号,不在编号章里就没有章号可取 */
|
|
1006
|
+
function chapterNumber(node, kind, chapter) {
|
|
1007
|
+
if (!chapter) fail(node, `Numbered ${kind} outside a numbered chapter`);
|
|
1008
|
+
const n = (chapterCounts[kind].get(chapter) ?? 0) + 1;
|
|
1009
|
+
chapterCounts[kind].set(chapter, n);
|
|
1010
|
+
return `${chapter}-${n}`;
|
|
1011
|
+
}
|
|
1003
1012
|
function blockNumbers(blocks, depth = 0, chapter, parentNumber, parentMode, tables = 0) {
|
|
1004
1013
|
let decimal = 0;
|
|
1005
1014
|
for (const node of blocks) {
|
|
@@ -1029,18 +1038,15 @@ function assembleDocument(entry, files, options = {}) {
|
|
|
1029
1038
|
continue;
|
|
1030
1039
|
}
|
|
1031
1040
|
if (node.type === "figure") {
|
|
1032
|
-
number =
|
|
1041
|
+
number = chapterNumber(node, "figure", chapter);
|
|
1033
1042
|
label = `图 ${number}`;
|
|
1034
1043
|
}
|
|
1035
1044
|
if (node.type === "table" && node.numbered) {
|
|
1036
|
-
number =
|
|
1045
|
+
number = chapterNumber(node, "table", chapter);
|
|
1037
1046
|
label = `表 ${number}`;
|
|
1038
1047
|
}
|
|
1039
1048
|
if (node.type === "equation" && node.numbered) {
|
|
1040
|
-
|
|
1041
|
-
const n = (eqCounts.get(chapter) ?? 0) + 1;
|
|
1042
|
-
eqCounts.set(chapter, n);
|
|
1043
|
-
number = `${chapter}-${n}`;
|
|
1049
|
+
number = chapterNumber(node, "equation", chapter);
|
|
1044
1050
|
label = `式(${number})`;
|
|
1045
1051
|
}
|
|
1046
1052
|
if (node.id) index[node.id] = {
|
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 { r as reportEntryOf } from "./report-files-
|
|
2
|
+
import { n as reportExport, r as checkReports } from "./export-DJQiBPPU.mjs";
|
|
3
|
+
import { r as reportEntryOf } from "./report-files-CgWzLxH-.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 +1 @@
|
|
|
1
|
-
export { o as ReportContentError, t as assembleDocument, a as parseReportJson, n as safeRelativePath, r as validateDocument } from "./assemble-
|
|
1
|
+
export { o as ReportContentError, t as assembleDocument, a as parseReportJson, n as safeRelativePath, r as validateDocument } from "./assemble-D25NLmLx.mjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as parseReportJson, i as walk, n as safeRelativePath, o as ReportContentError, r as validateDocument, t as assembleDocument } from "./assemble-
|
|
2
|
-
import { a as reportsDirName } from "./report-files-
|
|
1
|
+
import { a as parseReportJson, i as walk, n as safeRelativePath, o as ReportContentError, r as validateDocument, t as assembleDocument } from "./assemble-D25NLmLx.mjs";
|
|
2
|
+
import { a as reportsDirName } from "./report-files-CgWzLxH-.mjs";
|
|
3
3
|
import { access, mkdir, open, readFile, readdir, realpath, rename, stat, unlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
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 reportsDirName, i as reportIdsOf, n as readReportFiles, r as reportEntryOf, t as REPORT_ID_PATTERN } from "./report-files-
|
|
1
|
+
import { i as readReport, n as reportExport, r as checkReports, t as reportEntry } from "./export-DJQiBPPU.mjs";
|
|
2
|
+
import { a as reportsDirName, i as reportIdsOf, n as readReportFiles, r as reportEntryOf, t as REPORT_ID_PATTERN } from "./report-files-CgWzLxH-.mjs";
|
|
3
3
|
export { REPORT_ID_PATTERN, checkReports, readReport, readReportFiles, reportEntry, reportEntryOf, reportExport, reportIdsOf, reportsDirName };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as parseReportJson, n as safeRelativePath, o as ReportContentError, r as validateDocument } from "./assemble-
|
|
1
|
+
import { a as parseReportJson, n as safeRelativePath, o as ReportContentError, r as validateDocument } from "./assemble-D25NLmLx.mjs";
|
|
2
2
|
import { readFile, readdir, realpath, stat } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
//#region src/node/report-files.ts
|
package/dist/site-server.d.mts
CHANGED
|
@@ -10,19 +10,15 @@ type WordReportSite = {
|
|
|
10
10
|
hrefBase: string;
|
|
11
11
|
pageComments: (page: string) => ReportPageComments;
|
|
12
12
|
};
|
|
13
|
-
/** loader 用到的路由参数:报告页是 :report,图片再加
|
|
13
|
+
/** loader 用到的路由参数:报告页是 :report,图片再加 * */
|
|
14
14
|
type ReportRouteArgs = {
|
|
15
15
|
params: Partial<Record<string, string>>;
|
|
16
|
-
request?: Request;
|
|
17
16
|
};
|
|
18
17
|
/** 报告列表:站点 reports/ 下的每份报告各一行;读不对的报告也列出来,带上原话 */
|
|
19
18
|
declare function reportListLoader(_args: ReportRouteArgs, site: WordReportSite): Promise<ReportListData>;
|
|
20
19
|
/** 报告页:整份报告组装成一页(各章按 include 的顺序接在一起);报告读不对直接报错,原话在错误页上 */
|
|
21
20
|
declare function reportPageLoader({ params }: ReportRouteArgs, site: WordReportSite): Promise<ReportPageData>;
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
* 浏览器每次都来问(no-cache),ETag 由文件大小与修改时间算出,没变就回 304、不再传图
|
|
25
|
-
*/
|
|
26
|
-
declare function reportAssetLoader({ params, request }: ReportRouteArgs, site: WordReportSite): Promise<Response>;
|
|
21
|
+
/** 报告里的一张图:地址是组装后的路径 assets/<文档 id>/<文件名>,只给这份报告用到的图 */
|
|
22
|
+
declare function reportAssetLoader({ params }: ReportRouteArgs, site: WordReportSite): Promise<Response>;
|
|
27
23
|
//#endregion
|
|
28
24
|
export { type ReportListData, type ReportPageComments, type ReportPageData, ReportRouteArgs, type ReportSummary, type ReportTocItem, WordReportSite, reportAssetLoader, reportListLoader, reportPageLoader };
|
package/dist/site-server.mjs
CHANGED
|
@@ -1,60 +1,18 @@
|
|
|
1
|
-
import { t as assembleDocument } from "./assemble-
|
|
2
|
-
import { i as reportIdsOf, n as readReportFiles, r as reportEntryOf, t as REPORT_ID_PATTERN } from "./report-files-
|
|
1
|
+
import { t as assembleDocument } from "./assemble-D25NLmLx.mjs";
|
|
2
|
+
import { i as reportIdsOf, n as readReportFiles, r as reportEntryOf, t as REPORT_ID_PATTERN } from "./report-files-CgWzLxH-.mjs";
|
|
3
3
|
import { n as reportHrefOf } from "./page-data-DLPoVgXy.mjs";
|
|
4
|
-
import { readFile, realpath, stat } from "node:fs/promises";
|
|
5
4
|
import path from "node:path";
|
|
6
5
|
import { createReadStream } from "node:fs";
|
|
7
6
|
import { Readable } from "node:stream";
|
|
8
7
|
//#region src/site/loaders.server.ts
|
|
9
8
|
/**
|
|
10
|
-
* `@godcreator/word-report/site/server`:报告列表、报告页与报告里图片的 loader
|
|
11
|
-
* 报告页与图片在 JSON 原文没变时沿用上一次的组装结果。
|
|
9
|
+
* `@godcreator/word-report/site/server`:报告列表、报告页与报告里图片的 loader,报告每次从站点 reports/ 现读、现组装。
|
|
12
10
|
*/
|
|
13
11
|
/** 组装结果里有 Object.create(null) 的对象:换成普通对象再交给 React Router 序列化 */
|
|
14
12
|
const plainOf = (result) => JSON.parse(JSON.stringify(result));
|
|
15
|
-
/**
|
|
16
|
-
* 每份报告最近一次组装,键是站点目录加报告 id。一页报告的几十张图同时请求,组装一次要几百毫秒到几秒、
|
|
17
|
-
* 全在一个线程上排队:不共用的话,最后一张图要等几十次组装
|
|
18
|
-
*/
|
|
19
|
-
const latestAssemblies = /* @__PURE__ */ new Map();
|
|
20
|
-
async function assembleFromDisk(siteDir, reportId) {
|
|
21
|
-
const entry = reportEntryOf(reportId);
|
|
22
|
-
const files = await readReportFiles(siteDir, entry);
|
|
23
|
-
return {
|
|
24
|
-
files,
|
|
25
|
-
result: assembleDocument(entry, files, { mode: "complete" })
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
/** 上次组装读过的每个 JSON 文件原文都没变;文件没了也算变了,交给重新组装去报错 */
|
|
29
|
-
async function sameFilesOnDisk(siteDir, files) {
|
|
30
|
-
const root = await realpath(siteDir);
|
|
31
|
-
return (await Promise.all(Object.entries(files).map(async ([relative, text]) => {
|
|
32
|
-
try {
|
|
33
|
-
return await readFile(path.join(root, relative), "utf8") === text;
|
|
34
|
-
} catch (error) {
|
|
35
|
-
if (error.code === "ENOENT") return false;
|
|
36
|
-
throw error;
|
|
37
|
-
}
|
|
38
|
-
}))).every(Boolean);
|
|
39
|
-
}
|
|
40
|
-
/** 报告的整份组装结果:JSON 原文和上次一样就用上次的,同时到的请求共用同一次组装;结果只读,要改先复制 */
|
|
41
13
|
async function assembledReport(siteDir, reportId) {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
if (latest !== void 0) {
|
|
45
|
-
const assembly = await latest;
|
|
46
|
-
if (await sameFilesOnDisk(siteDir, assembly.files)) return assembly.result;
|
|
47
|
-
}
|
|
48
|
-
const newer = latestAssemblies.get(key);
|
|
49
|
-
if (newer !== void 0 && newer !== latest) return (await newer).result;
|
|
50
|
-
const fresh = assembleFromDisk(siteDir, reportId);
|
|
51
|
-
latestAssemblies.set(key, fresh);
|
|
52
|
-
try {
|
|
53
|
-
return (await fresh).result;
|
|
54
|
-
} catch (error) {
|
|
55
|
-
if (latestAssemblies.get(key) === fresh) latestAssemblies.delete(key);
|
|
56
|
-
throw error;
|
|
57
|
-
}
|
|
14
|
+
const entry = reportEntryOf(reportId);
|
|
15
|
+
return assembleDocument(entry, await readReportFiles(siteDir, entry), { mode: "complete" });
|
|
58
16
|
}
|
|
59
17
|
/** 地址里的报告 id:形状不对或站点里没有这份报告都是 404 */
|
|
60
18
|
async function reportIdOf(params, siteDir) {
|
|
@@ -130,29 +88,15 @@ const imageContentTypes = {
|
|
|
130
88
|
".jpg": "image/jpeg",
|
|
131
89
|
".jpeg": "image/jpeg"
|
|
132
90
|
};
|
|
133
|
-
/**
|
|
134
|
-
|
|
135
|
-
* 浏览器每次都来问(no-cache),ETag 由文件大小与修改时间算出,没变就回 304、不再传图
|
|
136
|
-
*/
|
|
137
|
-
async function reportAssetLoader({ params, request }, site) {
|
|
91
|
+
/** 报告里的一张图:地址是组装后的路径 assets/<文档 id>/<文件名>,只给这份报告用到的图 */
|
|
92
|
+
async function reportAssetLoader({ params }, site) {
|
|
138
93
|
const reportId = await reportIdOf(params, site.siteDir);
|
|
139
94
|
const compiledPath = `assets/${params["*"] ?? ""}`;
|
|
140
95
|
const source = (await assembledReport(site.siteDir, reportId)).assets[compiledPath];
|
|
141
96
|
if (source === void 0) return new Response(`报告 ${reportId} 里没有图 ${compiledPath}`, { status: 404 });
|
|
142
97
|
const contentType = imageContentTypes[path.extname(source).toLowerCase()];
|
|
143
98
|
if (contentType === void 0) throw new Error(`报告 ${reportId} 的图 ${source} 不是 png 或 jpg`);
|
|
144
|
-
|
|
145
|
-
const { size, mtimeMs } = await stat(file);
|
|
146
|
-
const headers = {
|
|
147
|
-
"content-type": contentType,
|
|
148
|
-
"cache-control": "no-cache",
|
|
149
|
-
etag: `"${size.toString(16)}-${Math.trunc(mtimeMs).toString(16)}"`
|
|
150
|
-
};
|
|
151
|
-
if (request?.headers.get("if-none-match") === headers.etag) return new Response(null, {
|
|
152
|
-
status: 304,
|
|
153
|
-
headers
|
|
154
|
-
});
|
|
155
|
-
return new Response(Readable.toWeb(createReadStream(file)), { headers });
|
|
99
|
+
return new Response(Readable.toWeb(createReadStream(path.join(site.siteDir, source))), { headers: { "content-type": contentType } });
|
|
156
100
|
}
|
|
157
101
|
//#endregion
|
|
158
102
|
export { reportAssetLoader, reportListLoader, reportPageLoader };
|
package/dist/site.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as assembleDocument } from "./assemble-
|
|
1
|
+
import { t as assembleDocument } from "./assemble-D25NLmLx.mjs";
|
|
2
2
|
import { n as reportHrefOf, t as reportAssetHrefOf } from "./page-data-DLPoVgXy.mjs";
|
|
3
3
|
import { Link, useLoaderData } from "react-router";
|
|
4
4
|
import { DocsBody, DocsPage, DocsTitle } from "@fumadocs/base-ui/layouts/docs/page";
|
|
@@ -61,10 +61,19 @@ class Composer:
|
|
|
61
61
|
if kind not in ('Table', 'Figure'):
|
|
62
62
|
raise ValueError(f'Unknown caption kind: {kind}')
|
|
63
63
|
name = {'Table': '表', 'Figure': '图'}[kind]
|
|
64
|
-
number = self.
|
|
64
|
+
number = self.chapter_number(r'STYLEREF 1 \s', 'SEQ ' + kind + r' \* ARABIC \s 1', bookmark)
|
|
65
65
|
return self.paragraph(f'{name} {number} {text}',
|
|
66
66
|
'Table Caption' if kind == 'Table' else -35)
|
|
67
67
|
|
|
68
|
+
def chapter_number(self, chapter_code, sequence_code, bookmark):
|
|
69
|
+
"""Return「章号-序号」 placeholders; ``bookmark`` spans both fields so REF shows the whole number."""
|
|
70
|
+
chapter = self.field_token(chapter_code)
|
|
71
|
+
number = self.field_token(sequence_code)
|
|
72
|
+
index = len(self._pending_numbers)
|
|
73
|
+
begin, end = f'@NUMSTART{index}@', f'@NUMEND{index}@'
|
|
74
|
+
self._pending_numbers.append((begin, end, bookmark))
|
|
75
|
+
return f'{begin}{chapter}-{number}{end}'
|
|
76
|
+
|
|
68
77
|
def list(self, items, ordered=False):
|
|
69
78
|
doc = self.doc
|
|
70
79
|
list_start = doc.Content.End - 1
|
|
@@ -100,12 +109,8 @@ class Composer:
|
|
|
100
109
|
boldSymbol=bold_symbol))
|
|
101
110
|
if inline:
|
|
102
111
|
return self.paragraph(before + token + after)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
index = len(self._pending_numbers)
|
|
106
|
-
begin, end = f'@NUMSTART{index}@', f'@NUMEND{index}@'
|
|
107
|
-
self._pending_numbers.append((begin, end, bookmark))
|
|
108
|
-
return self.paragraph(f'\t{token}\t({begin}{chapter}-{number}{end})', 'Equation')
|
|
112
|
+
number = self.chapter_number(r'STYLEREF 1 \s', r'SEQ Equation \* ARABIC \s 1', bookmark)
|
|
113
|
+
return self.paragraph(f'\t{token}\t({number})', 'Equation')
|
|
109
114
|
|
|
110
115
|
def footnote(self, token, text):
|
|
111
116
|
rng = self.doc.Content.Duplicate
|
|
@@ -313,12 +318,26 @@ class ContentComposer(Composer):
|
|
|
313
318
|
setattr(run.Font, {'strong': 'Bold', 'em': 'Italic', 'sup': 'Superscript', 'sub': 'Subscript'}[mark], True)
|
|
314
319
|
return rng
|
|
315
320
|
|
|
321
|
+
def chapter_sequence(self, kind, bookmark):
|
|
322
|
+
"""「章号-序号」 for an equation, figure or table in the current top-level chapter."""
|
|
323
|
+
if self.chapter_is_appendix:
|
|
324
|
+
# The SEQ "\s 1" restart only fires on built-in Heading 1
|
|
325
|
+
# paragraphs; the template's Appendix Heading is Normal-based,
|
|
326
|
+
# so appendix sequences never restart there. Giving each
|
|
327
|
+
# appendix its own SEQ identifier keeps a natural 1, 2, 3 …
|
|
328
|
+
# count that renumbers on insert or delete like main chapters,
|
|
329
|
+
# without touching heading styles or their linked lists.
|
|
330
|
+
letter = chr(ord('A') + self.appendix_count - 1)
|
|
331
|
+
return self.chapter_number(r'STYLEREF "Appendix Heading" \s',
|
|
332
|
+
'SEQ ' + kind + 'Appendix' + letter + r' \* ARABIC', bookmark)
|
|
333
|
+
return self.chapter_number(r'STYLEREF 1 \s', 'SEQ ' + kind + r' \* ARABIC \s 1', bookmark)
|
|
334
|
+
|
|
316
335
|
def numbered_caption(self, node, kind):
|
|
317
336
|
from .content import bookmark
|
|
318
337
|
numbered = node.get('numbered', True)
|
|
319
338
|
prefix = ''
|
|
320
339
|
if numbered:
|
|
321
|
-
prefix = {'Figure': '图 ', 'Table': '表 '}[kind] + self.
|
|
340
|
+
prefix = {'Figure': '图 ', 'Table': '表 '}[kind] + self.chapter_sequence(kind, bookmark(node['id'], '_n')) + ' '
|
|
322
341
|
rng = self.rich_paragraph(node['caption'], 'Table Caption' if kind == 'Table' else -35, prefix=prefix)
|
|
323
342
|
title = self.part(rng, utf16_length(prefix), rng.End - rng.Start - utf16_length(prefix) - 1)
|
|
324
343
|
self.mark(node['id'], title, '_t')
|
|
@@ -416,24 +435,9 @@ class ContentComposer(Composer):
|
|
|
416
435
|
token = self.math_token(node, False)
|
|
417
436
|
suffix = ''
|
|
418
437
|
if node['numbered']:
|
|
419
|
-
chapter = self.field_token(r'STYLEREF 1 \s' if self.chapter_style == 'Heading 1' else r'STYLEREF "Appendix Heading" \s')
|
|
420
|
-
if self.chapter_is_appendix:
|
|
421
|
-
# The SEQ "\s 1" restart only fires on built-in Heading 1
|
|
422
|
-
# paragraphs; the template's Appendix Heading is Normal-based,
|
|
423
|
-
# so appendix sequences never restart there. Giving each
|
|
424
|
-
# appendix its own SEQ identifier keeps a natural 1, 2, 3 …
|
|
425
|
-
# count that renumbers on insert or delete like main chapters,
|
|
426
|
-
# without touching heading styles or their linked lists.
|
|
427
|
-
letter = chr(ord('A') + self.appendix_count - 1)
|
|
428
|
-
number = self.field_token(r'SEQ EquationAppendix' + letter + r' \* ARABIC')
|
|
429
|
-
else:
|
|
430
|
-
number = self.field_token(r'SEQ Equation \* ARABIC \s 1')
|
|
431
|
-
self.chapter_first_equation = False
|
|
432
|
-
i = len(self._pending_numbers)
|
|
433
|
-
begin, end = f'@NUMSTART{i}@', f'@NUMEND{i}@'
|
|
434
438
|
from .content import bookmark
|
|
435
|
-
self.
|
|
436
|
-
suffix =
|
|
439
|
+
self.chapter_first_equation = False
|
|
440
|
+
suffix = '\t(' + self.chapter_sequence('Equation', bookmark(node['id'], '_n')) + ')'
|
|
437
441
|
rng = self.paragraph('\t' + token + suffix, 'Equation')
|
|
438
442
|
rng.ParagraphFormat.TabStops.ClearAll()
|
|
439
443
|
rng.ParagraphFormat.TabStops.Add(self.width / 2, 1, 0)
|
|
@@ -166,7 +166,10 @@ def validate_content(content, directory):
|
|
|
166
166
|
blocks(item["children"], depth + 1, scope, mode, tables)
|
|
167
167
|
elif kind == "equation" and item["numbered"]:
|
|
168
168
|
require(chapter is not None, f"{item['id']}: numbered equation outside a chapter")
|
|
169
|
+
elif kind == "figure":
|
|
170
|
+
require(chapter is not None, f"{item['id']}: numbered figure outside a chapter")
|
|
169
171
|
elif kind == "table":
|
|
172
|
+
require(chapter is not None or not item["numbered"], f"{item['id']}: numbered table outside a chapter")
|
|
170
173
|
require(tables < 6, f"{item['id']}: Word renderer supports at most six table levels")
|
|
171
174
|
for row in item["rows"]:
|
|
172
175
|
for cell in row["cells"]:
|