dsh-plugin-lookatstudy 0.25.0 → 0.25.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/lib/client.js +6 -12
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +54 -25
- package/package.json +1 -1
package/lib/index.mjs
CHANGED
|
@@ -266,7 +266,6 @@ const ZH = {
|
|
|
266
266
|
"chat.reasoning": "思考过程({n} 字)",
|
|
267
267
|
"chat.think": "思考",
|
|
268
268
|
"langview.original": "原文",
|
|
269
|
-
"langview.bilingual": "对照",
|
|
270
269
|
"langview.translation": "译文",
|
|
271
270
|
"msg.speak": "朗读",
|
|
272
271
|
"note.pin": "置顶笔记",
|
|
@@ -638,7 +637,6 @@ const EN = {
|
|
|
638
637
|
"chat.reasoning": "Thinking ({n} chars)",
|
|
639
638
|
"chat.think": "Think",
|
|
640
639
|
"langview.original": "Original",
|
|
641
|
-
"langview.bilingual": "Both",
|
|
642
640
|
"langview.translation": "Translation",
|
|
643
641
|
"msg.speak": "Speak",
|
|
644
642
|
"note.pin": "Pin note",
|
|
@@ -838,21 +836,6 @@ function escapeHtml(text) {
|
|
|
838
836
|
function inline(escaped) {
|
|
839
837
|
return escaped.replace(/`([^`]+)`/g, "<code>$1</code>").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>").replace(/!\[([^\]]*)\]\((https?:\/\/[^)\s]+|data:image\/[^)\s]+|\/lookatstudy\/api\/attachment\/[^)\s]+)\)/g, (_m, alt, src) => `<img src="${src}" alt="${alt}" loading="lazy">`).replace(/\[([^\]]+)\]\((https?:\/\/[^)\s]+)\)/g, "<a href=\"$2\" target=\"_blank\" rel=\"noreferrer\">$1</a>");
|
|
840
838
|
}
|
|
841
|
-
/** Interleave one body with its translation, paragraph by paragraph (upstream
|
|
842
|
-
* translation-layout "microsoft" semantics: original paragraph then its
|
|
843
|
-
* translation as a quote, tight pairing for bilingual reading; length
|
|
844
|
-
* mismatches append the remainder in order). Pure. */
|
|
845
|
-
function renderBilingual(bodyMd, translationMd) {
|
|
846
|
-
const par = (md) => md.split(/\n\s*\n/).map((p) => p.trim()).filter((p) => p !== "");
|
|
847
|
-
const a = par(bodyMd);
|
|
848
|
-
const b = par(translationMd);
|
|
849
|
-
const out = [];
|
|
850
|
-
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
851
|
-
if (a[i] !== void 0) out.push(a[i]);
|
|
852
|
-
if (b[i] !== void 0) out.push(`> ${b[i].split("\n").join("\n> ")}`);
|
|
853
|
-
}
|
|
854
|
-
return out.join("\n\n");
|
|
855
|
-
}
|
|
856
839
|
/** True when the line opens a GFM table row (pipes with a delimiter row next). */
|
|
857
840
|
function isTableRow(line) {
|
|
858
841
|
return line.trim().startsWith("|") && line.trim().endsWith("|") && line.includes("|", 1);
|
|
@@ -3138,7 +3121,6 @@ function workbenchState(state, now) {
|
|
|
3138
3121
|
markdown: ref.lesson.body,
|
|
3139
3122
|
speechText: normalizeSpeechText(normalizeMathNotation(ref.lesson.body)),
|
|
3140
3123
|
...ref.lesson.translation !== void 0 ? {
|
|
3141
|
-
bilingualHtml: renderMarkdown(normalizeMathNotation(renderBilingual(ref.lesson.body, ref.lesson.translation))),
|
|
3142
3124
|
translationHtml: renderMarkdown(normalizeMathNotation(ref.lesson.translation)),
|
|
3143
3125
|
translationLang: ref.lesson.translationLang ?? ""
|
|
3144
3126
|
} : {}
|
|
@@ -6599,6 +6581,50 @@ function validateDesign(design, validFiles) {
|
|
|
6599
6581
|
droppedLessons
|
|
6600
6582
|
};
|
|
6601
6583
|
}
|
|
6584
|
+
/**
|
|
6585
|
+
* Heading similarity (word-level overlap) — the translation misalignment
|
|
6586
|
+
* guard. Ported verbatim from upstream import-pipeline.ts (their audit F16,
|
|
6587
|
+
* 2026-09-13): ordinal alignment assumes both heading lists share structure;
|
|
6588
|
+
* a machine-translated file with a dropped header shifts every ordinal, and
|
|
6589
|
+
* A's translation would silently land on B's node — overlap too low skips the
|
|
6590
|
+
* segment (宁缺毋错). Empty titles cannot be checked and pass conservatively.
|
|
6591
|
+
*/
|
|
6592
|
+
function headingsSimilar(a, b) {
|
|
6593
|
+
const norm = (s) => s.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim().split(/\s+/).filter(Boolean);
|
|
6594
|
+
const A = norm(a);
|
|
6595
|
+
const B = norm(b);
|
|
6596
|
+
if (A.length === 0 || B.length === 0) return true;
|
|
6597
|
+
if (A.join(" ") === B.join(" ")) return true;
|
|
6598
|
+
const setB = new Set(B);
|
|
6599
|
+
return A.filter((w) => setB.has(w)).length / A.length >= .3;
|
|
6600
|
+
}
|
|
6601
|
+
/**
|
|
6602
|
+
* Replace a translation body's image references BY POSITION with the
|
|
6603
|
+
* original lesson's images (upstream method, import-pipeline.ts): translated
|
|
6604
|
+
* figure i ← original figure i (the original's refs are already absolute
|
|
6605
|
+
* CDN URLs or inlined data URLs); extras beyond the original count drop
|
|
6606
|
+
* (machine translation adding figures is rare — dropping is cleanest);
|
|
6607
|
+
* HTML imgs convert to markdown. One unified regex pass so HTML→markdown
|
|
6608
|
+
* conversion is never re-scanned with a shared index.
|
|
6609
|
+
*/
|
|
6610
|
+
function replaceImagesByPosition(content, originalImgs) {
|
|
6611
|
+
let imgIdx = 0;
|
|
6612
|
+
return content.replace(/!\[([^\]]*)\]\(([^)]+)\)|<img[^>]+src=["']([^"']+)["'][^>]*>/gi, (match, mdAlt, _mdSrc, htmlSrc) => {
|
|
6613
|
+
if (imgIdx >= originalImgs.length) return "";
|
|
6614
|
+
const replacement = originalImgs[imgIdx++];
|
|
6615
|
+
if (htmlSrc !== void 0) return `["']/i.exec(match)?.[1] ?? ""}](${replacement})`;
|
|
6616
|
+
return ``;
|
|
6617
|
+
});
|
|
6618
|
+
}
|
|
6619
|
+
/** The original lesson body's image refs in order (the position-mapping source). */
|
|
6620
|
+
function originalImagesOf(body) {
|
|
6621
|
+
const out = [];
|
|
6622
|
+
for (const m of body.matchAll(/!\[([^\]]*)\]\(([^)]+)\)|<img[^>]+src=["']([^"']+)["'][^>]*>/gi)) {
|
|
6623
|
+
const src = m[2] ?? m[3] ?? "";
|
|
6624
|
+
if (src !== "") out.push(src);
|
|
6625
|
+
}
|
|
6626
|
+
return out;
|
|
6627
|
+
}
|
|
6602
6628
|
/** GitHub-style anchor slug for ParsedLesson/ParsedSection anchors. */
|
|
6603
6629
|
function slugAnchor(title) {
|
|
6604
6630
|
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -6635,13 +6661,6 @@ function buildCourseFromDesign(courseTitle, validated, contents, translations) {
|
|
|
6635
6661
|
const titleIndex = lesson.anchor === null ? -1 : findTitleIndex(headings, lesson.anchor);
|
|
6636
6662
|
const isFirst = !firstLessonSeen.has(lesson.file);
|
|
6637
6663
|
firstLessonSeen.add(lesson.file);
|
|
6638
|
-
const pair = translations?.get(lesson.file);
|
|
6639
|
-
let translation;
|
|
6640
|
-
if (pair !== void 0) {
|
|
6641
|
-
const tHeadings = extractHeadings(pair.content);
|
|
6642
|
-
const tIndex = lesson.anchor === null ? -1 : findTitleIndex(tHeadings, lesson.anchor);
|
|
6643
|
-
translation = sliceLessonBody(pair.content, tHeadings, tIndex, isFirst);
|
|
6644
|
-
}
|
|
6645
6664
|
const body = sliceLessonBody(content, headings, titleIndex, isFirst);
|
|
6646
6665
|
if (body.length > 8e3 && splittableHeadingCount(headings, titleIndex) > 0) pacingViolations.push({
|
|
6647
6666
|
title: lesson.title,
|
|
@@ -6649,6 +6668,16 @@ function buildCourseFromDesign(courseTitle, validated, contents, translations) {
|
|
|
6649
6668
|
chars: body.length,
|
|
6650
6669
|
splittable: splittableHeadingCount(headings, titleIndex)
|
|
6651
6670
|
});
|
|
6671
|
+
const pair = translations?.get(lesson.file);
|
|
6672
|
+
let translation;
|
|
6673
|
+
if (pair !== void 0) {
|
|
6674
|
+
const tHeadings = extractHeadings(pair.content);
|
|
6675
|
+
let tIndex = titleIndex;
|
|
6676
|
+
let keep = true;
|
|
6677
|
+
if (lesson.anchor === null || titleIndex === -1) tIndex = -1;
|
|
6678
|
+
else if (titleIndex >= tHeadings.length || !headingsSimilar(lesson.title, tHeadings[titleIndex].title)) keep = false;
|
|
6679
|
+
if (keep) translation = replaceImagesByPosition(sliceLessonBody(pair.content, tHeadings, tIndex, isFirst), originalImagesOf(body));
|
|
6680
|
+
}
|
|
6652
6681
|
return {
|
|
6653
6682
|
title: lesson.title,
|
|
6654
6683
|
anchor: slugAnchor(lesson.title),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-lookatstudy",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.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",
|