dsh-plugin-lookatstudy 0.11.0 → 0.12.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.
@@ -0,0 +1,55 @@
1
+ import { n as readZipText, t as readZip } from "./zip-reader-KnRrq0av.mjs";
2
+ //#region src/vendor/docx-parser.ts
3
+ function decodeEntities(s) {
4
+ return s.replace(/<[^>]+>/g, "").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, "\"").replace(/&apos;/g, "'").replace(/&amp;/g, "&");
5
+ }
6
+ function parseParagraphs(documentXml) {
7
+ const paras = [];
8
+ const pRe = /<w:p\b[^>]*\/>|<w:p\b[^>]*>[\s\S]*?<\/w:p>/g;
9
+ let pm;
10
+ while ((pm = pRe.exec(documentXml)) !== null) {
11
+ const p = pm[0];
12
+ const style = p.match(/<w:pStyle w:val="([^"]+)"/i)?.[1] ?? "";
13
+ let text = "";
14
+ const tRe = /<w:t(?:\s[^>]*)?>([\s\S]*?)<\/w:t>|<w:tab\b[^>]*\/>|<w:br\b[^>]*\/>/g;
15
+ let tm;
16
+ while ((tm = tRe.exec(p)) !== null) if (tm[1] !== void 0) text += decodeEntities(tm[1]);
17
+ else if (/w:tab/.test(tm[0])) text += " ";
18
+ else text += "\n";
19
+ paras.push({
20
+ style: style.trim(),
21
+ text
22
+ });
23
+ }
24
+ return paras;
25
+ }
26
+ function headingLevel(style) {
27
+ const s = style.toLowerCase().replace(/\s+/g, "");
28
+ const m = s.match(/heading(\d)/) ?? s.match(/titre(\d)/) ?? s.match(/berschrift(\d)/);
29
+ if (m) return Math.min(6, Math.max(1, Number(m[1])));
30
+ if (/^(title|titre)$/.test(s)) return 1;
31
+ return 0;
32
+ }
33
+ /** .docx → markdown。Word Heading 样式保真为 # 级标题,代码样式段落围栏。 */
34
+ function parseDocx(buf) {
35
+ const entries = readZip(buf);
36
+ const doc = readZipText(entries, "word/document.xml");
37
+ if (!doc) throw new Error("docx 结构异常:缺 word/document.xml");
38
+ const lines = [];
39
+ for (const para of parseParagraphs(doc)) {
40
+ const t = para.text.trim();
41
+ if (!t) {
42
+ lines.push("");
43
+ continue;
44
+ }
45
+ const lvl = headingLevel(para.style);
46
+ if (lvl > 0) lines.push("", "#".repeat(lvl) + " " + t, "");
47
+ else if (/code|sourcecode/i.test(para.style)) lines.push("```", t, "```", "");
48
+ else lines.push(t, "");
49
+ }
50
+ const md = lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
51
+ if (!md) throw new Error("docx 里没有可识别的文本");
52
+ return md;
53
+ }
54
+ //#endregion
55
+ export { parseDocx };
@@ -0,0 +1,306 @@
1
+ import { n as htmlToMarkdown } from "./html-article-Da8ksU0i.mjs";
2
+ import { n as readZipText, t as readZip } from "./zip-reader-KnRrq0av.mjs";
3
+ //#region src/vendor/epub-parser.ts
4
+ /** 提取某标签的全部出现(OPF 的属性顺序不定,先抓整标签再逐个提属性)。 */
5
+ function tags(xml, tagName) {
6
+ const out = [];
7
+ const re = new RegExp(`<${tagName}\\b[^>]*>`, "g");
8
+ let m;
9
+ while ((m = re.exec(xml)) !== null) {
10
+ const raw = m[0];
11
+ const attrs = {};
12
+ const attrRe = /([\w:-]+)\s*=\s*"([^"]*)"/g;
13
+ let a;
14
+ while ((a = attrRe.exec(raw)) !== null) attrs[a[1]] = a[2];
15
+ out.push({
16
+ raw,
17
+ attrs
18
+ });
19
+ }
20
+ return out;
21
+ }
22
+ function firstTagText(xml, tagName) {
23
+ return xml.match(new RegExp(`<${tagName}[^>]*>([^<]*)</${tagName}>`, "i"))?.[1]?.trim() ?? "";
24
+ }
25
+ /** zip 内路径归一:posix 分隔 + href 相对 OPF 目录解析。 */
26
+ function resolveZipPath(opfPath, href) {
27
+ const cleanHref = decodeURIComponent(href.split("#")[0] ?? href).replace(/\\/g, "/");
28
+ if (!opfPath.includes("/")) return cleanHref.replace(/^\.\//, "");
29
+ const parts = `${opfPath.slice(0, opfPath.lastIndexOf("/"))}/${cleanHref}`.split("/");
30
+ const resolved = [];
31
+ for (const p of parts) if (p === "..") resolved.pop();
32
+ else if (p !== "." && p !== "") resolved.push(p);
33
+ return resolved.join("/");
34
+ }
35
+ /** EPUB3 nav.xhtml 或 EPUB2 toc.ncx → href(去 fragment)→ 章节标题。 */
36
+ function parseTocLabels(tocXml, isNcx) {
37
+ const labels = /* @__PURE__ */ new Map();
38
+ if (isNcx) {
39
+ const blockRe = /<navPoint\b[\s\S]*?<\/navPoint>/g;
40
+ let m;
41
+ while ((m = blockRe.exec(tocXml)) !== null) {
42
+ const label = firstTagText(m[0], "text");
43
+ const src = m[0].match(/<content[^>]+src="([^"]+)"/i)?.[1];
44
+ if (label && src) labels.set(decodeURIComponent(src.split("#")[0].replace(/\\/g, "/")), label);
45
+ }
46
+ } else {
47
+ const aRe = /<a\b[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
48
+ let m;
49
+ while ((m = aRe.exec(tocXml)) !== null) {
50
+ const label = (m[2] ?? "").replace(/<[^>]+>/g, "").trim();
51
+ if (label) labels.set(decodeURIComponent((m[1] ?? "").split("#")[0].replace(/\\/g, "/")), label);
52
+ }
53
+ }
54
+ return labels;
55
+ }
56
+ function sanitizeFileName(title) {
57
+ return (title || "chapter").replace(/[\\/:*?"<>|#\s]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50) || "chapter";
58
+ }
59
+ /** 文件内章标记。两类形态:heading 行(# 前缀)与**裸行**(Gutenberg 常见——章号在
60
+ * 原书 html 里是段落不是标题,转 markdown 后成裸文本 "CHAPTER XX.")。
61
+ * 裸行限行长(≤60)防正文中引用句误切;罗马数字/裸序号低置信需序列验证。 */
62
+ const CH_HEADING = /^(?:#{1,3}\s*)?(?:CHAPTER|Chapter|chap\.)\s+[IVXLC0-9]{1,7}\b[.:)]?\s*(.*)$/;
63
+ const LETTER_HEADING = /^(?:#{1,3}\s*)?(?:Letter|LETTER)\s+\d{1,3}\b[.:)]?\s*(.*)$/;
64
+ const ROMAN_HEADING = /^(?:#{1,3}\s*)?(?:[—[]\s*)?([IVXLC]{1,7})\s*[—\]]?\.?\s*$/;
65
+ const NUM_HEADING = /^(?:#{1,3}\s*)?[—[]?\s*(\d{1,3})\s*[—\]]?\.?\s*$/;
66
+ const romanValue = (s) => {
67
+ const vals = {
68
+ I: 1,
69
+ V: 5,
70
+ X: 10,
71
+ L: 50,
72
+ C: 100
73
+ };
74
+ let v = 0;
75
+ for (let i = 0; i < s.length; i++) {
76
+ const cur = vals[s[i]] ?? 0;
77
+ const next = vals[s[i + 1]] ?? 0;
78
+ v += cur < next ? -cur : cur;
79
+ }
80
+ return v;
81
+ };
82
+ /**
83
+ * 文件内按章标记切分。返回 null = 无高置信切分(保持单章)。
84
+ * 置信规则:CHAPTER/Letter 标记 ≥2 即切;罗马数字/裸序号须形成连续递增
85
+ * 序列(≥3 个)才切——正文中孤立的 "I" / "2" 不当章号。
86
+ * 首标记前的引言/卷头(<800 字符)并入首章;够长则独立成"引言"章。
87
+ */
88
+ function splitChaptersInBody(body, opts) {
89
+ const lines = body.split("\n");
90
+ const marks = [];
91
+ for (let i = 0; i < lines.length; i++) {
92
+ const ln = lines[i].trimEnd();
93
+ if (!ln || ln.length > 60) continue;
94
+ const strip = (s) => s.replace(/^#{1,3}\s*/, "");
95
+ let m = ln.match(CH_HEADING);
96
+ if (m) {
97
+ marks.push({
98
+ line: i,
99
+ title: strip(ln),
100
+ kind: "ch",
101
+ seq: 0
102
+ });
103
+ continue;
104
+ }
105
+ m = ln.match(LETTER_HEADING);
106
+ if (m) {
107
+ marks.push({
108
+ line: i,
109
+ title: strip(ln),
110
+ kind: "letter",
111
+ seq: 0
112
+ });
113
+ continue;
114
+ }
115
+ m = ln.match(ROMAN_HEADING);
116
+ if (m && (ln.startsWith("#") || /[—[]/.test(ln))) {
117
+ marks.push({
118
+ line: i,
119
+ title: strip(ln),
120
+ kind: "roman",
121
+ seq: romanValue(m[1])
122
+ });
123
+ continue;
124
+ }
125
+ m = ln.match(NUM_HEADING);
126
+ if (m && (ln.startsWith("#") || /[—[]/.test(ln))) {
127
+ marks.push({
128
+ line: i,
129
+ title: strip(ln),
130
+ kind: "num",
131
+ seq: parseInt(m[1], 10)
132
+ });
133
+ continue;
134
+ }
135
+ }
136
+ const minHigh = opts?.minHighConf ?? 2;
137
+ const highConf = marks.filter((mk) => mk.kind === "ch" || mk.kind === "letter");
138
+ let splitMarks;
139
+ if (highConf.length >= minHigh) splitMarks = highConf;
140
+ else {
141
+ splitMarks = [];
142
+ for (const kind of ["roman", "num"]) {
143
+ const seq = marks.filter((mk) => mk.kind === kind);
144
+ const chain = [];
145
+ let expect = 0;
146
+ for (const mk of seq) if (expect === 0) {
147
+ if (mk.seq === 1 || mk.seq === 2) {
148
+ chain.push(mk);
149
+ expect = mk.seq + 1;
150
+ }
151
+ } else if (mk.seq === expect) {
152
+ chain.push(mk);
153
+ expect++;
154
+ }
155
+ if (chain.length >= 3) splitMarks.push(...chain);
156
+ }
157
+ if (splitMarks.length < 3) return null;
158
+ }
159
+ if (splitMarks.length < 1) return null;
160
+ const out = [];
161
+ const preamble = lines.slice(0, splitMarks[0].line).join("\n").trim();
162
+ for (let k = 0; k < splitMarks.length; k++) {
163
+ const start = splitMarks[k].line;
164
+ const end = k + 1 < splitMarks.length ? splitMarks[k + 1].line : lines.length;
165
+ let content = lines.slice(start + 1, end).join("\n").trim();
166
+ if (k === 0 && preamble && preamble.length < 800) content = `${preamble}\n\n${content}`.trim();
167
+ out.push({
168
+ title: splitMarks[k].title,
169
+ content
170
+ });
171
+ }
172
+ if (preamble && preamble.length >= 800) out.unshift({
173
+ title: "引言",
174
+ content: preamble
175
+ });
176
+ return out.filter((c) => c.content.replace(/\s/g, "").length > 0);
177
+ }
178
+ /**
179
+ * 清理电子书正文噪声(Gutenberg 头/尾块、1894 插图版装饰行)。
180
+ * 只删确定性标志,不猜内容:
181
+ * - 头:"The Project Gutenberg eBook of…" 段落(heading 形态)
182
+ * - 尾:"THE FULL PROJECT GUTENBERG™ LICENSE" 起截断到文件尾;"End of the Project Gutenberg" 行起同理
183
+ * - 行内:`[ _Copyright 1894 by …_ ]` 插图版装饰片段
184
+ */
185
+ function sanitizeEpubBody(body) {
186
+ let md = body;
187
+ const lines0 = md.split("\n");
188
+ for (let i = 0; i < lines0.length; i++) {
189
+ const ln = lines0[i].trim();
190
+ if (ln.length <= 100 && (/(?:THE\s+)?FULL PROJECT GUTENBERG/i.test(ln) || /END OF THE PROJECT GUTENBERG/i.test(ln) || ln === "START: FULL LICENSE" || ln === "START: THE FULL LICENSE")) {
191
+ md = lines0.slice(0, i).join("\n");
192
+ break;
193
+ }
194
+ }
195
+ md = md.replace(/^#{1,3}\s*(?:The Project Gutenberg eBook[^*\n]*|Project Gutenberg[^*\n]*)\n[\s\S]*?\n(?:\n|$)/im, "");
196
+ md = md.replace(/^#{1,3}[ \t]*$/gm, "");
197
+ md = md.replace(/\\?\[?\s*_?Copyright 1[6-9]\d\d[^_\n]*_?\s*\\?]?/g, "");
198
+ return md.trim();
199
+ }
200
+ function parseEpub(buf) {
201
+ const entries = readZip(buf);
202
+ const read = (p) => readZipText(entries, p);
203
+ const opfPath = read("META-INF/container.xml").match(/full-path="([^"]+)"/i)?.[1];
204
+ if (!opfPath) throw new Error("epub 结构异常:找不到 container.xml 里的 OPF 路径");
205
+ const opf = read(opfPath);
206
+ if (!opf) throw new Error(`epub 结构异常:OPF 文件缺失(${opfPath})`);
207
+ const bookTitle = firstTagText(opf, "dc:title") || "未命名电子书";
208
+ const manifest = /* @__PURE__ */ new Map();
209
+ for (const t of tags(opf, "item")) manifest.set(t.attrs["id"] ?? "", {
210
+ href: t.attrs["href"] ?? "",
211
+ mediaType: (t.attrs["media-type"] ?? "").toLowerCase(),
212
+ properties: t.attrs["properties"] ?? ""
213
+ });
214
+ const spineIds = tags(opf, "itemref").map((t) => t.attrs["idref"] ?? "").filter(Boolean);
215
+ const spineTocId = opf.match(/<spine\b[^>]*\btoc="([^"]+)"/i)?.[1];
216
+ let tocLabels = /* @__PURE__ */ new Map();
217
+ const navItem = [...manifest.values()].find((it) => it.properties.split(/\s+/).includes("nav"));
218
+ if (navItem?.href) tocLabels = parseTocLabels(read(resolveZipPath(opfPath, navItem.href)), false);
219
+ if (tocLabels.size === 0 && spineTocId && manifest.has(spineTocId)) tocLabels = parseTocLabels(read(resolveZipPath(opfPath, manifest.get(spineTocId).href)), true);
220
+ const opfDirKey = (href) => decodeURIComponent(href.split("#")[0] ?? href).replace(/\\/g, "/");
221
+ /** 无标题标记(不再编造"第 N 章";配对/课程设计层的锚点) */
222
+ const UNTITLED_MARK = "未命名章节";
223
+ const chapters = [];
224
+ let n = 0;
225
+ for (const id of spineIds) {
226
+ const item = manifest.get(id);
227
+ if (!item?.href) continue;
228
+ if (!(item.mediaType === "application/xhtml+xml" || item.mediaType === "text/html" || /\.(xhtml|html|htm)$/i.test(item.href))) continue;
229
+ if (item.properties.split(/\s+/).includes("nav")) continue;
230
+ const xhtml = read(resolveZipPath(opfPath, item.href));
231
+ if (!xhtml) continue;
232
+ const md = htmlToMarkdown(xhtml, { stripImages: true });
233
+ if (!md || md.replace(/[#\s>*-]/g, "").length < 8) continue;
234
+ n++;
235
+ const body0 = md.startsWith("# ") && md.includes("\n") ? md.slice(md.indexOf("\n") + 1).trim() : md;
236
+ const firstHeading = md.startsWith("# ") ? md.split("\n")[0].slice(2).trim() : "";
237
+ const fileTitle = tocLabels.get(opfDirKey(item.href)) || firstHeading || UNTITLED_MARK;
238
+ if (/^(contents|table of contents)$/i.test(fileTitle) && body0.replace(/\s/g, "").length < 4e3) continue;
239
+ if (body0.replace(/\s/g, "").length < 1500 && /project gutenberg|copyright/i.test(body0)) continue;
240
+ if (/^(版权信息|版权|版权页|著作权|版權)/.test(fileTitle)) continue;
241
+ if ((body0.match(/^(书名|作者|译者|编者|丛书|出版社|出版时间|出版年|ISBN|版次|定价|装帧|字数|品牌|出品方)[::]/gm) ?? []).length >= 3 && body0.replace(/\s/g, "").length < 2500) continue;
242
+ if (/^目\s*录$/.test(fileTitle)) {
243
+ const lines = body0.split("\n").map((l) => l.trim()).filter(Boolean);
244
+ if (lines.length >= 3 && lines.filter((l) => l.includes("](")).length / lines.length >= .6) continue;
245
+ }
246
+ const body = sanitizeEpubBody(body0);
247
+ if (body.replace(/\s/g, "").length < 8) continue;
248
+ const titleIsLicense = /FULL PROJECT GUTENBERG|LICENSE/i.test(fileTitle);
249
+ const split = splitChaptersInBody(body, titleIsLicense ? { minHighConf: 1 } : void 0);
250
+ if (split && split.length >= (titleIsLicense ? 1 : 2)) {
251
+ for (const seg of split) {
252
+ chapters.push({
253
+ path: `chapters/${String(n).padStart(2, "0")}-${sanitizeFileName(seg.title)}.md`,
254
+ title: seg.title,
255
+ markdown: `# ${seg.title}\n\n${seg.content}`
256
+ });
257
+ n++;
258
+ }
259
+ n--;
260
+ continue;
261
+ }
262
+ if (titleIsLicense) {
263
+ if (body.replace(/\s/g, "").length > 2e3) chapters.push({
264
+ path: `chapters/${String(n).padStart(2, "0")}-appendix.md`,
265
+ title: "附录",
266
+ markdown: `# 附录\n\n${body}`
267
+ });
268
+ continue;
269
+ }
270
+ chapters.push({
271
+ path: `chapters/${String(n).padStart(2, "0")}-${sanitizeFileName(fileTitle)}.md`,
272
+ title: fileTitle,
273
+ markdown: `# ${fileTitle}\n\n${body}`
274
+ });
275
+ }
276
+ const absorbed = /* @__PURE__ */ new Set();
277
+ for (let i = 0; i + 1 < chapters.length; i++) {
278
+ const stub = chapters[i];
279
+ const next = chapters[i + 1];
280
+ if (absorbed.has(stub.path)) continue;
281
+ const stubBody = stub.markdown.includes("\n") ? stub.markdown.slice(stub.markdown.indexOf("\n") + 1).trim() : "";
282
+ if (stub.title === UNTITLED_MARK || stub.title === "附录") continue;
283
+ if (stubBody.replace(/\s/g, "").length > 300) continue;
284
+ if (next.title !== UNTITLED_MARK) continue;
285
+ const nextBody = next.markdown.includes("\n") ? next.markdown.slice(next.markdown.indexOf("\n") + 1).trim() : next.markdown;
286
+ chapters[i] = {
287
+ path: stub.path,
288
+ title: stub.title,
289
+ markdown: `# ${stub.title}\n\n${stubBody}\n\n${nextBody}`
290
+ };
291
+ absorbed.add(stub.path);
292
+ chapters.splice(i + 1, 1);
293
+ i--;
294
+ }
295
+ if (chapters.length === 0) throw new Error("epub 里没有可识别的章节文本");
296
+ return {
297
+ title: bookTitle,
298
+ chapters
299
+ };
300
+ }
301
+ /** 文件夹导入路径用:整本书压平成一个 markdown(全部 H1 降为 H2,给结构设计当 anchor 拆章)。 */
302
+ function parseEpubFlat(buf) {
303
+ return parseEpub(buf).chapters.map((c) => c.markdown.replace(/^# /gm, "## ")).join("\n\n");
304
+ }
305
+ //#endregion
306
+ export { parseEpubFlat };