dsh-m 0.9.46 → 0.9.48

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/CHANGELOG.md CHANGED
@@ -8,6 +8,21 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
8
8
 
9
9
  ## 中文
10
10
 
11
+ ### 0.9.48 变更:README 实体解码 + 相对路径仓库基址锚定(0.9.47 实装反馈两连修)
12
+
13
+ - **HTML 实体解码**:0.9.47 解析了标签但未解码实体——dsh-task-board 类 README 徽章间独立成行的 ` ` 按字面漏出。现命名实体(nbsp/amp/lt/gt/quot/copy/mdash/箭头/分数等 60+ 高频集)与数字实体(十进制/十六进制)均解码;文本段与 HTML 属性值都解(徽章 src 里 `&labelColor` → `&labelColor`,shields 参数不再丢失);`code`/`pre` 内不解码;未知实体与裸 `&` 原样保留;单趟解码不回炉(`<` → `<` 文本,与浏览器一致),解码结果只作为文本节点、绝不重新参与标签解析,无注入面。
14
+ - **行内优先判定**:包裹标签(`<p align="center">` 等)剥壳后,若内部无空行分段、无 markdown 块结构、且行首标签均为行内/void 标签,则整块按行内 token 流渲染——徽章 + `&nbsp;` 连排在同一居中行(GitHub 语义),不再每行拆成独立段落;含块级结构(`tr/td/ul/li` 开头等)仍走 markdown 递归保结构。
15
+ - **相对路径仓库基址锚定**:`[README.md](README.md)` 这类相对链接此前被 safeUrl 归 `#`(点击无反应——既有安全归化,非 0.9.47 回归)。现 README 预览可携带仓库基址:相对链接锚 `github.com/<o>/<r>/blob/HEAD/<path>`、相对图片锚 `raw.githubusercontent.com/<o>/<r>/HEAD/<path>`(`./`/`../` 前缀归一);绝对 URL 与页内锚点不受影响;无基址时保持归 `#` 旧行为。数据链路:npm packument 兜底时顺带提取 `repository` 归一为 `owner/repo`(仅 GitHub,git+https/git@/.git 形态归一,host-api 透传 `repo` 字段);客户端以卡片 `github` 字段兜底;渲染基址为 renderMarkdown 级同步设置、进出恢复,不跨渲染泄漏。
16
+ - **测试**:client-markdown 净增 6 例(实体四类、独立 `&nbsp;` 徽章块、code 不解码与属性解码、基址锚定七断言、无基址回归与跨渲染泄漏);versions 净增 1 例(repository 两形态归一 + 非 GitHub 归空);全量 1181/1181、typecheck 零错误;真实数据端到端——dsh-TUI 25926 字符零泄漏零回归、task-board 真实徽章块三徽章一行居中 + `&amp;` 解码、相对链接正确锚 blob/HEAD。纯客户端变更,刷新页面即生效。
17
+
18
+ ### 0.9.47 变更:README 预览内嵌 HTML 子集渲染(GitHub 风 README 不再满屏标签)
19
+
20
+ - **问题**:市场/收藏/已装详情 Modal 的 README 折叠页,遇到 GitHub 风重度内嵌 HTML 的 README(`<p align="center">` 徽章墙、`<img>` Logo、`<details>` 折叠等——npm packument 兜底与本地读取都常见)会把标签按字面整屏显示,观感如「乱码」。根因是渲染端自研 markdown 渲染器此前完全不解析 HTML 标签(原始内容本身没有问题,GitHub 上渲染正常)。
21
+ - **渲染子集(`src/client/markdown.js`)**:块级包裹标签(`p/div/center/blockquote/details/ul/ol/li/table/thead/tbody/tr/td/th/h1-6/figure/section/dl` 等)剥壳后按完整 markdown 递归渲染,内部标题/列表/表格/围栏结构全保留;行内标签(`a/img/strong/em/del/code/kbd/mark/sub/sup/u/small/span/br/hr` 等)token 化构造元素;`align` 属性映射 `textAlign`,`<img width/height>` 映射显式尺寸并放开徽章 20px 高度帽(无 width 的徽章仍走小图帽);`<details>/<summary>` 原生折叠;文本含标签的普通段落/标题/表格单元格也走行内 token 流(`Press <kbd>Ctrl</kbd>` 类可渲染)。
22
+ - **安全模型(不变式)**:绝不向 DOM 注入原始 HTML 字符串——只解析出白名单标签与白名单属性,全部经 `h()` 构造 React 元素,URL 一律过 `safeUrl` 闸门(`javascript:` 等归 `#`);未知标签剥壳留文本,`script/style/svg/iframe` 等容器连同内容整体丢弃;递归深度护栏(>8 层降级纯文本)。无 `dangerouslySetInnerHTML`,不存在 XSS 注入面。
23
+ - **已知边界**:npm packument 兜底的 README 不携带仓库基址,相对路径图片(如 `docs/assets/logo.svg`)按 safeUrl 归 `#` 后由 onError 隐藏(徽章等外链图片不受影响);`<picture><source>` 的 source 分支丢弃、保 `img` 回退。
24
+ - **测试**:`client-markdown.test.mjs` 净增 10 例(块级剥壳递归、徽章行连排、行内混排、script 丢弃与 javascript: 归化、未知标签剥壳、details/summary、整篇 div 包裹、HTML 表格、HTML 标题与行内元素、未闭合标签降级与深度护栏);真实 dsh-TUI README(25926 字符)渲染验证零标签泄漏、64 个徽章链接正常构造;全量测试 1175/1175,typecheck 零错误。纯客户端变更,刷新页面即生效。
25
+
11
26
  ### 0.9.46 变更:双语 README 精简重构+实拍 WebP 截图集(文档版)
12
27
 
13
28
  - **README×2 重构**:中文 17.5KB → 8.3KB、英文 18.1KB → 9.4KB——以用户任务动线重组(30 秒上手 → 浏览/搜索/安装 → 已装与设置 → Agent 工具与 CLI → 兼容与 FAQ);获取线路、缓存语义等实现细节不再展开,统一移交 [`docs/DESIGN.md`](docs/DESIGN.md) 与 ADR 承接;运行相位/生效时机表述与激活分桶语义对齐(不硬承诺「一定重启/一定刷新」,以操作返回为准)。
@@ -349,6 +364,21 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
349
364
 
350
365
  ## English
351
366
 
367
+ ### Added in 0.9.48 — README entity decoding + repo-base anchoring for relative paths (two fixes from 0.9.47 field feedback)
368
+
369
+ - **HTML entity decoding**: 0.9.47 parsed tags but left entities raw — standalone `&nbsp;` lines between badges (dsh-task-board-style READMEs) leaked literally. Named entities (nbsp/amp/lt/gt/quot/copy/mdash/arrows/fractions — 60+ common set) and numeric entities (decimal/hex) now decode; both text runs and HTML attribute values decode (badge src `&amp;labelColor` → `&labelColor`, shields params no longer dropped); `code`/`pre` content stays literal; unknown entities and bare `&` are preserved; single-pass decoding without re-processing (`&amp;lt;` → `&lt;` as text, matching browsers). Decoded output only ever becomes text nodes — never re-parsed as tags, no injection surface.
370
+ - **Inline-first heuristic**: after unwrapping a block wrapper (`<p align="center">` etc.), if the inner content has no blank-line paragraph breaks, no markdown block structures, and every line starts with an inline/void tag, the whole block renders through the inline token stream — badges + `&nbsp;` flow on one centered line (GitHub semantics) instead of one paragraph per line; content with block structures (lines starting with `tr/td/ul/li` etc.) still goes through the markdown recursion to preserve structure.
371
+ - **Repo-base anchoring for relative paths**: relative links like `[README.md](README.md)` previously collapsed to `#` via safeUrl (inert click — long-standing normalization, not a 0.9.47 regression). The README preview can now carry a repository base: relative links anchor to `github.com/<o>/<r>/blob/HEAD/<path>`, relative images anchor to `raw.githubusercontent.com/<o>/<r>/HEAD/<path>` (`./`/`../` prefixes normalized); absolute URLs and in-page anchors are untouched; without a base the old `#` behavior applies. Data flow: the npm packument fallback now also extracts `repository`, normalized to `owner/repo` (GitHub only; git+https/git@/.git forms normalized) and passed through host-api as `repo`; the client falls back to the card's `github` field; the render base is set per renderMarkdown call and restored on exit — no cross-render leakage.
372
+ - **Testing**: 6 new cases in client-markdown (four entity classes, standalone-`&nbsp;` badge block, code no-decode + attribute decoding, seven anchoring assertions, no-base regression + cross-render leakage); 1 new versions case (repository normalization in both shapes + non-GitHub → empty); full suite 1181/1181, typecheck clean; real-data end-to-end — dsh-TUI 25,926 chars zero leaks zero regression, the real task-board badge block renders three badges on one centered line with `&amp;` decoded, relative links anchor to blob/HEAD correctly. Client-only change — a page refresh takes effect.
373
+
374
+ ### Added in 0.9.47 — embedded-HTML subset rendering for the README preview (GitHub-style READMEs no longer show raw tags)
375
+
376
+ - **Problem**: the README fold in the detail Modal (market/curated/installed) showed GitHub-style HTML-heavy READMEs (`<p align="center">` badge walls, `<img>` logos, `<details>` blocks — common in both the npm packument fallback and local reads) with every tag rendered literally, looking like mojibake. Root cause: the in-house markdown renderer never parsed HTML tags at all. The source content itself was fine — GitHub renders it normally.
377
+ - **Rendering subset (`src/client/markdown.js`)**: block-level wrapper tags (`p/div/center/blockquote/details/ul/ol/li/table/thead/tbody/tr/td/th/h1-6/figure/section/dl` etc.) are unwrapped and their inner content re-rendered as full markdown, preserving headings/lists/tables/fences; inline tags (`a/img/strong/em/del/code/kbd/mark/sub/sup/u/small/span/br/hr` etc.) are tokenized into React elements; `align` maps to `textAlign`; `<img width/height>` maps to explicit size and lifts the 20px badge height cap (badges without width keep the small-image cap); `<details>/<summary>` render as native disclosure; ordinary paragraphs/headings/table cells whose text contains tags also go through the inline token stream (`Press <kbd>Ctrl</kbd>` renders properly).
378
+ - **Security model (invariant)**: raw HTML is never injected into the DOM — only whitelisted tags with whitelisted attributes are parsed, everything is built via `h()` React elements, and URLs pass the `safeUrl` gate (`javascript:` etc. collapse to `#`); unknown tags are unwrapped keeping their text, `script/style/svg/iframe` containers are dropped together with their content, and a recursion depth guard (>8) degrades to plain text. No `dangerouslySetInnerHTML` — no XSS surface.
379
+ - **Known limits**: the npm packument fallback carries no repository base URL, so relative-path images (e.g. `docs/assets/logo.svg`) collapse to `#` via safeUrl and hide on error (externally hosted badge images are unaffected); inside `<picture><source>` the source branch is dropped and the `img` fallback kept.
380
+ - **Testing**: 10 new cases in `client-markdown.test.mjs` (block unwrap recursion, badge rows, inline mix, script dropping + `javascript:` neutralization, unknown-tag unwrap, details/summary, whole-document div wrapper, HTML tables, HTML headings + inline elements, unclosed-tag degradation + depth guard); the real dsh-TUI README (25,926 chars) renders with zero tag leaks and 64 badge links constructed; full suite 1175/1175, typecheck clean. Client-only change — a page refresh takes effect.
381
+
352
382
  ### Added in 0.9.46 — bilingual README restructure + live-captured WebP screenshot set (docs release)
353
383
 
354
384
  - **README×2 restructured**: zh-CN 17.5KB → 8.3KB, en 18.1KB → 9.4KB — reorganized around the user's task flow (quick start → browse/search/install → installed & settings → agent tools & CLI → compatibility & FAQ). Fetch-chain and cache-semantics details no longer live here; they are delegated to [`docs/DESIGN.md`](docs/DESIGN.md) and the ADRs. Runtime-phase / takes-effect wording now matches the activation-bucket semantics (no hard promises of "always restart" or "always refresh" — follow the operation result).
package/docs/DESIGN.md CHANGED
@@ -182,6 +182,8 @@ npm 安装 / GitHub 安装 / 升级 / 自升级 / 卸载是**同一个事务模
182
182
  技术:`src/client.js` 经 **esbuild** 打包为 `lib/client.js`;`window.__ModuleLoader__.load({ id: "dsh-m", factory })` 注册;**React 从 module loader require**(零额外运行时依赖);manifest 注入 `@deepseek-ai/dsh-client-runtime`、`@deepseek-ai/dsh-client-ui-slots`、`@deepseek-ai/dsh-client-ui-settings`。
183
183
  图标:GitHub 来源自动用 `https://github.com/<owner>.png?size=64`;`icon` 字段可覆盖;npm-only 条目首字母色块回退。
184
184
 
185
+ **README 预览渲染(`src/client/markdown.js`,0.9.47 起)**:自研轻量渲染器,安全模型 = 「白名单标签 + 白名单属性 + safeUrl 闸门 → 全部经 `h()` 构造 React 元素」,**绝不注入原始 HTML 字符串**(无 `dangerouslySetInnerHTML`,文本节点 React 转义兜底)。markdown 侧支持标题/列表/表格/围栏/引用/徽章链接等;HTML 侧支持 GitHub 风 README 常见内嵌子集——块级包裹标签(`p/div/center/details/table/tr/td/li/ul/ol/h1-6` 等)剥壳后渲染:内容纯行内(无空行分段/无 markdown 块结构/行首均为行内标签)时整块行走内 token 流(徽章 + `&nbsp;` 连排一行,GitHub 语义,0.9.48),否则按 markdown 递归(保内部结构);行内标签(`a/img/strong/em/code/kbd/mark/br/sub/sup` 等)token 化渲染,`align` 属性映射 `textAlign`、`<img width/height>` 映射显式尺寸(放开徽章 20px 高度帽);未知标签剥壳留文本,`script/style/svg/iframe` 等容器连同内容丢弃,`javascript:` 等危险 URL 归 `#`。**HTML 实体解码(0.9.48)**:命名(60+ 高频集)+ 数字(十/十六进制),文本段与属性值都解、`code/pre` 不解、单趟不回炉、解码结果仅作文本节点。**仓库基址锚定(0.9.48)**:`renderMarkdown(src, { repo: "owner/repo" })` 后相对链接锚 `github.com/<o>/<r>/blob/HEAD/`、相对图片锚 `raw.githubusercontent.com/<o>/<r>/HEAD/`;基址来源 = npm packument `repository`(host-api readme 结果 `repo` 字段,仅 GitHub 归一)> 卡片 `github` 字段;无基址时相对路径归 `#`(0.9.47 前行为)。已知边界:monorepo 子目录 README 的相对路径锚到仓库根(packument 不携带子目录信息);绝对 URL 与页内锚点不受锚定影响。数据源:已装条目读 profile 本地 `README.md`(≤64KB 截断);未装条目回源 npm packument 顶层 `readme` 字段(0.9.45 U10b)。
186
+
185
187
  ## 5. Agent 工具(host)与 CLI
186
188
 
187
189
  - 工具前缀 `dshm_`,共 7 个:`dshm_search` / `dshm_list` / `dshm_install` / `dshm_uninstall` / `dshm_outdated` / `dshm_upgrade` / `dshm_restart`。Agent tools 运行于 Host 进程,与 Web GUI **共用 `host` namespace 缓存与 active config**;`dshm_search` 走服务端过滤(metadata-only,`withLatest=false`、limit ≤80),返回不含本地路径的短 summary。
package/lib/client.js CHANGED
@@ -399,21 +399,110 @@ var init_backdrop = __esm({
399
399
  var markdown_exports = {};
400
400
  __export(markdown_exports, {
401
401
  createMarkdown: () => createMarkdown,
402
+ decodeEntities: () => decodeEntities,
402
403
  safeUrl: () => safeUrl
403
404
  });
404
- function safeUrl(u) {
405
+ function normalizeRepoInput(v) {
406
+ const s = String(v || "").trim();
407
+ if (!s) return "";
408
+ if (/^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/.test(s)) return s;
409
+ const m = /github\.com[/:]([a-zA-Z0-9._-]+)\/([a-zA-Z0-9._-]+?)(?:\.git)?(?:[/?#]|$)/i.exec(s);
410
+ return m ? `${m[1]}/${m[2]}` : "";
411
+ }
412
+ function safeUrl(u, kind) {
405
413
  const t = String(u || "").trim();
406
414
  if (/^(https?:\/\/|mailto:)/i.test(t)) return t;
407
- if (/^[/#]/.test(t)) return t;
415
+ if (t.startsWith("#")) return t;
416
+ if (RENDER_REPO && (kind === "img" || kind === "link")) {
417
+ const path = t.replace(/^(?:\.\/|\.\.\/|\/)+/, "");
418
+ return kind === "img" ? `https://raw.githubusercontent.com/${RENDER_REPO}/HEAD/${path}` : `https://github.com/${RENDER_REPO}/blob/HEAD/${path}`;
419
+ }
420
+ if (t.startsWith("/")) return t;
408
421
  return "#";
409
422
  }
423
+ function decodeEntities(s) {
424
+ const t = String(s || "");
425
+ if (!t.includes("&")) return t;
426
+ return t.replace(HTML_ENTITY_RE, (raw, body) => {
427
+ if (body.startsWith("#")) {
428
+ const code = body[1] === "x" || body[1] === "X" ? parseInt(body.slice(2), 16) : parseInt(body.slice(1), 10);
429
+ return Number.isInteger(code) && code > 0 && code <= 1114111 ? String.fromCodePoint(code) : raw;
430
+ }
431
+ const named = HTML_ENTITIES[body] !== void 0 ? HTML_ENTITIES[body] : HTML_ENTITIES[body.toLowerCase()];
432
+ return named !== void 0 ? named : raw;
433
+ });
434
+ }
435
+ function parseHtmlAttrs(s) {
436
+ const attrs = {};
437
+ if (!s) return attrs;
438
+ const re = /([a-zA-Z][a-zA-Z0-9-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
439
+ let m;
440
+ while (m = re.exec(s)) {
441
+ const raw = m[2] !== void 0 ? m[2] : m[3] !== void 0 ? m[3] : m[4] !== void 0 ? m[4] : "";
442
+ attrs[m[1].toLowerCase()] = decodeEntities(raw);
443
+ }
444
+ return attrs;
445
+ }
446
+ function htmlAlignStyle(attrs) {
447
+ const a = String(attrs.align || "").toLowerCase();
448
+ return a === "center" || a === "left" || a === "right" || a === "justify" ? { textAlign: a } : void 0;
449
+ }
450
+ function htmlImgStyle(attrs) {
451
+ const size = (v) => /^\d+(\.\d+)?$/.test(v) ? Number(v) : /^\d+(\.\d+)?%$/.test(v) ? v : null;
452
+ const st = {};
453
+ const w = size(String(attrs.width || ""));
454
+ if (w !== null) {
455
+ st.width = w;
456
+ st.maxWidth = "100%";
457
+ st.height = "auto";
458
+ st.maxHeight = "none";
459
+ }
460
+ const hgt = size(String(attrs.height || ""));
461
+ if (hgt !== null) st.height = hgt;
462
+ return Object.keys(st).length ? st : void 0;
463
+ }
464
+ function htmlBlockEnd(lines, i) {
465
+ const om = /<([a-zA-Z][a-zA-Z0-9-]*)/.exec(lines[i]);
466
+ const tag = om ? om[1].toLowerCase() : "";
467
+ if (!tag || HTML_VOID_TAGS.has(tag)) return i;
468
+ if (new RegExp(`</${tag}\\s*>`, "i").test(lines[i].slice(om.index))) return i;
469
+ const closeRe = new RegExp(`</${tag}\\s*>`, "i");
470
+ let lastNonBlank = i;
471
+ for (let j = i + 1; j < lines.length; j++) {
472
+ if (!lines[j].trim()) {
473
+ if (!HTML_SPAN_BLANK.has(tag) || j - i > 400) return lastNonBlank;
474
+ continue;
475
+ }
476
+ lastNonBlank = j;
477
+ if (closeRe.test(lines[j])) return j;
478
+ if (j - i > 400) return j;
479
+ }
480
+ return lastNonBlank;
481
+ }
482
+ function decodeTextNodes(nodes) {
483
+ return nodes.map((n) => {
484
+ if (typeof n === "string") return decodeEntities(n);
485
+ if (Array.isArray(n)) return decodeTextNodes(n);
486
+ if (n && typeof n === "object" && n.type !== "code" && n.type !== "pre") {
487
+ const cur = n.props && n.props.children !== void 0 ? n.props.children : n.children;
488
+ if (cur !== void 0) {
489
+ const wasArr = Array.isArray(cur);
490
+ const decoded = decodeTextNodes(wasArr ? cur : [cur]);
491
+ const next = wasArr ? decoded : decoded[0];
492
+ if (n.props && n.props.children !== void 0) return { ...n, props: { ...n.props, children: next } };
493
+ return { ...n, children: next };
494
+ }
495
+ }
496
+ return n;
497
+ });
498
+ }
410
499
  function createMarkdown(h2) {
411
500
  function ExtLink2({ href, className, children }) {
412
501
  return h2(
413
502
  "a",
414
503
  {
415
504
  className: className || "dshm-md-a",
416
- href: safeUrl(href),
505
+ href: safeUrl(href, "link"),
417
506
  target: "_blank",
418
507
  rel: "noopener noreferrer",
419
508
  onClick: (e) => e.stopPropagation()
@@ -421,11 +510,13 @@ function createMarkdown(h2) {
421
510
  children
422
511
  );
423
512
  }
424
- function MdImg2({ src, alt }) {
513
+ function MdImg2({ src, alt, style, title }) {
425
514
  return h2("img", {
426
515
  className: "dshm-md-img",
427
- src: safeUrl(src),
516
+ src: safeUrl(src, "img"),
428
517
  alt: alt || "",
518
+ title: title || void 0,
519
+ style: style || void 0,
429
520
  referrerPolicy: "no-referrer",
430
521
  onError: (e) => {
431
522
  e.currentTarget.style.display = "none";
@@ -448,7 +539,7 @@ function createMarkdown(h2) {
448
539
  const lm = /\]\(([^)]*)\)\s*$/.exec(tok);
449
540
  const img = h2(MdImg2, { src: im && im[2], alt: im && im[1] });
450
541
  const href = lm && lm[1];
451
- nodes.push(href && safeUrl(href) !== "#" ? h2(ExtLink2, { key: k, href }, img) : h2("span", { key: k }, img));
542
+ nodes.push(href && safeUrl(href, "link") !== "#" ? h2(ExtLink2, { key: k, href }, img) : h2("span", { key: k }, img));
452
543
  } else if (tok.startsWith("![") || tok.startsWith("<![")) {
453
544
  const im = /^!\[([^\]]*)\]\(([^)]*)\)$/.exec(tok);
454
545
  nodes.push(h2(MdImg2, { key: k, src: im && im[2], alt: im && im[1] }));
@@ -474,7 +565,7 @@ function createMarkdown(h2) {
474
565
  if (last < src.length) nodes.push(src.slice(last));
475
566
  return nodes;
476
567
  }
477
- function mdBlocks(lines, kb) {
568
+ function mdBlocks(lines, kb, depth = 0) {
478
569
  const out = [];
479
570
  let i = 0;
480
571
  let n = 0;
@@ -485,6 +576,8 @@ function createMarkdown(h2) {
485
576
  const isUl = (s) => /^\s*[-*+]\s+/.test(s);
486
577
  const isOl = (s) => /^\s*\d+[.)]\s+/.test(s);
487
578
  const isTableRow = (s) => s.includes("|") && /^\s*\|/.test(s);
579
+ const isHtmlBlock = (s) => /^\s*<\/?[a-zA-Z][a-zA-Z0-9-]*(?:\s[^>]*?)?\/?>/.test(s);
580
+ const inlineOf = (s, key) => decodeTextNodes(/<[a-zA-Z]/.test(s) ? mdHtmlInline(s, key) : mdInline(s, key));
488
581
  while (i < lines.length) {
489
582
  const line = lines[i];
490
583
  if (!line.trim()) {
@@ -492,6 +585,12 @@ function createMarkdown(h2) {
492
585
  continue;
493
586
  }
494
587
  const k = `${kb}-b${n++}`;
588
+ if (isHtmlBlock(line)) {
589
+ const end = htmlBlockEnd(lines, i);
590
+ out.push(...mdHtmlBlock(lines.slice(i, end + 1).join("\n"), `${k}H`, depth));
591
+ i = end + 1;
592
+ continue;
593
+ }
495
594
  if (isFence(line)) {
496
595
  const buf2 = [];
497
596
  i++;
@@ -502,7 +601,7 @@ function createMarkdown(h2) {
502
601
  }
503
602
  if (isHeading(line)) {
504
603
  const hm = /^(#{1,6})\s+(.*)$/.exec(line);
505
- out.push(h2(`h${hm[1].length}`, { key: k }, mdInline(hm[2], k)));
604
+ out.push(h2(`h${hm[1].length}`, { key: k }, inlineOf(hm[2], k)));
506
605
  i++;
507
606
  continue;
508
607
  }
@@ -514,7 +613,7 @@ function createMarkdown(h2) {
514
613
  if (isQuote(line)) {
515
614
  const buf2 = [];
516
615
  while (i < lines.length && isQuote(lines[i])) buf2.push(lines[i++].replace(/^\s*>\s?/, ""));
517
- out.push(h2("blockquote", { key: k }, mdBlocks(buf2, k)));
616
+ out.push(h2("blockquote", { key: k }, mdBlocks(buf2, k, depth)));
518
617
  continue;
519
618
  }
520
619
  if (isUl(line) || isOl(line)) {
@@ -522,7 +621,7 @@ function createMarkdown(h2) {
522
621
  const re = ordered ? /^\s*\d+[.)]\s+(.*)$/ : /^\s*[-*+]\s+(.*)$/;
523
622
  const items = [];
524
623
  while (i < lines.length && (ordered ? isOl(lines[i]) : isUl(lines[i]))) {
525
- items.push(h2("li", { key: `li${items.length}` }, mdInline(re.exec(lines[i])[1], `${k}-${items.length}`)));
624
+ items.push(h2("li", { key: `li${items.length}` }, inlineOf(re.exec(lines[i])[1], `${k}-${items.length}`)));
526
625
  i++;
527
626
  }
528
627
  out.push(h2(ordered ? "ol" : "ul", { key: k }, items));
@@ -541,30 +640,160 @@ function createMarkdown(h2) {
541
640
  h2(
542
641
  "table",
543
642
  { key: k },
544
- h2("thead", null, h2("tr", null, head.map((c, x) => h2("th", { key: x }, mdInline(c, `${k}h${x}`))))),
545
- h2("tbody", null, rows.map((r, y) => h2("tr", { key: y }, r.map((c, x) => h2("td", { key: x }, mdInline(c, `${k}${y}x${x}`))))))
643
+ h2("thead", null, h2("tr", null, head.map((c, x) => h2("th", { key: x }, inlineOf(c, `${k}h${x}`))))),
644
+ h2("tbody", null, rows.map((r, y) => h2("tr", { key: y }, r.map((c, x) => h2("td", { key: x }, inlineOf(c, `${k}${y}x${x}`))))))
546
645
  )
547
646
  );
548
647
  continue;
549
648
  }
550
649
  const buf = [line];
551
650
  i++;
552
- while (i < lines.length && lines[i].trim() && !isFence(lines[i]) && !isHeading(lines[i]) && !isHr(lines[i]) && !isQuote(lines[i]) && !isUl(lines[i]) && !isOl(lines[i])) {
651
+ while (i < lines.length && lines[i].trim() && !isFence(lines[i]) && !isHeading(lines[i]) && !isHr(lines[i]) && !isQuote(lines[i]) && !isUl(lines[i]) && !isOl(lines[i]) && !isHtmlBlock(lines[i])) {
553
652
  buf.push(lines[i]);
554
653
  i++;
555
654
  }
556
- out.push(h2("p", { key: k }, mdInline(buf.join(" "), k)));
655
+ out.push(h2("p", { key: k }, inlineOf(buf.join(" "), k)));
656
+ }
657
+ return out;
658
+ }
659
+ function mdHtmlInline(src, kb) {
660
+ const out = [];
661
+ const stack = [];
662
+ let last = 0;
663
+ let n = 0;
664
+ let m;
665
+ const cur = () => stack.length ? stack[stack.length - 1].kids : out;
666
+ const pushText = (t) => {
667
+ if (!t) return;
668
+ const t2 = decodeEntities(t);
669
+ if (!t2.trim()) {
670
+ if (cur().length) cur().push(" ");
671
+ return;
672
+ }
673
+ const parts = t2.split(/\n[ \t]*\n+/);
674
+ parts.forEach((part, idx) => {
675
+ const nodes = decodeTextNodes(mdInline(part.replace(/\s+/g, " "), `${kb}t${n++}`));
676
+ if (!nodes.length) return;
677
+ if (idx === 0 || stack.length) cur().push(...nodes);
678
+ else out.push(h2("p", { key: `${kb}tp${n++}` }, ...nodes));
679
+ });
680
+ };
681
+ const closeTop = () => {
682
+ const f = stack.pop();
683
+ cur().push(f.build(f.kids));
684
+ };
685
+ const sepNode = (name, key) => {
686
+ const sep = HTML_UNWRAP_SEP[name];
687
+ if (sep === "br") cur().push(h2("br", { key }));
688
+ else if (sep === "sp") cur().push(" ");
689
+ };
690
+ HTML_TAG_RE.lastIndex = 0;
691
+ while (m = HTML_TAG_RE.exec(src)) {
692
+ if (m.index > last) pushText(src.slice(last, m.index));
693
+ const full = m[0];
694
+ const name = m[2].toLowerCase();
695
+ const key = `${kb}h${n++}`;
696
+ if (m[1]) {
697
+ const idx = stack.map((f) => f.name).lastIndexOf(name);
698
+ if (idx >= 0) {
699
+ while (stack.length > idx + 1) closeTop();
700
+ closeTop();
701
+ } else sepNode(name, key);
702
+ } else if (HTML_SKIP_TAGS.has(name)) {
703
+ const cm = new RegExp(`</${name}\\s*>`, "i").exec(src.slice(m.index + full.length));
704
+ HTML_TAG_RE.lastIndex = cm ? m.index + full.length + cm.index + cm[0].length : src.length;
705
+ last = HTML_TAG_RE.lastIndex;
706
+ continue;
707
+ } else if (name === "img") {
708
+ const attrs = parseHtmlAttrs(m[3]);
709
+ cur().push(h2(MdImg2, { key, src: attrs.src, alt: attrs.alt, title: attrs.title, style: htmlImgStyle(attrs) }));
710
+ } else if (name === "br" || name === "hr") {
711
+ cur().push(h2(name, { key }));
712
+ } else if (HTML_VOID_TAGS.has(name) || m[4]) {
713
+ } else if (HTML_INLINE_MAP[name]) {
714
+ const attrs = parseHtmlAttrs(m[3]);
715
+ if (name === "a") {
716
+ stack.push({ name, kids: [], build: (kids) => h2(ExtLink2, { key, href: attrs.href, title: attrs.title }, kids) });
717
+ } else {
718
+ const type = HTML_INLINE_MAP[name];
719
+ const style = htmlAlignStyle(attrs);
720
+ stack.push({ name, kids: [], build: (kids) => h2(type, style ? { key, style } : { key }, kids) });
721
+ }
722
+ } else if (/^h[1-6]$/.test(name)) {
723
+ const style = htmlAlignStyle(parseHtmlAttrs(m[3]));
724
+ const lvl = name;
725
+ stack.push({ name, kids: [], build: (kids) => h2(lvl, style ? { key, style } : { key }, kids) });
726
+ } else {
727
+ sepNode(name, key);
728
+ }
729
+ last = m.index + full.length;
557
730
  }
731
+ if (last < src.length) pushText(src.slice(last));
732
+ while (stack.length) closeTop();
558
733
  return out;
559
734
  }
560
- function renderMarkdown2(src) {
561
- return mdBlocks(String(src || "").replace(/\r\n?/g, "\n").split("\n"), "md");
735
+ function isInlineOnlyHtml(inner) {
736
+ const lines = String(inner).split("\n");
737
+ if (lines.some((l) => l !== "" && !l.trim())) return false;
738
+ return !lines.some((l) => {
739
+ if (/^\s*(#{1,6}\s|[-*+]\s|\d+[.)]\s|>|`|\|)/.test(l)) return true;
740
+ const tm = /^\s*<\/?([a-zA-Z][a-zA-Z0-9-]*)/.exec(l);
741
+ if (!tm) return false;
742
+ const name = tm[1].toLowerCase();
743
+ return !(name === "img" || name === "br" || name === "hr" || HTML_INLINE_MAP[name]);
744
+ });
745
+ }
746
+ function mdHtmlBlock(raw, kb, depth) {
747
+ const s = String(raw).trim();
748
+ if (depth > 8) return [s];
749
+ const om = /^<([a-zA-Z][a-zA-Z0-9-]*)((?:\s[^>]*)?)>/.exec(s);
750
+ if (!om) return mdHtmlInline(s, kb);
751
+ const tag = om[1].toLowerCase();
752
+ if (!HTML_BLOCK_WRAP.has(tag)) return mdHtmlInline(s, kb);
753
+ const cm = new RegExp(`</${tag}\\s*>\\s*$`, "i").exec(s.slice(om[0].length));
754
+ if (!cm) return mdHtmlInline(s, kb);
755
+ let inner = s.slice(om[0].length, om[0].length + cm.index);
756
+ const attrs = parseHtmlAttrs(om[2] || "");
757
+ const style = htmlAlignStyle(attrs);
758
+ const inlineKids = () => isInlineOnlyHtml(inner) ? mdHtmlInline(inner, kb) : renderMarkdown2(inner, depth + 1);
759
+ if (tag === "details") {
760
+ const sm = /<summary[^>]*>([\s\S]*?)<\/summary>/i.exec(inner);
761
+ let sumNode = null;
762
+ if (sm) {
763
+ inner = inner.slice(0, sm.index) + inner.slice(sm.index + sm[0].length);
764
+ sumNode = h2("summary", { key: `${kb}sum` }, ...mdHtmlInline(sm[1], `${kb}sm`));
765
+ }
766
+ const kids = sumNode ? [sumNode, ...inlineKids()] : inlineKids();
767
+ return [h2("details", attrs.open !== void 0 ? { key: kb, open: true } : { key: kb }, ...kids)];
768
+ }
769
+ if (/^h[1-6]$/.test(tag)) return [h2(tag, style ? { key: kb, style } : { key: kb }, ...mdHtmlInline(inner, kb))];
770
+ return [h2(tag, style ? { key: kb, style } : { key: kb }, ...inlineKids())];
771
+ }
772
+ function renderMarkdown2(src, opts = {}, depth = 0) {
773
+ const prev = RENDER_REPO;
774
+ RENDER_REPO = normalizeRepoInput(opts && opts.repo);
775
+ try {
776
+ return mdBlocks(String(src || "").replace(/\r\n?/g, "\n").split("\n"), "md", depth);
777
+ } finally {
778
+ RENDER_REPO = prev;
779
+ }
562
780
  }
563
781
  return { ExtLink: ExtLink2, MdImg: MdImg2, renderMarkdown: renderMarkdown2 };
564
782
  }
783
+ var RENDER_REPO, HTML_ENTITIES, HTML_ENTITY_RE, HTML_TAG_RE, HTML_VOID_TAGS, HTML_SKIP_TAGS, HTML_BLOCK_WRAP, HTML_SPAN_BLANK, HTML_INLINE_MAP, HTML_UNWRAP_SEP;
565
784
  var init_markdown = __esm({
566
785
  "src/client/markdown.js"() {
567
786
  "use strict";
787
+ RENDER_REPO = "";
788
+ HTML_ENTITIES = { nbsp: "\xA0", amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", copy: "\xA9", reg: "\xAE", trade: "\u2122", mdash: "\u2014", ndash: "\u2013", hellip: "\u2026", middot: "\xB7", laquo: "\xAB", raquo: "\xBB", times: "\xD7", divide: "\xF7", plusmn: "\xB1", deg: "\xB0", sup2: "\xB2", sup3: "\xB3", frac12: "\xBD", frac14: "\xBC", frac34: "\xBE", euro: "\u20AC", pound: "\xA3", yen: "\xA5", cent: "\xA2", sect: "\xA7", para: "\xB6", bull: "\u2022", dagger: "\u2020", Dagger: "\u2021", permil: "\u2030", prime: "\u2032", Prime: "\u2033", larr: "\u2190", uarr: "\u2191", rarr: "\u2192", darr: "\u2193", harr: "\u2194", minus: "\u2212", infin: "\u221E", ne: "\u2260", le: "\u2264", ge: "\u2265", asymp: "\u2248", equiv: "\u2261", check: "\u2713", cross: "\u2717", star: "\u2606", starf: "\u2605", hearts: "\u2665", alpha: "\u03B1", beta: "\u03B2", gamma: "\u03B3", delta: "\u03B4", pi: "\u03C0", Omega: "\u03A9", ldquo: "\u201C", rdquo: "\u201D", lsquo: "\u2018", rsquo: "\u2019" };
789
+ HTML_ENTITY_RE = /&(#[0-9]+|#x[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);/g;
790
+ HTML_TAG_RE = /<(\/?)([a-zA-Z][a-zA-Z0-9-]*)((?:\s+[a-zA-Z][a-zA-Z0-9-]*(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*)\s*(\/?)>/g;
791
+ HTML_VOID_TAGS = /* @__PURE__ */ new Set(["br", "hr", "img", "source", "input", "wbr", "area", "col", "link", "meta"]);
792
+ HTML_SKIP_TAGS = /* @__PURE__ */ new Set(["script", "style", "iframe", "object", "embed", "noscript", "svg", "title", "textarea", "select", "video", "audio", "canvas", "template", "form", "button", "head"]);
793
+ HTML_BLOCK_WRAP = /* @__PURE__ */ new Set(["p", "div", "center", "blockquote", "details", "ul", "ol", "li", "table", "thead", "tbody", "tr", "td", "th", "figure", "figcaption", "section", "article", "main", "header", "footer", "nav", "aside", "dl", "dt", "dd"]);
794
+ HTML_SPAN_BLANK = /* @__PURE__ */ new Set(["details", "div", "blockquote", "table", "thead", "tbody", "tr", "ul", "ol", "dl", "figure", "section", "article", "td", "th", "li"]);
795
+ HTML_INLINE_MAP = { a: "a", strong: "strong", b: "strong", em: "em", i: "em", del: "del", s: "del", strike: "del", code: "code", kbd: "kbd", sub: "sub", sup: "sup", u: "u", small: "small", mark: "mark", span: "span" };
796
+ HTML_UNWRAP_SEP = { tr: "br", li: "br", p: "br", h1: "br", h2: "br", h3: "br", h4: "br", h5: "br", h6: "br", table: "br", ul: "br", ol: "br", td: "sp", th: "sp" };
568
797
  }
569
798
  });
570
799
 
@@ -2031,6 +2260,12 @@ button.dshm-badge:hover{filter:brightness(.95)}
2031
2260
  .dshm-readme.md th{background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.1))}
2032
2261
  .dshm-readme.md hr{border:0;border-top:1px solid var(--dsw-alias-border-l2,#cbd5e1);margin:8px 0}
2033
2262
  .dshm-readme.md .dshm-md-note{margin-top:8px;padding-top:6px;border-top:1px dashed var(--dsw-alias-border-l2,#cbd5e1);color:var(--dsw-alias-label-caption,#9ca3af);font-size:11px}
2263
+ /* 0.9.47 HTML \u5B50\u96C6\uFF1Adetails/summary/kbd/mark \u4E0E li\xB7td \u5185\u5C42 p \u95F4\u8DDD\u6536\u655B */
2264
+ .dshm-readme.md details{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:8px;padding:4px 10px;margin:6px 0}
2265
+ .dshm-readme.md summary{cursor:pointer;font-weight:600;margin:2px 0}
2266
+ .dshm-readme.md kbd{background:rgba(127,127,127,.16);border:1px solid var(--dsw-alias-border-l2,#cbd5e1);border-bottom-width:2px;border-radius:4px;padding:0 4px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px}
2267
+ .dshm-readme.md mark{background:rgba(255,213,0,.35);color:inherit;border-radius:2px;padding:0 2px}
2268
+ .dshm-readme.md li>p,.dshm-readme.md td>p,.dshm-readme.md th>p,.dshm-readme.md summary>p,.dshm-readme.md p>p{margin:2px 0}
2034
2269
  .dshm-links{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:7px;padding-top:6px;border-top:1px solid var(--dsw-alias-border-l2,#e5e7eb);font-size:11px;color:var(--dsw-alias-label-caption,#6b7280)}
2035
2270
  .dshm-links-k,.dshm-links-sep{color:var(--dsw-alias-label-caption,#9ca3af)}
2036
2271
  .dshm-links a{color:var(--dsw-alias-state-business-primary,#4d6bfe);text-decoration:none;font-weight:500}
@@ -2655,7 +2890,7 @@ function DetailModal({ it, labels, busy, onClose, onInstall, onUpgrade, upgradeB
2655
2890
  if (e.target.open) setRmOpen(true);
2656
2891
  } },
2657
2892
  h("summary", null, lookup("readme.show")),
2658
- rmOpen ? h(ReadmeBlock, { pkg: it.npm }) : h("div", { className: "dshm-hint" }, lookup("readme.loading"))
2893
+ rmOpen ? h(ReadmeBlock, { pkg: it.npm, repo: it.github }) : h("div", { className: "dshm-hint" }, lookup("readme.loading"))
2659
2894
  ) : null,
2660
2895
  shots.length ? h(
2661
2896
  "div",
@@ -3169,11 +3404,11 @@ function ProgressLine() {
3169
3404
  st.currentPackage ? h("span", { className: "dshm-hint" }, String(st.currentPackage).slice(0, 44)) : null
3170
3405
  );
3171
3406
  }
3172
- function ReadmeBlock({ pkg }) {
3173
- const [state, setState] = useState({ loading: true, text: "", err: "", truncated: false });
3407
+ function ReadmeBlock({ pkg, repo }) {
3408
+ const [state, setState] = useState({ loading: true, text: "", err: "", truncated: false, repo: "" });
3174
3409
  useEffect(() => {
3175
3410
  let live = true;
3176
- api("readme", { pkg }).then((d) => live && setState({ loading: false, text: d.readme, err: "", truncated: d.truncated })).catch((e) => live && setState({ loading: false, text: "", err: String(e && e.message || e) }));
3411
+ api("readme", { pkg }).then((d) => live && setState({ loading: false, text: d.readme, err: "", truncated: d.truncated, repo: d.repo || "" })).catch((e) => live && setState({ loading: false, text: "", err: String(e && e.message || e) }));
3177
3412
  return () => {
3178
3413
  live = false;
3179
3414
  };
@@ -3184,7 +3419,7 @@ function ReadmeBlock({ pkg }) {
3184
3419
  return h(
3185
3420
  "div",
3186
3421
  { className: "dshm-readme md", onClick: (e) => e.stopPropagation() },
3187
- renderMarkdown(state.text),
3422
+ renderMarkdown(state.text, { repo: state.repo || repo }),
3188
3423
  state.truncated ? h("div", { className: "dshm-md-note" }, lookup("readme.truncated")) : null
3189
3424
  );
3190
3425
  }
@@ -3325,7 +3560,7 @@ function InstalledTab({ notify, installed, updates, onMutation, ops }) {
3325
3560
  }),
3326
3561
  open: openPkg === it.pkg,
3327
3562
  onToggle: () => setOpenPkg(openPkg === it.pkg ? null : it.pkg),
3328
- detail: readmePkg === it.pkg ? h(ReadmeBlock, { pkg: it.pkg }) : DetailRows([
3563
+ detail: readmePkg === it.pkg ? h(ReadmeBlock, { pkg: it.pkg, repo: it.github }) : DetailRows([
3329
3564
  [lookup("detail.pkg"), it.pkg],
3330
3565
  [lookup("detail.spec"), it.spec],
3331
3566
  [lookup("detail.latest"), vm.latestLabelDetail],
@@ -449,7 +449,7 @@ export function createApiDispatcher(ctx) {
449
449
  catch (localErr) {
450
450
  try {
451
451
  const fallback = await d.npmPackumentReadme(target);
452
- result = { pkg: target, name: target, readme: fallback.readme, truncated: false };
452
+ result = { pkg: target, name: target, readme: fallback.readme, truncated: false, repo: fallback.repo || undefined };
453
453
  }
454
454
  catch {
455
455
  throw localErr;
@@ -113,11 +113,28 @@ export async function npmPackument(pkg, timeoutMs = 20_000, signal, registry, de
113
113
  const versions = data?.versions !== null && typeof data?.versions === 'object' ? Object.keys(data.versions) : [];
114
114
  return { versions };
115
115
  }
116
+ /**
117
+ * packument repository → "owner/repo"(仅 github.com;git+https / git@ / .git / #frag 归一;
118
+ * 非 GitHub 返回空串——README 相对路径锚定只对 GitHub raw/blob 有意义)。0.9.48。
119
+ */
120
+ export function extractGithubRepo(v) {
121
+ const url = typeof v === 'string'
122
+ ? v
123
+ : v && typeof v === 'object' && typeof v.url === 'string'
124
+ ? v.url
125
+ : '';
126
+ if (!url)
127
+ return '';
128
+ const m = /github\.com[/:]([A-Za-z0-9._-]+)\/([A-Za-z0-9._-]+?)(?:\.git)?(?:[/?#]|$)/i.exec(url);
129
+ return m ? `${m[1]}/${m[2]}` : '';
130
+ }
116
131
  /**
117
132
  * npm packument readme 兜底(0.9.45 U10b 修复):市场/收藏详情 Modal 的 README 折叠页对
118
133
  * 「未安装」条目无法走本地 profile 读取——回源 packument 顶层 readme 字段(publish 管线
119
134
  * 对多数包写入)。安全基线同 npmPackument:HTTPS、8MB 上限、超时;无 readme 字段返回空串
120
135
  * (不抛,客户端显示「没有 README」);L1(ADR-0012):显式 registry 参数 > 生效源。
136
+ * 0.9.48:随附 repo(packument repository 归一为 owner/repo,仅 GitHub),供 README
137
+ * 相对链接/图片锚定仓库基址;无 repository 或非 GitHub 时为空串。
121
138
  */
122
139
  export async function npmPackumentReadme(pkg, timeoutMs = 20_000, signal, registry, deps) {
123
140
  if (!/^@?[A-Za-z0-9-._~]+(\/[A-Za-z0-9-._~]+)?$/.test(pkg))
@@ -130,7 +147,7 @@ export async function npmPackumentReadme(pkg, timeoutMs = 20_000, signal, regist
130
147
  maxBytes: 8 * 1024 * 1024,
131
148
  });
132
149
  const readme = typeof data?.readme === 'string' && data.readme.trim() !== '' ? data.readme : '';
133
- return { readme };
150
+ return { readme, repo: extractGithubRepo(data?.repository) };
134
151
  }
135
152
  /**
136
153
  * 升级探测(检测读)。L3 权威链(ADR-0012):新鲜度敏感——npmjs 权威源优先
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-m",
3
- "version": "0.9.46",
3
+ "version": "0.9.48",
4
4
  "description": "可自定义收录清单的 DSH 插件市场:收录、安装、卸载、升级一站完成,附 agent 工具与 dshm CLI。",
5
5
  "license": "MIT",
6
6
  "type": "module",