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.
- package/README.md +28 -7
- package/lib/client.js +1460 -109
- package/lib/client.js.map +1 -1
- package/lib/docx-parser-BhyqPImb.mjs +55 -0
- package/lib/epub-parser-oH96guBW.mjs +306 -0
- package/lib/html-article-Da8ksU0i.mjs +460 -0
- package/lib/index.d.mts +5 -4
- package/lib/index.mjs +3098 -1287
- package/lib/inflate-DIKUrTRi.mjs +311 -0
- package/lib/pptx-parser-B9IDkiGM.mjs +96 -0
- package/lib/zip-reader-KnRrq0av.mjs +67 -0
- package/package.json +1 -1
|
@@ -0,0 +1,460 @@
|
|
|
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: stripTailNavigation(body.split("\n")[0]?.replace(/^#\s+/, "").trim() === title && body.startsWith("# ") ? body : `# ${title}\n\n${body}`)
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* 尾部**站点模板指纹**清理(upstream v0.23.1, 2026-08-23 真实站点采样驱动; vendored verbatim)。
|
|
422
|
+
* 原则:规则只管高置信度的**机器生成模板**(跨文章稳定、作为正文出现概率≈0);
|
|
423
|
+
* 除此之外的不确定判断(作者自己写的推广段/水印/相关阅读算不算正文)一律不猜——
|
|
424
|
+
* 那是课程设计层(tutor/Step4)的职责,解析层动了就是误删正文
|
|
425
|
+
* (实测:按"欢迎关注公众号"删规则,把 CSDN 作者自己写的推广段删了)。
|
|
426
|
+
*/
|
|
427
|
+
const NAV_TAIL_PATTERNS = [
|
|
428
|
+
/返回\S{0,6}[,,]?\s*查看更多/,
|
|
429
|
+
/点击进入\S{0,8}首页/,
|
|
430
|
+
/^热门文章$/,
|
|
431
|
+
/^最新文章$/,
|
|
432
|
+
/^目录\s*$/,
|
|
433
|
+
/^END\b.*版权/
|
|
434
|
+
];
|
|
435
|
+
/** 行内导航后缀:正文与模板被并成一行时剥掉(精确短语,全文安全) */
|
|
436
|
+
const INLINE_NAV_SUFFIXES = ["目录 热门文章 最新文章"];
|
|
437
|
+
function stripTailNavigation(md) {
|
|
438
|
+
if (!md) return md;
|
|
439
|
+
for (const suffix of INLINE_NAV_SUFFIXES) md = md.split(suffix).join("");
|
|
440
|
+
const lines = md.split("\n");
|
|
441
|
+
let end = lines.length;
|
|
442
|
+
for (let i = lines.length - 1; i >= Math.max(0, lines.length - 25); i--) {
|
|
443
|
+
const ln = lines[i].trim();
|
|
444
|
+
if (!ln) continue;
|
|
445
|
+
const bare = ln.replace(/^#{1,6}\s*/, "").replace(/^[>\-*]\s*/, "");
|
|
446
|
+
const isNav = NAV_TAIL_PATTERNS.some((re) => re.test(bare) || re.test(ln));
|
|
447
|
+
const isBareImage = /^!\[[^\]]*\]\([^)]*\)$/.test(ln) || /^\S+\.(jpeg|jpg|png|gif|webp)\)?$/i.test(ln);
|
|
448
|
+
if (isNav || isBareImage) continue;
|
|
449
|
+
end = i + 1;
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
return lines.slice(0, end).join("\n").trimEnd();
|
|
453
|
+
}
|
|
454
|
+
function htmlToMarkdownOf(roots, stripImages) {
|
|
455
|
+
const lines = [];
|
|
456
|
+
emitBlocks(roots, stripImages, lines);
|
|
457
|
+
return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
458
|
+
}
|
|
459
|
+
//#endregion
|
|
460
|
+
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
|
*/
|