Skip to content

fix(dashboard): make plugin readme toc anchors work in dialog#5915

Open
Clhikari wants to merge 3 commits intoAstrBotDevs:masterfrom
Clhikari:fix/dashboard-readme-toc-anchor
Open

fix(dashboard): make plugin readme toc anchors work in dialog#5915
Clhikari wants to merge 3 commits intoAstrBotDevs:masterfrom
Clhikari:fix/dashboard-readme-toc-anchor

Conversation

@Clhikari
Copy link
Contributor

@Clhikari Clhikari commented Mar 8, 2026

Motivation / 动机

Fixes #5914
Plugin README/文档弹窗中的目录锚点点击无效,无法跳转到对应章节。
根因是:标题锚点生成不稳定 + Hash 路由场景下目录 #... 点击未在弹窗内处理滚动。


Modifications / 改动点

  • 使用 github-slugger 生成 Markdown 标题锚点 id,替换原先自定义正则 slug 逻辑。
  • ReadmeDialog 中处理文档内锚点点击:
    • 仅拦截 #...
    • 排除 #/...(避免干扰现有 hash 路由);
    • 在弹窗内容容器内平滑滚动到目标标题。
  • 保持原有行为不变:
    • 外链仍使用 _blank + noopener,noreferrer
    • 代码块复制按钮逻辑不变。
  • 新增前端依赖并更新锁文件。

核心变更文件:

  • dashboard/src/components/shared/ReadmeDialog.vue

  • dashboard/package.json

  • dashboard/pnpm-lock.yaml

  • This is NOT a breaking change. / 这不是一个破坏性变更。

Screenshots or Test Results / 运行截图或测试结果

output.mp4

Checklist / 检查清单

  • 😊 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。/ If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
  • 👀 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”。/ My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
  • 🤓 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到了 requirements.txtpyproject.toml 文件相应位置。/ I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in requirements.txt and pyproject.toml.
  • 😮 我的更改没有引入恶意代码。/ My changes do not introduce malicious code.

Summary by Sourcery

改进插件 README 对话框中的锚点处理,以在对话框内可靠地导航到 Markdown 标题。

Bug 修复:

  • 使插件 README 对话框中的基于哈希的链接可以滚动到对话框内对应的标题,而不是没有任何反应。

增强功能:

  • 在 README 对话框中使用 slug 生成工具创建稳定的 Markdown 标题 ID,以支持一致的页内锚点。

构建:

  • 向仪表盘前端添加 github-slugger 依赖,并相应更新锁定文件。
Original summary in English

Summary by Sourcery

Improve plugin README dialog anchor handling for reliable in-dialog navigation of markdown headings.

Bug Fixes:

  • Make hash-based links inside the plugin README dialog scroll to the corresponding heading within the dialog instead of doing nothing.

Enhancements:

  • Generate stable markdown heading IDs in the README dialog using a slugging utility to support consistent in-page anchors.

Build:

  • Add the github-slugger dependency to the dashboard frontend and update the lockfile accordingly.

Bug 修复:

  • 使插件 README 对话框中的基于哈希的链接可以滚动到对话框内对应的标题,而不是不起任何作用。

增强功能:

  • 在 README 对话框中使用 slugging 库生成稳定的 Markdown 标题 ID,以支持可靠的页面内锚点。

构建:

  • 为仪表盘前端添加 github-slugger 依赖,并相应更新锁定文件。
Original summary in English

Summary by Sourcery

改进插件 README 对话框中的锚点处理,以在对话框内可靠地导航到 Markdown 标题。

Bug 修复:

  • 使插件 README 对话框中的基于哈希的链接可以滚动到对话框内对应的标题,而不是没有任何反应。

增强功能:

  • 在 README 对话框中使用 slug 生成工具创建稳定的 Markdown 标题 ID,以支持一致的页内锚点。

构建:

  • 向仪表盘前端添加 github-slugger 依赖,并相应更新锁定文件。
Original summary in English

Summary by Sourcery

Improve plugin README dialog anchor handling for reliable in-dialog navigation of markdown headings.

Bug Fixes:

  • Make hash-based links inside the plugin README dialog scroll to the corresponding heading within the dialog instead of doing nothing.

Enhancements:

  • Generate stable markdown heading IDs in the README dialog using a slugging utility to support consistent in-page anchors.

Build:

  • Add the github-slugger dependency to the dashboard frontend and update the lockfile accordingly.

Bug 修复:

  • 确保插件 README 对话框中的基于 hash 的链接会滚动到对话框内对应的标题,而不是不起任何作用。

增强功能:

  • 在 README 对话框中使用 slugging 库生成稳定的 Markdown 标题 ID,以支持可靠的页内锚点。

构建:

  • github-slugger 依赖添加到仪表盘前端,并相应更新锁定文件(lockfile)。
Original summary in English

Summary by Sourcery

改进插件 README 对话框中的锚点处理,以在对话框内可靠地导航到 Markdown 标题。

Bug 修复:

  • 使插件 README 对话框中的基于哈希的链接可以滚动到对话框内对应的标题,而不是没有任何反应。

增强功能:

  • 在 README 对话框中使用 slug 生成工具创建稳定的 Markdown 标题 ID,以支持一致的页内锚点。

构建:

  • 向仪表盘前端添加 github-slugger 依赖,并相应更新锁定文件。
Original summary in English

Summary by Sourcery

Improve plugin README dialog anchor handling for reliable in-dialog navigation of markdown headings.

Bug Fixes:

  • Make hash-based links inside the plugin README dialog scroll to the corresponding heading within the dialog instead of doing nothing.

Enhancements:

  • Generate stable markdown heading IDs in the README dialog using a slugging utility to support consistent in-page anchors.

Build:

  • Add the github-slugger dependency to the dashboard frontend and update the lockfile accordingly.

Bug 修复:

  • 使插件 README 对话框中的基于哈希的链接可以滚动到对话框内对应的标题,而不是不起任何作用。

增强功能:

  • 在 README 对话框中使用 slugging 库生成稳定的 Markdown 标题 ID,以支持可靠的页面内锚点。

构建:

  • 为仪表盘前端添加 github-slugger 依赖,并相应更新锁定文件。
Original summary in English

Summary by Sourcery

改进插件 README 对话框中的锚点处理,以在对话框内可靠地导航到 Markdown 标题。

Bug 修复:

  • 使插件 README 对话框中的基于哈希的链接可以滚动到对话框内对应的标题,而不是没有任何反应。

增强功能:

  • 在 README 对话框中使用 slug 生成工具创建稳定的 Markdown 标题 ID,以支持一致的页内锚点。

构建:

  • 向仪表盘前端添加 github-slugger 依赖,并相应更新锁定文件。
Original summary in English

Summary by Sourcery

Improve plugin README dialog anchor handling for reliable in-dialog navigation of markdown headings.

Bug Fixes:

  • Make hash-based links inside the plugin README dialog scroll to the corresponding heading within the dialog instead of doing nothing.

Enhancements:

  • Generate stable markdown heading IDs in the README dialog using a slugging utility to support consistent in-page anchors.

Build:

  • Add the github-slugger dependency to the dashboard frontend and update the lockfile accordingly.

@dosubot dosubot bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Mar 8, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a critical usability issue in the dashboard where anchor links within plugin README dialogs were not working correctly. The changes ensure that users can now navigate seamlessly through the documentation by clicking on table of contents entries. This was achieved by standardizing the generation of heading IDs and implementing custom scrolling behavior for internal links within the dialog, significantly improving the user experience for plugin documentation.

Highlights

  • Plugin README Anchor Fix: Resolved an issue where table of contents (TOC) anchors in plugin README dialogs were not functional, preventing navigation to corresponding sections.
  • Stable Heading ID Generation: Implemented github-slugger to generate consistent and stable Markdown heading IDs, replacing the previous custom regex-based slugging logic.
  • In-Dialog Anchor Scrolling: Added logic within the ReadmeDialog component to intercept and handle internal hash anchors, enabling smooth scrolling to target headings within the dialog content.
  • Dependency Update: Introduced github-slugger as a new frontend dependency and updated the lock file accordingly.
Changelog
  • dashboard/package.json
    • Added github-slugger dependency with version ^2.0.0.
  • dashboard/pnpm-lock.yaml
    • Included github-slugger@2.0.0 with its resolution integrity hash.
    • Added github-slugger@2.0.0 to the snapshots section.
  • dashboard/src/components/shared/ReadmeDialog.vue
    • Imported GithubSlugger for generating heading slugs.
    • Introduced isInDialogAnchor function to check if a link is an internal dialog anchor.
    • Added scrollToHeadingInDialog function to handle smooth scrolling to target headings within the dialog.
    • Modified the Markdown renderer rules to use GithubSlugger for setting id attributes on heading elements.
    • Updated handleContainerClick to intercept clicks on internal hash anchors and trigger the scrollToHeadingInDialog function, while preserving existing code block copy functionality.
Activity
  • No human activity has been recorded on this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@dosubot dosubot bot added the feature:plugin The bug / feature is about AstrBot plugin system. label Mar 8, 2026
Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了 1 个问题,并给出了一些高层次的反馈:

  • heading_open 渲染规则会在每次 renderedHtml 重新计算时被重新创建;建议在初始化 md 时只注册一次该规则(或防止重复赋值),以避免重复的初始化操作,并避免将来其他代码自定义标题渲染时产生潜在冲突。
  • scrollToHeadingInDialog 中,依赖 closest('.v-card-text') 会将滚动行为与特定的 DOM 结构强耦合;使用一个专门的 ref 或更明确的容器元素作为滚动目标,可以让对话框在布局发生变化时更加稳健。
供 AI Agent 使用的提示
Please address the comments from this code review:

## Overall Comments
- The `heading_open` renderer rule is recreated on every `renderedHtml` recomputation; consider registering this rule once when `md` is initialized (or guarding against multiple assignments) to avoid redundant setup and potential conflicts if other code later customizes heading rendering.
- In `scrollToHeadingInDialog`, relying on `closest('.v-card-text')` couples the scroll behavior to a specific DOM structure; using a dedicated ref or a more explicit container element for the scroll target would make the dialog behavior more robust to layout changes.

## Individual Comments

### Comment 1
<location path="dashboard/src/components/shared/ReadmeDialog.vue" line_range="36-38" />
<code_context>
+  return href?.startsWith("#") && href.length > 1 && !href.startsWith("#/");
+}
+
+function scrollToHeadingInDialog(anchorId, markdownContainer) {
+  if (!anchorId || !markdownContainer) return;
+  const decodedId = decodeURIComponent(anchorId);
+  const escapedId =
+    typeof CSS !== "undefined" && CSS.escape
</code_context>
<issue_to_address>
**issue (bug_risk):** Guard `decodeURIComponent` to avoid runtime errors on malformed anchors.

If `anchorId` has invalid `%` encoding (e.g., from malformed links), `decodeURIComponent` will throw and break the handler. Consider wrapping it in a try/catch and falling back to the original value:

```js
let decodedId;
try {
  decodedId = decodeURIComponent(anchorId);
} catch {
  decodedId = anchorId;
}
```

This keeps the dialog working even with bad anchors.
</issue_to_address>

Sourcery 对开源项目免费——如果你喜欢我们的评审,请考虑帮我们分享 ✨
帮我变得更有用!请对每条评论点 👍 或 👎,我会根据你的反馈改进之后的评审。
Original comment in English

Hey - I've found 1 issue, and left some high level feedback:

  • The heading_open renderer rule is recreated on every renderedHtml recomputation; consider registering this rule once when md is initialized (or guarding against multiple assignments) to avoid redundant setup and potential conflicts if other code later customizes heading rendering.
  • In scrollToHeadingInDialog, relying on closest('.v-card-text') couples the scroll behavior to a specific DOM structure; using a dedicated ref or a more explicit container element for the scroll target would make the dialog behavior more robust to layout changes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `heading_open` renderer rule is recreated on every `renderedHtml` recomputation; consider registering this rule once when `md` is initialized (or guarding against multiple assignments) to avoid redundant setup and potential conflicts if other code later customizes heading rendering.
- In `scrollToHeadingInDialog`, relying on `closest('.v-card-text')` couples the scroll behavior to a specific DOM structure; using a dedicated ref or a more explicit container element for the scroll target would make the dialog behavior more robust to layout changes.

## Individual Comments

### Comment 1
<location path="dashboard/src/components/shared/ReadmeDialog.vue" line_range="36-38" />
<code_context>
+  return href?.startsWith("#") && href.length > 1 && !href.startsWith("#/");
+}
+
+function scrollToHeadingInDialog(anchorId, markdownContainer) {
+  if (!anchorId || !markdownContainer) return;
+  const decodedId = decodeURIComponent(anchorId);
+  const escapedId =
+    typeof CSS !== "undefined" && CSS.escape
</code_context>
<issue_to_address>
**issue (bug_risk):** Guard `decodeURIComponent` to avoid runtime errors on malformed anchors.

If `anchorId` has invalid `%` encoding (e.g., from malformed links), `decodeURIComponent` will throw and break the handler. Consider wrapping it in a try/catch and falling back to the original value:

```js
let decodedId;
try {
  decodedId = decodeURIComponent(anchorId);
} catch {
  decodedId = anchorId;
}
```

This keeps the dialog working even with bad anchors.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

本次 PR 修复了插件 README 弹窗中目录锚点无法跳转的问题,实现很好。核心改动包括:使用 github-slugger 保证锚点 ID 的稳定性,并增加了在弹窗内处理锚点点击和滚动的逻辑。

我提出了两点改进建议:

  1. 优化 GithubSlugger 实例和 markdown-it 渲染规则的初始化时机,避免在 computed 属性中重复创建,以提升性能和代码可维护性。
  2. 增强 CSS.escape 的回退实现,以兼容不支持此 API 的旧版浏览器,确保在所有环境下锚点都能正常工作。

整体来看,这是一个高质量的修复,解决了用户体验上的一个痛点。

Comment on lines +91 to +99
const slugger = new GithubSlugger();

md.renderer.rules.heading_open = (tokens, idx, options, _env, self) => {
const headingToken = tokens[idx + 1];
const headingText = headingToken?.content || "";
const headingId = slugger.slug(headingText);
tokens[idx].attrSet("id", headingId || slugger.slug("section"));
return self.renderToken(tokens, idx, options);
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

markdown-it 的渲染规则(md.renderer.rules.heading_open)和 GithubSlugger 实例在 computed 属性中被重复定义和创建。这不仅效率低下,而且不是 markdown-it 的正确使用方式。渲染规则应该在 markdown-it 实例初始化后定义一次。

建议进行如下重构:

  1. const slugger = new GithubSlugger();md.renderer.rules.heading_open = ... 的定义移到 <script setup> 的顶层作用域,与 md 实例的初始化放在一起。
  2. renderedHtml computed 属性的开头,调用 slugger.reset() 来为每次新的渲染重置其内部状态。

这将提高性能并使代码更具可维护性。

Comment on lines +39 to +42
const escapedId =
typeof CSS !== "undefined" && CSS.escape
? CSS.escape(decodedId)
: decodedId.replace(/["\\]/g, "\\$&");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

CSS.escape 的回退实现(fallback)不完整。当前的 decodedId.replace(/["\\]/g, "\\$&") 只处理了双引号和反斜杠,但 CSS 选择器中还有其他特殊字符(如 .:[] 等)需要转义。如果 github-slugger 生成的 ID 包含这些字符,在不支持 CSS.escape 的旧版浏览器中,querySelector 将会失败。

      ? CSS.escape(decodedId)
      : decodedId.replace(/[!"#$%&'()*+,./:;<=>?@[\]^`{|}~]/g, "\\$&");

@Clhikari
Copy link
Contributor Author

Clhikari commented Mar 8, 2026

@sourcery-ai review

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了 1 个问题

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="dashboard/src/components/shared/ReadmeDialog.vue" line_range="48" />
<code_context>
+
+function scrollToHeadingInDialog(anchorId, markdownContainer, scrollContainer) {
+  if (!anchorId || !markdownContainer) return;
+  const decodedId = decodeURIComponent(anchorId);
+  const escapedId =
+    typeof CSS !== "undefined" && CSS.escape
</code_context>
<issue_to_address>
**suggestion (bug_risk):**`decodeURIComponent` 增加针对不合法锚点值的防护,以避免运行时错误。

因为 href 来自任意的 README 内容,一个格式错误的百分号编码锚点(例如 `#foo%2`)可能会导致 `decodeURIComponent` 抛出异常,从而破坏对话框的点击处理逻辑。请将这个调用包裹在 try/catch 中,如果出错则要么跳过处理,要么回退到直接使用原始的 `anchorId`,以确保在遇到不良输入时仍能保持健壮的行为。
</issue_to_address>

Sourcery 对开源项目免费——如果你觉得我们的评审有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据你的反馈改进后续的评审。
Original comment in English

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="dashboard/src/components/shared/ReadmeDialog.vue" line_range="48" />
<code_context>
+
+function scrollToHeadingInDialog(anchorId, markdownContainer, scrollContainer) {
+  if (!anchorId || !markdownContainer) return;
+  const decodedId = decodeURIComponent(anchorId);
+  const escapedId =
+    typeof CSS !== "undefined" && CSS.escape
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Guard `decodeURIComponent` against malformed anchor values to avoid runtime errors.

Because the href comes from arbitrary README content, a malformed percent-encoded anchor (e.g. `#foo%2`) would cause `decodeURIComponent` to throw and break the dialog click handler. Please wrap this call in try/catch and either skip handling on error or fall back to using the raw `anchorId` to keep behavior resilient to bad input.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@Clhikari
Copy link
Contributor Author

Clhikari commented Mar 8, 2026

@sourcery-ai review

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我已经查看了你的更改,看起来很棒!


Sourcery 对开源项目是免费的——如果你觉得我们的代码审查有帮助,请考虑分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进之后的代码审查。
Original comment in English

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature:plugin The bug / feature is about AstrBot plugin system. size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] 插件说明文档目录点击无效(ReadmeDialog 内锚点未跳转)

1 participant