vitepress-allyouneed 0.5.1 → 0.5.2

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
@@ -2,6 +2,31 @@
2
2
 
3
3
  本项目遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/);版本号遵循 [SemVer](https://semver.org/lang/zh-CN/)。
4
4
 
5
+ ## [0.5.2] - 2026-06-20
6
+
7
+ 实战(90 页 DE+ZH 物理知识库,重 KaTeX / Wikilink / GFM 表格)发现的三个 build-blocker / 死链 bug,纯插件侧可修,无需改任何内容。
8
+
9
+ ### Fixed
10
+ - **致命崩溃(同 0.5.1 那类 `state.pos`,但这次出在 `#tag` inline rule):`vitepress build` 在含 `#tag` 的 link label / 嵌套方括号 / GFM 表格单元格上整站构建失败**,报 `inline rule didn't increment state.pos`。
11
+ - 根因:`modules/tags/rule.ts` 的 `#tag` inline rule 注册在 `link` 之前;markdown-it 的 link 核心规则用 `parseLinkLabel → skipToken` 以 **silent 模式**扫描 `[...]` label,本规则在 silent 下 `return true` 却**没推进 `state.pos`**,触发 markdown-it 的安全检查并经 `parseLinkLabel` 对未消费的 `[` 递归。0.5.1 已修了 wikilink rule 的同款问题,但 tag rule 漏改 —— 复现文件正是报告里的 `themen/Astronomie und Exobiologie.md`(`[[#Biomarker|…]]` 被表格 `|` 拆成 `[[#Biomarker`,其中 `#Biomarker` 命中 tag rule)。
12
+ - 修复:silent 模式下也把 `state.pos` 推过匹配长度再 `return true`,token 产出仍门控在 `!silent`(对齐 markdown-it 自带规则)。
13
+ - **页内跳转锚点 `[[#heading]]` 即使 slug 精确匹配也被判死链 / 失去 `href`**(报告:仅德语主题就 74 处)。
14
+ - 根因:自引用 wikilink(只有 `#anchor`、无文件部分)解析时 `target` 被切成空串 → `lookupEntry('')`/`resolveSimple('')` 返回空 → 整条 wikilink 误判 dead。
15
+ - 修复:`core/resolver.ts` 与 `core/scan-wikilinks.ts` 双路径统一处理 —— 有 heading 部分且无文件部分时解析到**当前文件本身**,后续 heading 匹配(exact slug,各 `anchorMatch` 模式通吃)正常命中;`scan-wikilinks` 不再把自引用锚点计入死链汇总。
16
+ - **缺失的 embed 资源(`![[foo.gif]]` / 音视频 / PDF 指向不存在文件)把整个 Vite/Rollup 构建硬中断**,报 `Rollup failed to resolve import "/foo.gif"`。
17
+ - 根因:`modules/embeds/image.ts` 与 `modules/embeds/media.ts` 在 asset 解析不到时回退成绝对路径 `/{basename}`,该 URL 被 VitePress 的 Vite 插件当模块 import,Rollup 解析不到即 throw。
18
+ - 修复:缺失资源不再产出会被 Vite 解析的 `src`,改为按 `deadLink` 策略告警(`silent`/`warn`/`error`,`error` 推 `index.warnings` 让构建以非零退出但不中断渲染)+ 渲染一个**不触发 Vite 解析**的占位 `<span class="ayn-embed ayn-embed--missing">`,与死链行为一致,绝不 throw。
19
+
20
+ ### Tests
21
+ - 新增 `tests/v052-bugfixes.test.ts`(Bug1 silent-pos / Bug2 自引用锚点 + scanWikilinks / Bug3 image+media 缺失占位 + `deadLink='error'` 告警)。
22
+ - 更新 `tests/render.test.ts`、`tests/v03-phase2.test.ts`:缺失 image/media 现在断言占位 `<span>` 而非旧的会崩 build 的 `<img>/<audio>/<iframe src="/…">`;media player 用例改指向 `fixtures/vault/media/` 下的真实占位资源。
23
+ - 真实 `vitepress build` 端到端验证(复现 Astronomie 嵌套链接 + 自引用锚点 + 缺失 gif/mp4/pdf)全部通过:无 `state.pos` 崩溃、零死链上报、缺失 embed 降级告警、`build complete` 退出码 0。
24
+
25
+ ### Files touched
26
+ - `src/modules/tags/rule.ts`(silent 推进 `state.pos`)
27
+ - `src/core/resolver.ts`、`src/core/scan-wikilinks.ts`(自引用锚点 → 当前文件)
28
+ - `src/modules/embeds/image.ts`、`src/modules/embeds/media.ts`(缺失资源 → 占位 + `deadLink` 策略,不 throw)
29
+
5
30
  ## [0.5.1] - 2026-06-20
6
31
 
7
32
  ### Fixed
package/README.md CHANGED
@@ -4,9 +4,15 @@
4
4
 
5
5
  Forked from [`actuallysomecat/markdown-it-wikilinks-plus`](https://github.com/actuallysomecat/markdown-it-wikilinks-plus) (MIT) 并针对 VitePress 大幅重写。
6
6
 
7
- ## 当前版本:0.5.1(2026-06-20)
7
+ ## 当前版本:0.5.2(2026-06-20)
8
8
 
9
- **主题集成大改 —— 目标:让第三方 VitePress 主题作者不需要知道本插件存在**
9
+ **实战 build-blocker 修复**(详见 [CHANGELOG](./CHANGELOG.md#052---2026-06-20)):
10
+
11
+ - `#tag` 出现在 link label / 嵌套方括号 / GFM 表格单元格时不再让 `vitepress build` 整站崩溃(`inline rule didn't increment state.pos`)。
12
+ - 页内自引用锚点 `[[#heading]]` 不再被误判死链 —— slug 精确匹配现在正常生成可点击 `href`。
13
+ - 缺失的 embed 资源(`![[foo.gif]]` / 音视频 / PDF)不再硬中断 Vite 构建,改为按 `deadLink` 策略告警 + 渲染占位标记。
14
+
15
+ ### 0.5.1:主题集成大改 —— 目标:让第三方 VitePress 主题作者不需要知道本插件存在
10
16
 
11
17
  ### CSS 自动让用户赢(`@layer`)
12
18
 
package/dist/index.cjs CHANGED
@@ -680,13 +680,11 @@ function resolveWikilink(rawTarget, index, options, kind = "page", currentSource
680
680
  const wasFolderForm = /\/$/.test(target);
681
681
  if (wasFolderForm) target = target.replace(/\/+$/, "");
682
682
  target = stripMarkdownExt(target);
683
- const entry = lookupEntry(
684
- target,
685
- index,
686
- options,
687
- currentSourcePath,
688
- wasFolderForm
689
- );
683
+ let selfEntry;
684
+ if (!target && headingPart && currentSourcePath) {
685
+ selfEntry = index.files.get(currentSourcePath);
686
+ }
687
+ const entry = selfEntry ?? lookupEntry(target, index, options, currentSourcePath, wasFolderForm);
690
688
  if (!entry) {
691
689
  return {
692
690
  url: buildDeadUrl(rawTarget, options),
@@ -1038,14 +1036,14 @@ function renderImageHtml(rawTarget, aliasParts, env) {
1038
1036
  const { altText, dim } = parseAltAndDim(aliasParts);
1039
1037
  const processedTarget = options.embeds.postProcessImageTarget(rawTarget);
1040
1038
  const { asset } = resolveAsset(processedTarget, index, options, env.currentPath);
1041
- let src;
1042
- if (asset) {
1043
- asset.referencedBy.add(env.currentPath ?? "<unknown>");
1044
- env.referencedAssets?.add(asset);
1045
- src = buildPlaceholderUrl(asset, options) + options.embeds.uriSuffix;
1046
- } else {
1047
- src = options.base + encodeURIComponent(basename(processedTarget)) + options.embeds.uriSuffix;
1039
+ if (!asset) {
1040
+ handleMissingAsset(env, processedTarget);
1041
+ return renderMissingImageHtml(processedTarget, options);
1048
1042
  }
1043
+ let src;
1044
+ asset.referencedBy.add(env.currentPath ?? "<unknown>");
1045
+ env.referencedAssets?.add(asset);
1046
+ src = buildPlaceholderUrl(asset, options) + options.embeds.uriSuffix;
1049
1047
  const finalAlt = determineAlt(altText, processedTarget, options);
1050
1048
  const attrs = {
1051
1049
  src,
@@ -1068,6 +1066,24 @@ function renderImageHtml(rawTarget, aliasParts, env) {
1068
1066
  }
1069
1067
  return "<img " + Object.entries(attrs).map(([k, v]) => `${escapeAttrName(k)}="${escapeHtml(v)}"`).join(" ") + " />";
1070
1068
  }
1069
+ function handleMissingAsset(env, target) {
1070
+ const { options } = env;
1071
+ if (options.deadLink === "silent") return;
1072
+ const msg = `vitepress-allyouneed: missing image embed ![[${target}]]` + (env.currentPath ? ` (in ${env.currentPath})` : "");
1073
+ if (options.deadLink === "warn") {
1074
+ console.warn(msg);
1075
+ return;
1076
+ }
1077
+ env.index.warnings.push({
1078
+ kind: "unknown",
1079
+ message: msg,
1080
+ affected: env.currentPath ? [env.currentPath] : []
1081
+ });
1082
+ }
1083
+ function renderMissingImageHtml(target, _options) {
1084
+ const bn = basename(target);
1085
+ return `<span class="ayn-embed ayn-embed--missing" data-missing-src="${escapeHtml(bn)}" title="Missing image: ${escapeHtml(target)}">\u26A0 ${escapeHtml(bn)}</span>`;
1086
+ }
1071
1087
  function handleImageEmbed(state, rawTarget, aliasParts, env) {
1072
1088
  const html = renderImageHtml(rawTarget, aliasParts, env);
1073
1089
  const token = state.push("html_inline", "", 0);
@@ -1292,14 +1308,42 @@ function resolveSrc(rawTarget, env) {
1292
1308
  env.referencedAssets?.add(asset);
1293
1309
  return buildPlaceholderUrl(asset, options) + options.embeds.uriSuffix;
1294
1310
  }
1295
- return options.base + encodeURIComponent(basename(processedTarget)) + options.embeds.uriSuffix;
1311
+ return null;
1312
+ }
1313
+ function handleMissingMedia(env, kind, rawTarget) {
1314
+ const { options } = env;
1315
+ const processedTarget = options.embeds.postProcessImageTarget(rawTarget);
1316
+ if (options.deadLink === "silent") return;
1317
+ const msg = `vitepress-allyouneed: missing ${kind} embed ![[${processedTarget}]]` + (env.currentPath ? ` (in ${env.currentPath})` : "");
1318
+ if (options.deadLink === "warn") {
1319
+ console.warn(msg);
1320
+ return;
1321
+ }
1322
+ env.index.warnings.push({
1323
+ kind: "unknown",
1324
+ message: msg,
1325
+ affected: env.currentPath ? [env.currentPath] : []
1326
+ });
1327
+ }
1328
+ function renderMissingMediaHtml(rawTarget, env) {
1329
+ const processedTarget = env.options.embeds.postProcessImageTarget(rawTarget);
1330
+ const bn = basename(processedTarget);
1331
+ return `<span class="ayn-embed ayn-embed--missing" data-missing-src="${escapeHtml(bn)}" title="Missing media: ${escapeHtml(processedTarget)}">\u26A0 ${escapeHtml(bn)}</span>`;
1296
1332
  }
1297
1333
  function renderAudioHtml(rawTarget, _aliasParts, env) {
1298
1334
  const src = resolveSrc(rawTarget, env);
1335
+ if (src === null) {
1336
+ handleMissingMedia(env, "audio", rawTarget);
1337
+ return renderMissingMediaHtml(rawTarget, env);
1338
+ }
1299
1339
  return `<audio class="ayn-embed ayn-embed--audio" controls preload="metadata" src="${escapeHtml(src)}">Your browser does not support the audio element.</audio>`;
1300
1340
  }
1301
1341
  function renderVideoHtml(rawTarget, aliasParts, env) {
1302
1342
  const src = resolveSrc(rawTarget, env);
1343
+ if (src === null) {
1344
+ handleMissingMedia(env, "video", rawTarget);
1345
+ return renderMissingMediaHtml(rawTarget, env);
1346
+ }
1303
1347
  const dim = parseAliasDim(aliasParts);
1304
1348
  const attrs = [
1305
1349
  `class="ayn-embed ayn-embed--video"`,
@@ -1313,6 +1357,10 @@ function renderVideoHtml(rawTarget, aliasParts, env) {
1313
1357
  }
1314
1358
  function renderPdfHtml(rawTarget, aliasParts, env) {
1315
1359
  const src = resolveSrc(rawTarget, env);
1360
+ if (src === null) {
1361
+ handleMissingMedia(env, "pdf", rawTarget);
1362
+ return renderMissingMediaHtml(rawTarget, env);
1363
+ }
1316
1364
  const dim = parseAliasDim(aliasParts);
1317
1365
  const width = dim.width !== void 0 ? `${dim.width}px` : "100%";
1318
1366
  const height = dim.height !== void 0 ? `${dim.height}px` : "600px";
@@ -2287,7 +2335,7 @@ function renderTemplate(v) {
2287
2335
  // src/core/views/generate-data.ts
2288
2336
  var import_node_fs6 = __toESM(require("fs"), 1);
2289
2337
  var import_node_path6 = __toESM(require("path"), 1);
2290
- var PLUGIN_VERSION = true ? "0.5.1" : "0.0.0-dev";
2338
+ var PLUGIN_VERSION = true ? "0.5.2" : "0.0.0-dev";
2291
2339
  var WIKILINK_RE = /(!?)\[\[([^\]\n]+)\]\]/g;
2292
2340
  var DEFAULT_BODY_TAG_RE = /(?:^|[\s([{,;。,;])#([\p{L}_][\p{L}\p{N}_/-]*)/gu;
2293
2341
  function buildBodyTagRe(userPattern) {
@@ -2646,7 +2694,10 @@ function makeTagRule() {
2646
2694
  const m = pattern.exec(slice);
2647
2695
  if (!m) return false;
2648
2696
  const tag = m[1];
2649
- if (silent) return true;
2697
+ if (silent) {
2698
+ state.pos = start + m[0].length;
2699
+ return true;
2700
+ }
2650
2701
  if (!env.referencedTags) env.referencedTags = /* @__PURE__ */ new Set();
2651
2702
  env.referencedTags.add(tag);
2652
2703
  const tagsViewName = env.options?.views?.names?.tags ?? "tags";
@@ -3792,7 +3843,8 @@ function scanWikilinks(index, options) {
3792
3843
  if (isAsset) continue;
3793
3844
  }
3794
3845
  }
3795
- const found = resolveSimple(rawTarget, index, options, f.relativePath);
3846
+ const isSelfAnchor = !rawTarget && !!headingPart;
3847
+ const found = isSelfAnchor || resolveSimple(rawTarget, index, options, f.relativePath);
3796
3848
  if (!found) {
3797
3849
  dead.push({
3798
3850
  source: f.relativePath,
@@ -3802,7 +3854,7 @@ function scanWikilinks(index, options) {
3802
3854
  continue;
3803
3855
  }
3804
3856
  if (scanAmbig && headingPart) {
3805
- const targetEntry = resolveSimpleEntry(rawTarget, index, options, f.relativePath);
3857
+ const targetEntry = isSelfAnchor ? f : resolveSimpleEntry(rawTarget, index, options, f.relativePath);
3806
3858
  if (!targetEntry) continue;
3807
3859
  const matches2 = findAmbiguousLeadingNumberMatches(targetEntry, headingPart);
3808
3860
  if (matches2.length > 1) {