dsh-plugin-lookatstudy 0.11.1 → 0.12.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.
@@ -21,6 +21,49 @@ function shapeTexts(xml) {
21
21
  function slideNumber(name) {
22
22
  return Number(name.match(/slide(\d+)\.xml$/i)?.[1] ?? 0);
23
23
  }
24
+ /**
25
+ * One `a:tbl` block → GFM markdown table, or "" when the table is an
26
+ * all-empty placeholder. Cells: text via a:p runs; gridSpan advances the
27
+ * column index (spanned columns padded); vMerge continuation cells carry no
28
+ * text of their own. Pipes escaped, inner whitespace collapsed.
29
+ */
30
+ function tableToMarkdown(tblXml) {
31
+ const rows = [];
32
+ const trRe = /<a:tr\b[^>]*>([\s\S]*?)<\/a:tr>/g;
33
+ let trm;
34
+ while ((trm = trRe.exec(tblXml)) !== null) {
35
+ const cells = [];
36
+ const tcRe = /<a:tc\b([^>]*)>([\s\S]*?)<\/a:tc>/g;
37
+ let col = 0;
38
+ let tcm;
39
+ while ((tcm = tcRe.exec(trm[1])) !== null) {
40
+ const attrs = tcm[1] ?? "";
41
+ const span = Math.max(1, Number(attrs.match(/gridSpan="(\d+)"/)?.[1] ?? 1));
42
+ const vMerge = /vMerge="1"/.test(attrs);
43
+ const raw = shapeTexts(tcm[2] ?? "").join(" ").trim().replace(/\|/g, "\\|").replace(/\s+/g, " ");
44
+ if (!vMerge) cells.push({
45
+ col,
46
+ text: raw
47
+ });
48
+ col += span;
49
+ }
50
+ rows.push(cells);
51
+ }
52
+ if (rows.filter((cs) => cs.length > 0).length === 0) return "";
53
+ if (rows.every((cs) => cs.every((c) => !c.text))) return "";
54
+ const sorted = rows.filter((cs) => cs.length > 0).map((cs) => cs.sort((a, b) => a.col - b.col));
55
+ const width = Math.max(...sorted.map((cs) => cs[cs.length - 1].col + 1));
56
+ const line = (cs) => {
57
+ const texts = Array.from({ length: width }, () => "");
58
+ for (const c of cs) texts[c.col] = c.text;
59
+ return `| ${texts.join(" | ")} |`;
60
+ };
61
+ return [
62
+ line(sorted[0]),
63
+ `| ${Array.from({ length: width }, () => "---").join(" | ")} |`,
64
+ ...sorted.slice(1).map(line)
65
+ ].join("\n");
66
+ }
24
67
  /** .pptx → markdown(每张 slide 一个 ##)。失败抛错,调用方按"无内容"兜底。 */
25
68
  function parsePptx(buf) {
26
69
  const entries = readZip(buf);
@@ -30,9 +73,14 @@ function parsePptx(buf) {
30
73
  for (const name of slideNames) {
31
74
  const xml = readZipText(entries, name);
32
75
  const no = slideNumber(name);
33
- const texts = shapeTexts(xml);
76
+ const tables = [];
77
+ const texts = shapeTexts(xml.replace(/<a:tbl\b[\s\S]*?<\/a:tbl>/g, (tbl) => {
78
+ const md = tableToMarkdown(tbl);
79
+ if (md) tables.push(md);
80
+ return "";
81
+ }));
34
82
  const title = texts[0] ?? "";
35
- const body = texts.slice(1);
83
+ const body = [...texts.slice(1), ...tables];
36
84
  lines.push(`\n## Slide ${no}: ${title || "(无标题)"}\n`);
37
85
  if (body.length) lines.push(body.join("\n\n"));
38
86
  const notes = shapeTexts(readZipText(entries, `ppt/notesSlides/notesSlide${no}.xml`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-lookatstudy",
3
- "version": "0.11.1",
3
+ "version": "0.12.1",
4
4
  "packageManager": "pnpm@11.7.0",
5
5
  "description": "Turn any markdown, local folder, or GitHub learning repo into a guided course inside DeepSeek Harness: gated skill-tree progression, BKT mastery tracking, SM-2 spaced repetition.",
6
6
  "type": "module",
@@ -1,112 +0,0 @@
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 };