dsh-plugin-lookatstudy 0.11.0 → 0.11.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/README.md +29 -7
- package/lib/client.js +1529 -109
- package/lib/client.js.map +1 -1
- package/lib/docx-parser-BhyqPImb.mjs +55 -0
- package/lib/epub-parser-DvlKap-d.mjs +112 -0
- package/lib/html-article-C6nhqJbX.mjs +426 -0
- package/lib/index.d.mts +5 -4
- package/lib/index.mjs +3011 -1230
- package/lib/inflate-DIKUrTRi.mjs +311 -0
- package/lib/pptx-parser-CD5pR2cj.mjs +48 -0
- package/lib/zip-reader-KnRrq0av.mjs +67 -0
- package/package.json +1 -1
|
@@ -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(/</g, "<").replace(/>/g, ">").replace(/"/g, "\"").replace(/'/g, "'").replace(/&/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,112 @@
|
|
|
1
|
+
import { n as htmlToMarkdown } from "./html-article-C6nhqJbX.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
|
+
function parseEpub(buf) {
|
|
60
|
+
const entries = readZip(buf);
|
|
61
|
+
const read = (p) => readZipText(entries, p);
|
|
62
|
+
const opfPath = read("META-INF/container.xml").match(/full-path="([^"]+)"/i)?.[1];
|
|
63
|
+
if (!opfPath) throw new Error("epub 结构异常:找不到 container.xml 里的 OPF 路径");
|
|
64
|
+
const opf = read(opfPath);
|
|
65
|
+
if (!opf) throw new Error(`epub 结构异常:OPF 文件缺失(${opfPath})`);
|
|
66
|
+
const bookTitle = firstTagText(opf, "dc:title") || "未命名电子书";
|
|
67
|
+
const manifest = /* @__PURE__ */ new Map();
|
|
68
|
+
for (const t of tags(opf, "item")) manifest.set(t.attrs["id"] ?? "", {
|
|
69
|
+
href: t.attrs["href"] ?? "",
|
|
70
|
+
mediaType: (t.attrs["media-type"] ?? "").toLowerCase(),
|
|
71
|
+
properties: t.attrs["properties"] ?? ""
|
|
72
|
+
});
|
|
73
|
+
const spineIds = tags(opf, "itemref").map((t) => t.attrs["idref"] ?? "").filter(Boolean);
|
|
74
|
+
const spineTocId = opf.match(/<spine\b[^>]*\btoc="([^"]+)"/i)?.[1];
|
|
75
|
+
let tocLabels = /* @__PURE__ */ new Map();
|
|
76
|
+
const navItem = [...manifest.values()].find((it) => it.properties.split(/\s+/).includes("nav"));
|
|
77
|
+
if (navItem?.href) tocLabels = parseTocLabels(read(resolveZipPath(opfPath, navItem.href)), false);
|
|
78
|
+
if (tocLabels.size === 0 && spineTocId && manifest.has(spineTocId)) tocLabels = parseTocLabels(read(resolveZipPath(opfPath, manifest.get(spineTocId).href)), true);
|
|
79
|
+
const opfDirKey = (href) => decodeURIComponent(href.split("#")[0] ?? href).replace(/\\/g, "/");
|
|
80
|
+
const chapters = [];
|
|
81
|
+
let n = 0;
|
|
82
|
+
for (const id of spineIds) {
|
|
83
|
+
const item = manifest.get(id);
|
|
84
|
+
if (!item?.href) continue;
|
|
85
|
+
if (!(item.mediaType === "application/xhtml+xml" || item.mediaType === "text/html" || /\.(xhtml|html|htm)$/i.test(item.href))) continue;
|
|
86
|
+
if (item.properties.split(/\s+/).includes("nav")) continue;
|
|
87
|
+
const xhtml = read(resolveZipPath(opfPath, item.href));
|
|
88
|
+
if (!xhtml) continue;
|
|
89
|
+
const md = htmlToMarkdown(xhtml, { stripImages: true });
|
|
90
|
+
if (!md || md.replace(/[#\s>*-]/g, "").length < 8) continue;
|
|
91
|
+
n++;
|
|
92
|
+
const body = md.startsWith("# ") && md.includes("\n") ? md.slice(md.indexOf("\n") + 1).trim() : md;
|
|
93
|
+
const firstHeading = md.startsWith("# ") ? md.split("\n")[0].slice(2).trim() : "";
|
|
94
|
+
const title = tocLabels.get(opfDirKey(item.href)) || firstHeading || `第 ${n} 章`;
|
|
95
|
+
chapters.push({
|
|
96
|
+
path: `chapters/${String(n).padStart(2, "0")}-${sanitizeFileName(title)}.md`,
|
|
97
|
+
title,
|
|
98
|
+
markdown: `# ${title}\n\n${body}`
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (chapters.length === 0) throw new Error("epub 里没有可识别的章节文本");
|
|
102
|
+
return {
|
|
103
|
+
title: bookTitle,
|
|
104
|
+
chapters
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** 文件夹导入路径用:整本书压平成一个 markdown(全部 H1 降为 H2,给结构设计当 anchor 拆章)。 */
|
|
108
|
+
function parseEpubFlat(buf) {
|
|
109
|
+
return parseEpub(buf).chapters.map((c) => c.markdown.replace(/^# /gm, "## ")).join("\n\n");
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
export { parseEpubFlat };
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
//#region src/vendor/html-article.ts
|
|
2
|
+
const ENTITIES = {
|
|
3
|
+
amp: "&",
|
|
4
|
+
lt: "<",
|
|
5
|
+
gt: ">",
|
|
6
|
+
quot: "\"",
|
|
7
|
+
apos: "'",
|
|
8
|
+
nbsp: "\xA0",
|
|
9
|
+
hellip: "…",
|
|
10
|
+
mdash: "—",
|
|
11
|
+
ndash: "–",
|
|
12
|
+
rsquo: "'",
|
|
13
|
+
lsquo: "'",
|
|
14
|
+
rdquo: "\"",
|
|
15
|
+
ldquo: "\"",
|
|
16
|
+
middot: "·",
|
|
17
|
+
bull: "•",
|
|
18
|
+
copy: "©",
|
|
19
|
+
times: "×",
|
|
20
|
+
divide: "÷",
|
|
21
|
+
deg: "°"
|
|
22
|
+
};
|
|
23
|
+
function decodeEntities(s) {
|
|
24
|
+
return s.replace(/&(#x?[0-9a-fA-F]+|[a-zA-Z]+);/g, (_m, ent) => {
|
|
25
|
+
if (ent.startsWith("#x") || ent.startsWith("#X")) return String.fromCodePoint(parseInt(ent.slice(2), 16));
|
|
26
|
+
if (ent.startsWith("#")) return String.fromCodePoint(parseInt(ent.slice(1), 10));
|
|
27
|
+
return ENTITIES[ent.toLowerCase()] ?? _m;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
const VOID_TAGS = /* @__PURE__ */ new Set([
|
|
31
|
+
"br",
|
|
32
|
+
"hr",
|
|
33
|
+
"img",
|
|
34
|
+
"input",
|
|
35
|
+
"meta",
|
|
36
|
+
"link",
|
|
37
|
+
"area",
|
|
38
|
+
"base",
|
|
39
|
+
"col",
|
|
40
|
+
"embed",
|
|
41
|
+
"source",
|
|
42
|
+
"track",
|
|
43
|
+
"wbr"
|
|
44
|
+
]);
|
|
45
|
+
/** Stack-parse well-formed-enough HTML/XHTML into a forest of nodes. */
|
|
46
|
+
function parseHtml(html) {
|
|
47
|
+
const roots = [];
|
|
48
|
+
const stack = [];
|
|
49
|
+
const push = (n) => {
|
|
50
|
+
(stack.length ? stack[stack.length - 1].children : roots).push(n);
|
|
51
|
+
};
|
|
52
|
+
const tokenRe = /<!--[\s\S]*?-->|<!\[CDATA\[([\s\S]*?)\]\]>|<\/([a-zA-Z][-\w:]*)\s*>|<([a-zA-Z][-\w:]*)((?:"[^"]*"|'[^']*'|[^>"'])*?)(\/?)>|([^<]+)/g;
|
|
53
|
+
let m;
|
|
54
|
+
while ((m = tokenRe.exec(html)) !== null) if (m[2] !== void 0) {
|
|
55
|
+
const close = m[2].toLowerCase();
|
|
56
|
+
for (let i = stack.length - 1; i >= 0; i--) if (stack[i].tag === close) {
|
|
57
|
+
stack.length = i;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
} else if (m[3] !== void 0) {
|
|
61
|
+
const tag = m[3].toLowerCase();
|
|
62
|
+
const attrs = {};
|
|
63
|
+
const attrRe = /([-\w:]+)\s*=\s*("([^"]*)"|'([^']*)')/g;
|
|
64
|
+
let a;
|
|
65
|
+
while ((a = attrRe.exec(m[4] ?? "")) !== null) attrs[a[1].toLowerCase()] = decodeEntities(a[3] ?? a[4] ?? "");
|
|
66
|
+
const node = {
|
|
67
|
+
tag,
|
|
68
|
+
attrs,
|
|
69
|
+
children: [],
|
|
70
|
+
text: ""
|
|
71
|
+
};
|
|
72
|
+
push(node);
|
|
73
|
+
if (!VOID_TAGS.has(tag) && m[5] !== "/") stack.push(node);
|
|
74
|
+
} else if (m[1] !== void 0) push({
|
|
75
|
+
tag: "",
|
|
76
|
+
attrs: {},
|
|
77
|
+
children: [],
|
|
78
|
+
text: m[1]
|
|
79
|
+
});
|
|
80
|
+
else if (m[6] !== void 0) {
|
|
81
|
+
const text = decodeEntities(m[6]);
|
|
82
|
+
if (text.trim()) push({
|
|
83
|
+
tag: "",
|
|
84
|
+
attrs: {},
|
|
85
|
+
children: [],
|
|
86
|
+
text
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return roots;
|
|
90
|
+
}
|
|
91
|
+
const BLOCK_TAGS = /* @__PURE__ */ new Set([
|
|
92
|
+
"p",
|
|
93
|
+
"div",
|
|
94
|
+
"section",
|
|
95
|
+
"article",
|
|
96
|
+
"header",
|
|
97
|
+
"footer",
|
|
98
|
+
"main",
|
|
99
|
+
"aside",
|
|
100
|
+
"nav",
|
|
101
|
+
"figure",
|
|
102
|
+
"figcaption",
|
|
103
|
+
"h1",
|
|
104
|
+
"h2",
|
|
105
|
+
"h3",
|
|
106
|
+
"h4",
|
|
107
|
+
"h5",
|
|
108
|
+
"h6",
|
|
109
|
+
"ul",
|
|
110
|
+
"ol",
|
|
111
|
+
"li",
|
|
112
|
+
"blockquote",
|
|
113
|
+
"pre",
|
|
114
|
+
"table",
|
|
115
|
+
"tr",
|
|
116
|
+
"thead",
|
|
117
|
+
"tbody",
|
|
118
|
+
"hr",
|
|
119
|
+
"br",
|
|
120
|
+
"address",
|
|
121
|
+
"details",
|
|
122
|
+
"summary"
|
|
123
|
+
]);
|
|
124
|
+
function nodeText(node) {
|
|
125
|
+
if (node.tag === "") return node.text;
|
|
126
|
+
return node.children.map(nodeText).join("");
|
|
127
|
+
}
|
|
128
|
+
function findDescendant(node, tag, cls) {
|
|
129
|
+
for (const c of node.children) {
|
|
130
|
+
if (c.tag === tag && (!cls || (c.attrs["class"] ?? "").split(/\s+/).includes(cls))) return c;
|
|
131
|
+
const hit = findDescendant(c, tag, cls);
|
|
132
|
+
if (hit) return hit;
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
function inlineRuns(nodes, stripImages) {
|
|
137
|
+
let out = "";
|
|
138
|
+
for (const n of nodes) switch (n.tag) {
|
|
139
|
+
case "":
|
|
140
|
+
out += n.text.replace(/\s+/g, " ");
|
|
141
|
+
break;
|
|
142
|
+
case "br":
|
|
143
|
+
out += "\n";
|
|
144
|
+
break;
|
|
145
|
+
case "strong":
|
|
146
|
+
case "b": {
|
|
147
|
+
const inner = inlineRuns(n.children, stripImages).trim();
|
|
148
|
+
if (inner) out += `**${inner}**`;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case "em":
|
|
152
|
+
case "i": {
|
|
153
|
+
const inner = inlineRuns(n.children, stripImages).trim();
|
|
154
|
+
if (inner) out += `*${inner}*`;
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
case "code":
|
|
158
|
+
out += "`" + nodeText(n).trim() + "`";
|
|
159
|
+
break;
|
|
160
|
+
case "a": {
|
|
161
|
+
const inner = inlineRuns(n.children, stripImages).trim();
|
|
162
|
+
const href = n.attrs["href"] ?? "";
|
|
163
|
+
out += inner && href && !href.startsWith("#") ? `[${inner}](${href})` : inner;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
case "img":
|
|
167
|
+
case "picture":
|
|
168
|
+
case "svg":
|
|
169
|
+
case "figure":
|
|
170
|
+
if (!stripImages && n.tag === "img") {
|
|
171
|
+
const alt = n.attrs["alt"] ?? "";
|
|
172
|
+
const src = n.attrs["src"] ?? "";
|
|
173
|
+
if (src) out += ``;
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
176
|
+
case "script":
|
|
177
|
+
if ((n.attrs["type"] ?? "").includes("math/tex")) {
|
|
178
|
+
const tex = nodeText(n).trim();
|
|
179
|
+
if (tex) out += tex.includes("\n") ? `$$${tex}$$` : `$${tex}$`;
|
|
180
|
+
}
|
|
181
|
+
break;
|
|
182
|
+
case "style":
|
|
183
|
+
case "head": break;
|
|
184
|
+
case "span":
|
|
185
|
+
if ((n.attrs["class"] ?? "").split(/\s+/).includes("katex")) {
|
|
186
|
+
const ann = findDescendant(n, "annotation");
|
|
187
|
+
const tex = ann ? nodeText(ann).trim() : "";
|
|
188
|
+
if (tex) out += tex.includes("\n") ? `$$${tex}$$` : `$${tex}$`;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
out += inlineRuns(n.children, stripImages);
|
|
192
|
+
break;
|
|
193
|
+
default: out += inlineRuns(n.children, stripImages);
|
|
194
|
+
}
|
|
195
|
+
return out.replace(/[ \t]+\n/g, "\n");
|
|
196
|
+
}
|
|
197
|
+
function emitBlocks(nodes, stripImages, lines, listDepth = 0, quotePrefix = "") {
|
|
198
|
+
const indent = " ".repeat(listDepth);
|
|
199
|
+
for (const n of nodes) {
|
|
200
|
+
const prefix = quotePrefix;
|
|
201
|
+
switch (n.tag) {
|
|
202
|
+
case "": {
|
|
203
|
+
const t = n.text.trim();
|
|
204
|
+
if (t) lines.push(prefix + t);
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
case "h1":
|
|
208
|
+
case "h2":
|
|
209
|
+
case "h3":
|
|
210
|
+
case "h4":
|
|
211
|
+
case "h5":
|
|
212
|
+
case "h6": {
|
|
213
|
+
const level = Number(n.tag[1]);
|
|
214
|
+
const t = inlineRuns(n.children, stripImages).trim();
|
|
215
|
+
if (t) lines.push(prefix + "#".repeat(level) + " " + t);
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
case "p":
|
|
219
|
+
case "div":
|
|
220
|
+
case "section":
|
|
221
|
+
case "article":
|
|
222
|
+
case "main":
|
|
223
|
+
case "figcaption":
|
|
224
|
+
case "summary":
|
|
225
|
+
case "details":
|
|
226
|
+
case "address": {
|
|
227
|
+
if (n.children.some((c) => BLOCK_TAGS.has(c.tag) && c.tag !== "br")) {
|
|
228
|
+
emitBlocks(n.children, stripImages, lines, listDepth, prefix);
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
const t = inlineRuns(n.children, stripImages).trim();
|
|
232
|
+
if (t) lines.push(prefix + indent + t);
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
case "ul":
|
|
236
|
+
case "ol": {
|
|
237
|
+
let idx = 1;
|
|
238
|
+
for (const c of n.children) if (c.tag === "li") {
|
|
239
|
+
const marker = n.tag === "ol" ? `${idx++}. ` : "- ";
|
|
240
|
+
if (!c.children.some((cc) => BLOCK_TAGS.has(cc.tag) && cc.tag !== "br")) {
|
|
241
|
+
const t = inlineRuns(c.children, stripImages).trim();
|
|
242
|
+
if (t) lines.push(prefix + indent + marker + t);
|
|
243
|
+
} else {
|
|
244
|
+
const nested = [];
|
|
245
|
+
emitBlocks(c.children, stripImages, nested, listDepth + 1, prefix);
|
|
246
|
+
if (nested.length) lines.push(prefix + indent + marker + nested[0].trimStart());
|
|
247
|
+
lines.push(...nested.slice(1));
|
|
248
|
+
}
|
|
249
|
+
} else emitBlocks([c], stripImages, lines, listDepth, prefix);
|
|
250
|
+
lines.push("");
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
case "blockquote": {
|
|
254
|
+
const inner = [];
|
|
255
|
+
emitBlocks(n.children, stripImages, inner, 0, "");
|
|
256
|
+
for (const l of inner) if (l.trim()) lines.push(prefix + "> " + l);
|
|
257
|
+
lines.push("");
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
case "pre": {
|
|
261
|
+
const code = n.children.map((c) => nodeText(c)).join("").replace(/\n+$/, "");
|
|
262
|
+
if (code.trim()) {
|
|
263
|
+
lines.push(prefix + "```", code.split("\n").map((l) => prefix + l).join("\n"), prefix + "```");
|
|
264
|
+
lines.push("");
|
|
265
|
+
}
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
case "table":
|
|
269
|
+
for (const c of n.children) if (c.tag === "tr") {
|
|
270
|
+
const cells = c.children.filter((cc) => cc.tag === "td" || cc.tag === "th").map((cc) => inlineRuns(cc.children, stripImages).trim().replace(/\|/g, "\\|"));
|
|
271
|
+
if (cells.length) lines.push(prefix + "| " + cells.join(" | ") + " |");
|
|
272
|
+
} else emitBlocks([c], stripImages, lines, listDepth, prefix);
|
|
273
|
+
lines.push("");
|
|
274
|
+
break;
|
|
275
|
+
case "hr":
|
|
276
|
+
lines.push(prefix + "---");
|
|
277
|
+
lines.push("");
|
|
278
|
+
break;
|
|
279
|
+
case "br":
|
|
280
|
+
lines.push("");
|
|
281
|
+
break;
|
|
282
|
+
case "script":
|
|
283
|
+
if ((n.attrs["type"] ?? "").includes("math/tex")) {
|
|
284
|
+
const tex = nodeText(n).trim();
|
|
285
|
+
if (tex) lines.push(prefix + (tex.includes("\n") ? `$$${tex}$$` : `$${tex}$`));
|
|
286
|
+
}
|
|
287
|
+
break;
|
|
288
|
+
case "span": {
|
|
289
|
+
if ((n.attrs["class"] ?? "").split(/\s+/).includes("katex")) {
|
|
290
|
+
const ann = findDescendant(n, "annotation");
|
|
291
|
+
const tex = ann ? nodeText(ann).trim() : "";
|
|
292
|
+
if (tex) lines.push(prefix + (tex.includes("\n") ? `$$${tex}$$` : `$${tex}$`));
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
const t = inlineRuns([n], stripImages).trim();
|
|
296
|
+
if (t) lines.push(prefix + t);
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
case "img":
|
|
300
|
+
case "picture":
|
|
301
|
+
case "svg":
|
|
302
|
+
case "figure":
|
|
303
|
+
if (!stripImages && n.tag === "img") {
|
|
304
|
+
const src = n.attrs["src"] ?? "";
|
|
305
|
+
if (src) lines.push(prefix + `![${n.attrs["alt"] ?? ""}](${src})`);
|
|
306
|
+
}
|
|
307
|
+
break;
|
|
308
|
+
case "style":
|
|
309
|
+
case "head":
|
|
310
|
+
case "nav":
|
|
311
|
+
case "button":
|
|
312
|
+
case "form":
|
|
313
|
+
case "noscript": break;
|
|
314
|
+
default: emitBlocks(n.children, stripImages, lines, listDepth, prefix);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/** 任意 HTML/XHTML → markdown(epub 章节用;数学源回收在 script 剥除之前,同上游)。 */
|
|
319
|
+
function htmlToMarkdown(html, opts = {}) {
|
|
320
|
+
const stripImages = opts.stripImages ?? false;
|
|
321
|
+
let roots = parseHtml(html);
|
|
322
|
+
if (roots.length === 0) roots = parseHtml(`<html><body>${html}</body></html>`);
|
|
323
|
+
const lines = [];
|
|
324
|
+
emitBlocks(roots, stripImages, lines);
|
|
325
|
+
return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
326
|
+
}
|
|
327
|
+
const STRIP_FOR_ARTICLE = /* @__PURE__ */ new Set([
|
|
328
|
+
"script",
|
|
329
|
+
"style",
|
|
330
|
+
"nav",
|
|
331
|
+
"header",
|
|
332
|
+
"footer",
|
|
333
|
+
"aside",
|
|
334
|
+
"form",
|
|
335
|
+
"noscript",
|
|
336
|
+
"button",
|
|
337
|
+
"iframe",
|
|
338
|
+
"svg"
|
|
339
|
+
]);
|
|
340
|
+
function textDensity(nodes) {
|
|
341
|
+
let chars = 0, tags = 0;
|
|
342
|
+
const walk = (ns) => {
|
|
343
|
+
for (const n of ns) {
|
|
344
|
+
if (n.tag === "") {
|
|
345
|
+
chars += n.text.trim().length;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (STRIP_FOR_ARTICLE.has(n.tag) || n.tag === "div" && (n.attrs["id"] ?? n.attrs["class"] ?? "").match(/comment|sidebar|related|share|footer|nav/i)) continue;
|
|
349
|
+
tags++;
|
|
350
|
+
walk(n.children);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
walk(nodes);
|
|
354
|
+
return {
|
|
355
|
+
chars,
|
|
356
|
+
tags
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
/** Pick the densest content root: <article> > <main> > <body> > whole forest. */
|
|
360
|
+
function pickContentRoot(roots) {
|
|
361
|
+
const findFirst = (tag) => {
|
|
362
|
+
const walk = (ns) => {
|
|
363
|
+
for (const n of ns) {
|
|
364
|
+
if (n.tag === tag) return n;
|
|
365
|
+
const hit = walk(n.children);
|
|
366
|
+
if (hit) return hit;
|
|
367
|
+
}
|
|
368
|
+
return null;
|
|
369
|
+
};
|
|
370
|
+
return walk(roots);
|
|
371
|
+
};
|
|
372
|
+
return [findFirst("article") ?? findFirst("main") ?? findFirst("body") ?? {
|
|
373
|
+
tag: "",
|
|
374
|
+
attrs: {},
|
|
375
|
+
children: roots,
|
|
376
|
+
text: ""
|
|
377
|
+
}];
|
|
378
|
+
}
|
|
379
|
+
/** 从完整 HTML 抽取文章正文并转 markdown。非文章页返回 null(诚实失败,同上游契约)。 */
|
|
380
|
+
function extractArticle(html, baseUrl = "") {
|
|
381
|
+
let roots = parseHtml(html);
|
|
382
|
+
if (roots.length === 0) return null;
|
|
383
|
+
const titleNode = findDescendant({
|
|
384
|
+
tag: "root",
|
|
385
|
+
attrs: {},
|
|
386
|
+
children: roots,
|
|
387
|
+
text: ""
|
|
388
|
+
}, "title");
|
|
389
|
+
const h1 = findDescendant({
|
|
390
|
+
tag: "root",
|
|
391
|
+
attrs: {},
|
|
392
|
+
children: roots,
|
|
393
|
+
text: ""
|
|
394
|
+
}, "h1");
|
|
395
|
+
const title = (titleNode ? nodeText(titleNode) : h1 ? inlineRuns(h1.children, false).trim() : "").trim() || "无标题文章";
|
|
396
|
+
const contentRoots = pickContentRoot(roots);
|
|
397
|
+
if (baseUrl) {
|
|
398
|
+
const walk = (ns) => {
|
|
399
|
+
for (const n of ns) {
|
|
400
|
+
if (n.tag === "img") {
|
|
401
|
+
const raw = n.attrs["src"] ?? "";
|
|
402
|
+
if (raw && !raw.startsWith("data:") && !/^https?:/i.test(raw)) try {
|
|
403
|
+
n.attrs["src"] = new URL(raw, baseUrl).toString();
|
|
404
|
+
} catch {}
|
|
405
|
+
}
|
|
406
|
+
walk(n.children);
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
walk(contentRoots);
|
|
410
|
+
}
|
|
411
|
+
const { chars } = textDensity(contentRoots);
|
|
412
|
+
if (chars < 120) return null;
|
|
413
|
+
const body = htmlToMarkdownOf(contentRoots, false);
|
|
414
|
+
if (!body || body.replace(/[#\s>*|-]/g, "").length < 40) return null;
|
|
415
|
+
return {
|
|
416
|
+
title,
|
|
417
|
+
markdown: body.split("\n")[0]?.replace(/^#\s+/, "").trim() === title && body.startsWith("# ") ? body : `# ${title}\n\n${body}`
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
function htmlToMarkdownOf(roots, stripImages) {
|
|
421
|
+
const lines = [];
|
|
422
|
+
emitBlocks(roots, stripImages, lines);
|
|
423
|
+
return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
424
|
+
}
|
|
425
|
+
//#endregion
|
|
426
|
+
export { htmlToMarkdown as n, extractArticle as t };
|
package/lib/index.d.mts
CHANGED
|
@@ -32,10 +32,11 @@ declare const Config: z<Config>;
|
|
|
32
32
|
declare const name = "lookatstudy-plugin";
|
|
33
33
|
declare const inject: string[];
|
|
34
34
|
/**
|
|
35
|
-
* Register the activation-gated study surface: the
|
|
36
|
-
* unregistered while dormant), the tutor persona (stable core + soul),
|
|
37
|
-
*
|
|
38
|
-
* while inactive, and empty sections are dropped at
|
|
35
|
+
* Register the activation-gated study surface: the 25 `study_*` tools (kept
|
|
36
|
+
* unregistered while dormant), the tutor persona (stable core + soul), the
|
|
37
|
+
* dynamic learner-snapshot context, and the `/study` command — every prompt
|
|
38
|
+
* text renders empty while inactive, and empty sections are dropped at
|
|
39
|
+
* assembly.
|
|
39
40
|
* @param ctx - plugin context carrying the tool registry and system prompt.
|
|
40
41
|
* @param config - validated plugin configuration.
|
|
41
42
|
*/
|