dsh-thesis 1.0.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.
Files changed (188) hide show
  1. package/DESIGN.md +390 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE.md +10 -0
  4. package/README.md +253 -0
  5. package/cordis.patch.yml +17 -0
  6. package/docs/host-api.md +640 -0
  7. package/lib/codewalk/commands.d.ts +23 -0
  8. package/lib/codewalk/commands.js +168 -0
  9. package/lib/codewalk/engine.d.ts +108 -0
  10. package/lib/codewalk/engine.js +449 -0
  11. package/lib/codewalk/errors.d.ts +31 -0
  12. package/lib/codewalk/errors.js +20 -0
  13. package/lib/codewalk/fs-types.d.ts +34 -0
  14. package/lib/codewalk/fs-types.js +10 -0
  15. package/lib/codewalk/gate.d.ts +30 -0
  16. package/lib/codewalk/gate.js +60 -0
  17. package/lib/codewalk/host-types.d.ts +198 -0
  18. package/lib/codewalk/host-types.js +11 -0
  19. package/lib/codewalk/prestep.d.ts +17 -0
  20. package/lib/codewalk/prestep.js +62 -0
  21. package/lib/codewalk/register.d.ts +23 -0
  22. package/lib/codewalk/register.js +31 -0
  23. package/lib/codewalk/skill.d.ts +15 -0
  24. package/lib/codewalk/skill.js +57 -0
  25. package/lib/codewalk/tools.d.ts +18 -0
  26. package/lib/codewalk/tools.js +314 -0
  27. package/lib/codewalk/types.d.ts +151 -0
  28. package/lib/codewalk/types.js +9 -0
  29. package/lib/commands.d.ts +33 -0
  30. package/lib/commands.js +60 -0
  31. package/lib/config.d.ts +125 -0
  32. package/lib/config.js +139 -0
  33. package/lib/dedup/commands.d.ts +21 -0
  34. package/lib/dedup/commands.js +93 -0
  35. package/lib/dedup/index.d.ts +75 -0
  36. package/lib/dedup/index.js +331 -0
  37. package/lib/dedup/normalize.d.ts +125 -0
  38. package/lib/dedup/normalize.js +329 -0
  39. package/lib/dedup/report.d.ts +114 -0
  40. package/lib/dedup/report.js +309 -0
  41. package/lib/dedup/rewrite.d.ts +71 -0
  42. package/lib/dedup/rewrite.js +228 -0
  43. package/lib/dedup/similarity.d.ts +169 -0
  44. package/lib/dedup/similarity.js +323 -0
  45. package/lib/gates/dangerous.d.ts +22 -0
  46. package/lib/gates/dangerous.js +47 -0
  47. package/lib/gates/detect.d.ts +54 -0
  48. package/lib/gates/detect.js +106 -0
  49. package/lib/gates/messages.d.ts +39 -0
  50. package/lib/gates/messages.js +74 -0
  51. package/lib/gates/prompt.d.ts +21 -0
  52. package/lib/gates/prompt.js +56 -0
  53. package/lib/gates/writing-intent.d.ts +49 -0
  54. package/lib/gates/writing-intent.js +68 -0
  55. package/lib/index.d.ts +36 -0
  56. package/lib/index.js +76 -0
  57. package/lib/ingest/code.d.ts +71 -0
  58. package/lib/ingest/code.js +356 -0
  59. package/lib/ingest/commands.d.ts +23 -0
  60. package/lib/ingest/commands.js +74 -0
  61. package/lib/ingest/docx.d.ts +29 -0
  62. package/lib/ingest/docx.js +92 -0
  63. package/lib/ingest/index.d.ts +179 -0
  64. package/lib/ingest/index.js +640 -0
  65. package/lib/ingest/ooxml.d.ts +58 -0
  66. package/lib/ingest/ooxml.js +393 -0
  67. package/lib/ingest/pdf.d.ts +86 -0
  68. package/lib/ingest/pdf.js +1016 -0
  69. package/lib/ingest/pptx.d.ts +32 -0
  70. package/lib/ingest/pptx.js +116 -0
  71. package/lib/ingest/text.d.ts +25 -0
  72. package/lib/ingest/text.js +180 -0
  73. package/lib/ingest/types.d.ts +48 -0
  74. package/lib/ingest/types.js +47 -0
  75. package/lib/ingest/xlsx.d.ts +72 -0
  76. package/lib/ingest/xlsx.js +411 -0
  77. package/lib/ingest/zip.d.ts +76 -0
  78. package/lib/ingest/zip.js +393 -0
  79. package/lib/intake/definitions.d.ts +65 -0
  80. package/lib/intake/definitions.js +42 -0
  81. package/lib/intake/gate-rules.d.ts +57 -0
  82. package/lib/intake/gate-rules.js +118 -0
  83. package/lib/intake/gate.d.ts +24 -0
  84. package/lib/intake/gate.js +86 -0
  85. package/lib/intake/index.d.ts +76 -0
  86. package/lib/intake/index.js +468 -0
  87. package/lib/intake/questions.d.ts +169 -0
  88. package/lib/intake/questions.js +720 -0
  89. package/lib/intake/spec.d.ts +66 -0
  90. package/lib/intake/spec.js +221 -0
  91. package/lib/intake/state.d.ts +192 -0
  92. package/lib/intake/state.js +420 -0
  93. package/lib/memory/recall.d.ts +69 -0
  94. package/lib/memory/recall.js +177 -0
  95. package/lib/memory/register.d.ts +37 -0
  96. package/lib/memory/register.js +52 -0
  97. package/lib/memory/service.d.ts +39 -0
  98. package/lib/memory/service.js +39 -0
  99. package/lib/memory/store.d.ts +45 -0
  100. package/lib/memory/store.js +123 -0
  101. package/lib/memory/tools.d.ts +19 -0
  102. package/lib/memory/tools.js +195 -0
  103. package/lib/memory/types.d.ts +55 -0
  104. package/lib/memory/types.js +6 -0
  105. package/lib/paper/commands.d.ts +13 -0
  106. package/lib/paper/commands.js +84 -0
  107. package/lib/paper/lib/aicheck.d.ts +38 -0
  108. package/lib/paper/lib/aicheck.js +241 -0
  109. package/lib/paper/lib/bibtex.d.ts +21 -0
  110. package/lib/paper/lib/bibtex.js +114 -0
  111. package/lib/paper/lib/check.d.ts +32 -0
  112. package/lib/paper/lib/check.js +215 -0
  113. package/lib/paper/lib/defense.d.ts +48 -0
  114. package/lib/paper/lib/defense.js +220 -0
  115. package/lib/paper/lib/docx.d.ts +35 -0
  116. package/lib/paper/lib/docx.js +213 -0
  117. package/lib/paper/lib/layout.d.ts +41 -0
  118. package/lib/paper/lib/layout.js +572 -0
  119. package/lib/paper/lib/ledger.d.ts +112 -0
  120. package/lib/paper/lib/ledger.js +349 -0
  121. package/lib/paper/lib/lit-api.d.ts +59 -0
  122. package/lib/paper/lib/lit-api.js +296 -0
  123. package/lib/paper/lib/lit-cache.d.ts +32 -0
  124. package/lib/paper/lib/lit-cache.js +91 -0
  125. package/lib/paper/lib/project.d.ts +29 -0
  126. package/lib/paper/lib/project.js +93 -0
  127. package/lib/paper/lib/review.d.ts +37 -0
  128. package/lib/paper/lib/review.js +194 -0
  129. package/lib/paper/lib/zip.d.ts +14 -0
  130. package/lib/paper/lib/zip.js +124 -0
  131. package/lib/paper/register.d.ts +20 -0
  132. package/lib/paper/register.js +183 -0
  133. package/lib/paper/tools/aicheck.d.ts +6 -0
  134. package/lib/paper/tools/aicheck.js +38 -0
  135. package/lib/paper/tools/build.d.ts +27 -0
  136. package/lib/paper/tools/build.js +166 -0
  137. package/lib/paper/tools/check.d.ts +7 -0
  138. package/lib/paper/tools/check.js +79 -0
  139. package/lib/paper/tools/decision.d.ts +13 -0
  140. package/lib/paper/tools/decision.js +53 -0
  141. package/lib/paper/tools/defense.d.ts +11 -0
  142. package/lib/paper/tools/defense.js +74 -0
  143. package/lib/paper/tools/init.d.ts +23 -0
  144. package/lib/paper/tools/init.js +99 -0
  145. package/lib/paper/tools/lit.d.ts +27 -0
  146. package/lib/paper/tools/lit.js +236 -0
  147. package/lib/paper/tools/progress.d.ts +18 -0
  148. package/lib/paper/tools/progress.js +77 -0
  149. package/lib/paper/tools/review.d.ts +11 -0
  150. package/lib/paper/tools/review.js +41 -0
  151. package/lib/ppt/actions.d.ts +97 -0
  152. package/lib/ppt/actions.js +263 -0
  153. package/lib/ppt/check.d.ts +64 -0
  154. package/lib/ppt/check.js +185 -0
  155. package/lib/ppt/commands.d.ts +28 -0
  156. package/lib/ppt/commands.js +71 -0
  157. package/lib/ppt/convert.d.ts +146 -0
  158. package/lib/ppt/convert.js +423 -0
  159. package/lib/ppt/index.d.ts +45 -0
  160. package/lib/ppt/index.js +57 -0
  161. package/lib/ppt/marp.d.ts +74 -0
  162. package/lib/ppt/marp.js +165 -0
  163. package/lib/ppt/outline.d.ts +206 -0
  164. package/lib/ppt/outline.js +695 -0
  165. package/lib/shared/define-tool.d.ts +77 -0
  166. package/lib/shared/define-tool.js +235 -0
  167. package/lib/shared/fs-errors.d.ts +50 -0
  168. package/lib/shared/fs-errors.js +90 -0
  169. package/lib/shared/index.d.ts +31 -0
  170. package/lib/shared/index.js +32 -0
  171. package/lib/shared/spawn.d.ts +38 -0
  172. package/lib/shared/spawn.js +50 -0
  173. package/lib/shared/text.d.ts +14 -0
  174. package/lib/shared/text.js +26 -0
  175. package/package.json +82 -0
  176. package/skills/code-walkthrough/SKILL.md +94 -0
  177. package/skills/code-walkthrough/references/examples.md +129 -0
  178. package/skills/paper-dedup/SKILL.md +58 -0
  179. package/skills/paper-intake/SKILL.md +64 -0
  180. package/skills/thesis-citation/SKILL.md +67 -0
  181. package/skills/thesis-defense/SKILL.md +87 -0
  182. package/skills/thesis-eng-design/SKILL.md +97 -0
  183. package/skills/thesis-inquiry/SKILL.md +55 -0
  184. package/skills/thesis-inquiry/references/intent-spec-template.md +39 -0
  185. package/skills/thesis-literature/SKILL.md +56 -0
  186. package/skills/thesis-opener/SKILL.md +77 -0
  187. package/skills/thesis-pipeline/SKILL.md +121 -0
  188. package/skills/thesis-writing/SKILL.md +86 -0
@@ -0,0 +1,58 @@
1
+ /**
2
+ * 极简 OOXML 文本抽取器:**不是**通用 XML 解析器,只服务 docx/pptx 的文本层。
3
+ *
4
+ * 覆盖范围(够用即止,不追求完整 XML 语义):
5
+ * - 提取 `<w:t>` / `<a:t>` 等文本节点,处理 `&amp; &lt; &gt; &quot; &apos;`
6
+ * 与十进制/十六进制数字实体;
7
+ * - 按 `<w:p>` / `<a:p>` 切段落;`<w:br>`/`<w:cr>`/`<a:br>` 变换行;
8
+ * `<w:tab/>` 变制表符;
9
+ * - 表格:一行渲染成一条 tab 分隔文本(`<w:tc>` 前加制表符、`<w:tr>` 结束收行),
10
+ * 单元格内的多个段落用空格拼接;
11
+ * - 跳过 `<w:instrText>`(域代码,不是正文)、`<w:delText>`(修订删除的旧文本)、
12
+ * `w:commentRangeStart..End` 区间(批注锚点,批注正文不在 document.xml 里);
13
+ * - 段内换行折叠为空格、首尾空白去掉,但**制表符保留**(表格列分隔依赖它)。
14
+ *
15
+ * 输出统一是 `string[]`(一段一行),由 docx/pptx 模块拼装。
16
+ *
17
+ * @module dsh-thesis/ingest
18
+ */
19
+ /** 把 XML 实体解码成字符;无法识别/越界的实体原样保留(不猜)。 */
20
+ export declare function decodeXmlEntities(input: string): string;
21
+ /** 扫描配置。 */
22
+ export interface OoxmlScanConfig {
23
+ /** 段落边界标签(`w:p`)。 */
24
+ readonly paragraphTags: readonly string[];
25
+ /** 文本节点标签(`w:t`);只取这些标签内的字符数据。 */
26
+ readonly textTags: readonly string[];
27
+ /** 换行标签(`w:br`/`w:cr`/`a:br`)。 */
28
+ readonly breakTags: readonly string[];
29
+ /** 制表标签(`w:tab`)。 */
30
+ readonly tabTags: readonly string[];
31
+ /** 表格单元格标签(`w:tc`):开始前输出一个制表符。 */
32
+ readonly cellTags: readonly string[];
33
+ /** 表格行标签(`w:tr`):结束后收一行。 */
34
+ readonly rowTags: readonly string[];
35
+ /** 整段跳过的标签(域代码/修订删除文本/批注区间起点)。 */
36
+ readonly skipTags: readonly string[];
37
+ /** 只抽取该标签内部(如 `dc:title`);缺省扫描全文。 */
38
+ readonly wantedTag?: string;
39
+ }
40
+ /** docx 正文抽取配置。 */
41
+ export declare const DOCX_PARTS: OoxmlScanConfig;
42
+ /** pptx 幻灯片抽取配置(`a:p`/`a:t`,`a:br` 换行)。 */
43
+ export declare const PPTX_PARTS: OoxmlScanConfig;
44
+ /** 对外主入口:按配置抽取段落。 */
45
+ export declare function extractOoxmlParagraphs(xml: string, config: OoxmlScanConfig): string[];
46
+ /** docx 正文 → 段落数组。 */
47
+ export declare function extractDocxParagraphs(documentXml: string): string[];
48
+ /** pptx 幻灯片 → 段落数组。 */
49
+ export declare function extractPptxParagraphs(slideXml: string): string[];
50
+ /** 判断段落数组里是否有可见文本(用于"扫不出字"的判断)。 */
51
+ export declare function hasVisibleText(paragraphs: readonly string[]): boolean;
52
+ /**
53
+ * 抽取某个标签内部的所有文本(用于 `dc:title` 这类零散场景)。
54
+ * 内部同样按 `<w:t>`/`<a:t>` 文本节点抽取,不含段落结构。
55
+ */
56
+ export declare function extractElementText(xml: string, wantedTag: string, textTags?: readonly string[]): string;
57
+ /** 抽取特定标签的文本内容,如 `dc:title`;找不到返回 undefined。 */
58
+ export declare function extractRawElement(xml: string, wantedNames: readonly string[]): string | undefined;
@@ -0,0 +1,393 @@
1
+ /**
2
+ * 极简 OOXML 文本抽取器:**不是**通用 XML 解析器,只服务 docx/pptx 的文本层。
3
+ *
4
+ * 覆盖范围(够用即止,不追求完整 XML 语义):
5
+ * - 提取 `<w:t>` / `<a:t>` 等文本节点,处理 `&amp; &lt; &gt; &quot; &apos;`
6
+ * 与十进制/十六进制数字实体;
7
+ * - 按 `<w:p>` / `<a:p>` 切段落;`<w:br>`/`<w:cr>`/`<a:br>` 变换行;
8
+ * `<w:tab/>` 变制表符;
9
+ * - 表格:一行渲染成一条 tab 分隔文本(`<w:tc>` 前加制表符、`<w:tr>` 结束收行),
10
+ * 单元格内的多个段落用空格拼接;
11
+ * - 跳过 `<w:instrText>`(域代码,不是正文)、`<w:delText>`(修订删除的旧文本)、
12
+ * `w:commentRangeStart..End` 区间(批注锚点,批注正文不在 document.xml 里);
13
+ * - 段内换行折叠为空格、首尾空白去掉,但**制表符保留**(表格列分隔依赖它)。
14
+ *
15
+ * 输出统一是 `string[]`(一段一行),由 docx/pptx 模块拼装。
16
+ *
17
+ * @module dsh-thesis/ingest
18
+ */
19
+ // ---------------------------------------------------------------------------
20
+ // 实体解码
21
+ // ---------------------------------------------------------------------------
22
+ /** 命名实体表(OOXML 里实际会出现的就这几个,再加 HTML 的 nbsp)。 */
23
+ const NAMED_ENTITIES = {
24
+ amp: '&',
25
+ lt: '<',
26
+ gt: '>',
27
+ quot: '"',
28
+ apos: "'",
29
+ nbsp: '\u00a0',
30
+ };
31
+ /** 把 XML 实体解码成字符;无法识别/越界的实体原样保留(不猜)。 */
32
+ export function decodeXmlEntities(input) {
33
+ if (!input.includes('&'))
34
+ return input;
35
+ return input.replace(/&(#x[0-9a-fA-F]+|#[0-9]+|[a-zA-Z][a-zA-Z0-9]*);/g, (match, body) => {
36
+ if (body.startsWith('#x') || body.startsWith('#X')) {
37
+ const code = Number.parseInt(body.slice(2), 16);
38
+ return validCodePoint(code) ? String.fromCodePoint(code) : match;
39
+ }
40
+ if (body.startsWith('#')) {
41
+ const code = Number.parseInt(body.slice(1), 10);
42
+ return validCodePoint(code) ? String.fromCodePoint(code) : match;
43
+ }
44
+ return NAMED_ENTITIES[body.toLowerCase()] ?? match;
45
+ });
46
+ }
47
+ /** 合法 Unicode 码位(排除代理区与超出 0x10FFFF 的值)。 */
48
+ function validCodePoint(code) {
49
+ if (!Number.isFinite(code) || code < 0 || code > 0x10ffff)
50
+ return false;
51
+ return !(code >= 0xd800 && code <= 0xdfff);
52
+ }
53
+ // ---------------------------------------------------------------------------
54
+ // 基础工具
55
+ // ---------------------------------------------------------------------------
56
+ /**
57
+ * 折叠段内空白:行内换行折叠为空格、去掉首尾空格,但保留制表符与内部空格。
58
+ * (制表符是表格列分隔符,不能动。)
59
+ */
60
+ function collapseParagraph(raw) {
61
+ return raw
62
+ .replace(/[\r\n\u2028\u2029]+/g, ' ')
63
+ .replace(/[ \t]+$/, '')
64
+ .replace(/^ +/, '');
65
+ }
66
+ /** 命名空间感知的标签集合:短名(`p`)或带前缀全名(`w:p`)写法都接受。 */
67
+ function tagMatches(names, full, short) {
68
+ for (const name of names) {
69
+ if (name === full)
70
+ return true;
71
+ if (name.indexOf(':') === -1 && name === short)
72
+ return true;
73
+ }
74
+ return false;
75
+ }
76
+ function shortName(full) {
77
+ const colon = full.lastIndexOf(':');
78
+ return colon === -1 ? full : full.slice(colon + 1);
79
+ }
80
+ /** docx 正文抽取配置。 */
81
+ export const DOCX_PARTS = {
82
+ paragraphTags: ['w:p'],
83
+ textTags: ['w:t'],
84
+ breakTags: ['w:br', 'w:cr'],
85
+ tabTags: ['w:tab'],
86
+ cellTags: ['w:tc'],
87
+ rowTags: ['w:tr'],
88
+ skipTags: ['w:instrText', 'w:delText', 'w:commentRangeStart'],
89
+ };
90
+ /** pptx 幻灯片抽取配置(`a:p`/`a:t`,`a:br` 换行)。 */
91
+ export const PPTX_PARTS = {
92
+ paragraphTags: ['a:p'],
93
+ textTags: ['a:t'],
94
+ breakTags: ['a:br'],
95
+ tabTags: [],
96
+ cellTags: ['a:tc'],
97
+ rowTags: ['a:tr'],
98
+ skipTags: [],
99
+ };
100
+ /**
101
+ * 跳过区间的配对查找:`w:instrText`/`w:delText` 自配对;
102
+ * `w:commentRangeStart`(自闭合)配对到 `w:commentRangeEnd`。
103
+ */
104
+ function skipPair(name) {
105
+ if (name === 'w:commentRangeStart')
106
+ return { start: 'w:commentRangeStart', end: 'w:commentRangeEnd' };
107
+ if (name === 'w:instrText' || name === 'w:delText')
108
+ return { start: name, end: name };
109
+ return undefined;
110
+ }
111
+ // ---------------------------------------------------------------------------
112
+ // 主扫描器
113
+ // ---------------------------------------------------------------------------
114
+ /**
115
+ * 段落抽取主循环。
116
+ *
117
+ * 状态:
118
+ * 1. `wanted` —— 是否在「只抽指定标签」的区间内(缺省恒真);
119
+ * 2. `skipName` —— 非空表示正在跳过(域代码区间 / 批注锚点区间);
120
+ * 3. `inText` —— 当前在文本节点内,此时的字符数据才被收集;
121
+ * 4. `inCell`/`cellText` —— 表格单元格内的段落先攒进 cellText;
122
+ * 5. `rowText` —— 一整行的 tab 分隔文本,行闭合时产出。
123
+ *
124
+ * 文本与结构分离:`segment` 负责"一段文本内部"(tab/换行/多 run 拼接),
125
+ * 表格状态只决定这一段写到 cellText 还是直接产出一行。
126
+ */
127
+ function parseOoxml(xml, config) {
128
+ const paragraphs = [];
129
+ let segment = '';
130
+ let inText = false;
131
+ let textChunk = '';
132
+ // 跳过区间
133
+ let skipName;
134
+ let skipEnd = '';
135
+ let skipDepth = 0;
136
+ // 只抽指定标签
137
+ const wantsTag = config.wantedTag !== undefined;
138
+ let wanted = !wantsTag;
139
+ const wantedShort = wantsTag ? shortName(config.wantedTag) : '';
140
+ let wantedDepth = 0;
141
+ // 表格
142
+ let inCell = false;
143
+ let inRow = false;
144
+ let cellText = '';
145
+ let rowText = '';
146
+ const hasRows = config.rowTags.length > 0;
147
+ const isRow = (full, short) => hasRows && tagMatches(config.rowTags, full, short);
148
+ const isCell = (full, short) => config.cellTags.length > 0 && tagMatches(config.cellTags, full, short);
149
+ const isParagraph = (full, short) => tagMatches(config.paragraphTags, full, short);
150
+ const isTextNode = (full, short) => tagMatches(config.textTags, full, short);
151
+ /** 收掉当前文本节点的字符数据,作为一个"片段"并进 segment。 */
152
+ const pushSegment = () => {
153
+ const text = collapseParagraph(decodeXmlEntities(textChunk));
154
+ textChunk = '';
155
+ inText = false;
156
+ if (text === '')
157
+ return;
158
+ if (segment === '')
159
+ segment = text;
160
+ else if (segment.endsWith('\t') || segment.endsWith('\n'))
161
+ segment += text;
162
+ else
163
+ segment += ` ${text}`;
164
+ };
165
+ /** 段落级收尾:把 segment 并进当前容器(单元格 → 行 → 普通段落)。 */
166
+ const finishSegment = () => {
167
+ if (inText)
168
+ pushSegment();
169
+ const trimmed = segment.replace(/[ \t]+$/, '');
170
+ segment = '';
171
+ if (trimmed.trim() === '')
172
+ return;
173
+ if (inCell)
174
+ cellText = cellText === '' ? trimmed : `${cellText} ${trimmed}`;
175
+ else if (inRow)
176
+ rowText = rowText === '' ? trimmed : `${rowText} ${trimmed}`;
177
+ else
178
+ paragraphs.push(trimmed);
179
+ };
180
+ /** 单元格收尾:把攒下的文本作为一列拼进行(列前带制表符)。 */
181
+ const finishCell = () => {
182
+ finishSegment();
183
+ const text = cellText.trim();
184
+ cellText = '';
185
+ inCell = false;
186
+ if (text === '')
187
+ return;
188
+ rowText = rowText === '' ? `\t${text}` : `${rowText}\t${text}`;
189
+ };
190
+ /** 行收尾:产出一整行 tab 分隔文本。 */
191
+ const finishRow = () => {
192
+ if (inCell) {
193
+ // 结构异常(行没闭合就遇到新行/文档结束):按已有内容收尾。
194
+ finishCell();
195
+ }
196
+ else {
197
+ finishSegment();
198
+ }
199
+ const text = rowText.trim();
200
+ rowText = '';
201
+ inRow = false;
202
+ if (text !== '')
203
+ paragraphs.push(text);
204
+ };
205
+ let i = 0;
206
+ while (i < xml.length) {
207
+ const lt = xml.indexOf('<', i);
208
+ if (lt === -1) {
209
+ if (inText && skipName === undefined && wanted)
210
+ textChunk += xml.slice(i);
211
+ break;
212
+ }
213
+ if (lt > i && inText && skipName === undefined && wanted)
214
+ textChunk += xml.slice(i, lt);
215
+ if (xml.startsWith('<!--', lt)) {
216
+ const end = xml.indexOf('-->', lt + 4);
217
+ i = end === -1 ? xml.length : end + 3;
218
+ continue;
219
+ }
220
+ if (xml.startsWith('<![CDATA[', lt)) {
221
+ const end = xml.indexOf(']]>', lt + 9);
222
+ const body = xml.slice(lt + 9, end === -1 ? xml.length : end);
223
+ if (inText && skipName === undefined && wanted)
224
+ textChunk += body;
225
+ i = end === -1 ? xml.length : end + 3;
226
+ continue;
227
+ }
228
+ if (xml.startsWith('<?', lt) || xml.startsWith('<!', lt)) {
229
+ const end = xml.indexOf('>', lt);
230
+ i = end === -1 ? xml.length : end + 1;
231
+ continue;
232
+ }
233
+ const gt = xml.indexOf('>', lt);
234
+ if (gt === -1)
235
+ break; // 尾部残标签:忽略,不当作文本
236
+ const body = xml.slice(lt + 1, gt);
237
+ i = gt + 1;
238
+ const nameMatch = /^\/?\s*([A-Za-z_][\w.:-]*)/.exec(body);
239
+ if (nameMatch === null)
240
+ continue;
241
+ const isEnd = body.startsWith('/');
242
+ const fullName = nameMatch[1];
243
+ const short = shortName(fullName);
244
+ const selfClosing = body.endsWith('/');
245
+ // ---- 只抽指定标签内部 ----
246
+ if (wantsTag) {
247
+ if (!isEnd && tagMatches([config.wantedTag], fullName, wantedShort) && !selfClosing) {
248
+ wanted = true;
249
+ wantedDepth += 1;
250
+ }
251
+ else if (isEnd && tagMatches([config.wantedTag], fullName, wantedShort) && wantedDepth > 0) {
252
+ wantedDepth -= 1;
253
+ if (wantedDepth === 0)
254
+ wanted = false;
255
+ }
256
+ if (!wanted)
257
+ continue;
258
+ }
259
+ // ---- 跳过区间(域代码 / 修订删除 / 批注锚点) ----
260
+ if (skipName !== undefined) {
261
+ if (isEnd && tagMatches([skipEnd], fullName, shortName(skipEnd))) {
262
+ if (skipDepth > 1 && shortName(skipEnd) === shortName(skipName))
263
+ skipDepth -= 1;
264
+ else {
265
+ skipName = undefined;
266
+ skipEnd = '';
267
+ skipDepth = 0;
268
+ }
269
+ }
270
+ else if (!isEnd && !selfClosing && tagMatches([skipName], fullName, shortName(skipName))) {
271
+ skipDepth += 1;
272
+ }
273
+ continue;
274
+ }
275
+ if (!isEnd) {
276
+ // 区间起点既可能是成对标签(<w:instrText>…</w:instrText>),
277
+ // 也可能是自闭合标签(<w:commentRangeStart/>),两种都要进入跳过态。
278
+ const hit = config.skipTags.find(tag => tagMatches([tag], fullName, short));
279
+ if (hit !== undefined) {
280
+ const pair = skipPair(hit);
281
+ if (pair !== undefined) {
282
+ skipName = pair.start;
283
+ skipEnd = pair.end;
284
+ skipDepth = 1;
285
+ }
286
+ continue;
287
+ }
288
+ }
289
+ // ---- 文本节点 ----
290
+ if (isTextNode(fullName, short)) {
291
+ if (isEnd) {
292
+ if (inText)
293
+ pushSegment();
294
+ }
295
+ else if (selfClosing) {
296
+ // `<w:t/>`:空文本,什么也不做。
297
+ }
298
+ else {
299
+ if (inText)
300
+ pushSegment(); // 非法嵌套:先把上一段收掉
301
+ inText = true;
302
+ }
303
+ continue;
304
+ }
305
+ // ---- 结构控制 ----
306
+ if (!isEnd && tagMatches(config.breakTags, fullName, short)) {
307
+ segment += '\n';
308
+ continue;
309
+ }
310
+ if (!isEnd && tagMatches(config.tabTags, fullName, short)) {
311
+ if (inText)
312
+ pushSegment();
313
+ segment += '\t';
314
+ continue;
315
+ }
316
+ if (!isEnd && isCell(fullName, short)) {
317
+ if (!inCell) {
318
+ finishSegment();
319
+ inCell = true;
320
+ cellText = '';
321
+ }
322
+ continue;
323
+ }
324
+ if (isEnd && isCell(fullName, short)) {
325
+ if (inCell)
326
+ finishCell();
327
+ continue;
328
+ }
329
+ if (!isEnd && isRow(fullName, short)) {
330
+ if (inRow)
331
+ finishRow();
332
+ inRow = true;
333
+ rowText = '';
334
+ continue;
335
+ }
336
+ if (isEnd && isRow(fullName, short)) {
337
+ if (inRow)
338
+ finishRow();
339
+ continue;
340
+ }
341
+ if (isParagraph(fullName, short)) {
342
+ // 段落结束收尾;段落开始时不清 segment(同一单元格内多 run/多段会拼接)。
343
+ if (isEnd)
344
+ finishSegment();
345
+ continue;
346
+ }
347
+ }
348
+ if (inRow)
349
+ finishRow();
350
+ finishSegment();
351
+ return paragraphs;
352
+ }
353
+ // ---------------------------------------------------------------------------
354
+ // 对外入口
355
+ // ---------------------------------------------------------------------------
356
+ /** 对外主入口:按配置抽取段落。 */
357
+ export function extractOoxmlParagraphs(xml, config) {
358
+ return parseOoxml(xml, config);
359
+ }
360
+ /** docx 正文 → 段落数组。 */
361
+ export function extractDocxParagraphs(documentXml) {
362
+ return parseOoxml(documentXml, DOCX_PARTS);
363
+ }
364
+ /** pptx 幻灯片 → 段落数组。 */
365
+ export function extractPptxParagraphs(slideXml) {
366
+ return parseOoxml(slideXml, PPTX_PARTS);
367
+ }
368
+ /** 判断段落数组里是否有可见文本(用于"扫不出字"的判断)。 */
369
+ export function hasVisibleText(paragraphs) {
370
+ return paragraphs.some(paragraph => paragraph.trim() !== '');
371
+ }
372
+ /**
373
+ * 抽取某个标签内部的所有文本(用于 `dc:title` 这类零散场景)。
374
+ * 内部同样按 `<w:t>`/`<a:t>` 文本节点抽取,不含段落结构。
375
+ */
376
+ export function extractElementText(xml, wantedTag, textTags = ['t']) {
377
+ const paragraphs = parseOoxml(xml, {
378
+ paragraphTags: [],
379
+ textTags,
380
+ breakTags: [],
381
+ tabTags: [],
382
+ cellTags: [],
383
+ rowTags: [],
384
+ skipTags: [],
385
+ wantedTag,
386
+ });
387
+ return paragraphs.join(' ').trim();
388
+ }
389
+ /** 抽取特定标签的文本内容,如 `dc:title`;找不到返回 undefined。 */
390
+ export function extractRawElement(xml, wantedNames) {
391
+ const found = extractElementText(xml, wantedNames[0] ?? 'title', wantedNames);
392
+ return found === '' ? undefined : found;
393
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * `.pdf` **尽力而为**的文本抽取(零依赖)。
3
+ *
4
+ * 能抽出来的:
5
+ * - 文本型 PDF:解析间接对象、解 `FlateDecode`(`node:zlib` 的 `inflateRawSync`)、
6
+ * `ASCIIHexDecode`/`ASCII85Decode` 流;扫描内容流里 `BT…ET` 块中的
7
+ * `Tj` / `TJ` / `'` / `"` 操作数(含 `\(` `\)` `\\` 与八进制转义);
8
+ * - 有 `ToUnicode` CMap 的字体:用 CMap 还原字符(含中文、符号字体);
9
+ * - 走 `/Resources /Font` 的字体名 → CMap 映射,`Tf` 切换字体时同步切换解码表。
10
+ *
11
+ * **抽不出来的(会明确说,绝不编造)**:
12
+ * - 加密 PDF(`/Encrypt`):直接失败;
13
+ * - 扫描版 PDF(页面只有图片、没有文字对象):失败并提示"请提供 .docx 或复制正文";
14
+ * - 对象流(Object Stream)/ 交叉引用流:不解析,可能少读页面(有提示);
15
+ * - 只用 CID 编码、又没有 ToUnicode 的嵌入子集字体:字节无法还原成字符,
16
+ * 结果会被清成占位符并标 `lowConfidence`;
17
+ * - 表格/分栏:PDF 没有语义,行序按内容流顺序,可能错乱。
18
+ *
19
+ * 文字抽取率低(少于 200 字符但页数 > 3)时结果带 `lowConfidence: true`。
20
+ *
21
+ * @module dsh-thesis/ingest
22
+ */
23
+ import { type ExtractOptions, type ExtractResult } from './types.ts';
24
+ /** 单文件字节上限(PDF 可能很大,超过就不硬啃)。 */
25
+ export declare const PDF_MAX_BYTES: number;
26
+ /** 等值 latin1 解码(每字节一个码位,绝不丢数据)。 */
27
+ export declare function bytesToLatin1(bytes: Uint8Array): string;
28
+ type ObjectKind = 'page' | 'stream' | 'dict' | 'unknown';
29
+ interface PdfObject {
30
+ readonly num: number;
31
+ readonly dict: string;
32
+ readonly stream?: Uint8Array;
33
+ readonly kind: ObjectKind;
34
+ }
35
+ /** 解析全部间接对象(`N G obj … endobj`)。 */
36
+ export declare function parseObjects(raw: string): PdfObject[];
37
+ /** 按过滤器解码一个流;返回 undefined 表示方法不支持(调用方据此提示)。 */
38
+ export declare function decodeStream(data: Uint8Array, dict: string): Uint8Array | undefined;
39
+ interface FontMap {
40
+ readonly codeBytes: 1 | 2;
41
+ readonly map: Map<number, string>;
42
+ }
43
+ /** 解析 ToUnicode CMap 流 → 字体映射。 */
44
+ export declare function parseToUnicode(cmapText: string): FontMap;
45
+ interface FontResource {
46
+ readonly codeBytes: 1 | 2;
47
+ readonly map: Map<number, string> | undefined;
48
+ }
49
+ /** 解析一页(或全局)的 `/Resources` → 字体名到解码表的映射。 */
50
+ export declare function buildFontTables(raw: string, objects: readonly PdfObject[]): Map<string, FontResource>;
51
+ /** TJ 数组项:字符串或字距调整量。 */
52
+ type TJItem = {
53
+ readonly kind: 'string';
54
+ readonly bytes: number[];
55
+ } | {
56
+ readonly kind: 'number';
57
+ readonly value: number;
58
+ };
59
+ /** 解析数组正文(不含方括号)为「字符串 / 数字」项序列。 */
60
+ export declare function parseArrayItems(body: string): TJItem[];
61
+ /** 读一个 PDF 字符串字面量(调用时 `text[i]` 是 `(`)。 */
62
+ export declare function readPdfString(text: string, start: number): {
63
+ bytes: number[];
64
+ next: number;
65
+ };
66
+ interface ContentExtract {
67
+ readonly text: string;
68
+ readonly strings: number;
69
+ readonly unmappedSources: number;
70
+ }
71
+ /** 扫描一段内容流,抽取文本操作数。 */
72
+ export declare function extractContentText(content: string, fonts: Map<string, FontResource>): ContentExtract;
73
+ /**
74
+ * 读取 PDF 文本(内部函数,返回空串表示抽不出来)。
75
+ * 第二个返回值是提示信息。
76
+ */
77
+ export declare function readPdfText(bytes: Uint8Array): {
78
+ text: string;
79
+ note?: string;
80
+ lowConfidence: boolean;
81
+ reason?: string;
82
+ pages: number;
83
+ };
84
+ /** 门面用入口:pdf 字节 → 抽取结果。 */
85
+ export declare function extractPdf(bytes: Uint8Array, options: ExtractOptions): ExtractResult;
86
+ export {};