Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"date-fns": "2.30.0",
"dompurify": "^3.3.1",
"event-source-polyfill": "^1.0.31",
"github-slugger": "^2.0.0",
"highlight.js": "^11.11.1",
"js-md5": "^0.8.3",
"katex": "^0.16.27",
Expand Down
8 changes: 8 additions & 0 deletions dashboard/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

88 changes: 76 additions & 12 deletions dashboard/src/components/shared/ReadmeDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import MarkdownIt from "markdown-it";
import hljs from "highlight.js";
import axios from "axios";
import DOMPurify from "dompurify";
import GithubSlugger from "github-slugger";
import "highlight.js/styles/github-dark.css";
import { useI18n } from "@/i18n/composables";

Expand All @@ -18,6 +19,16 @@ const md = new MarkdownIt({
md.enable(["table", "strikethrough"]);
md.renderer.rules.table_open = () => '<div class="table-container"><table>';
md.renderer.rules.table_close = () => "</table></div>";
md.renderer.rules.heading_open = (tokens, idx, options, env, self) => {
const headingToken = tokens[idx + 1];
const headingText = headingToken?.content || "";
const slugger = env?.slugger;
if (slugger && typeof slugger.slug === "function") {
const headingId = slugger.slug(headingText);
tokens[idx].attrSet("id", headingId || slugger.slug("section"));
}
return self.renderToken(tokens, idx, options);
};

// 2. 复制按钮的 SVG 图标常量
const ICONS = {
Expand All @@ -28,6 +39,41 @@ const ICONS = {
COPY: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>',
};

function isInDialogAnchor(href) {
return href?.startsWith("#") && href.length > 1 && !href.startsWith("#/");
}

function safeDecodeAnchorId(anchorId) {
try {
return decodeURIComponent(anchorId);
} catch {
return anchorId;
}
}

function scrollToHeadingInDialog(anchorId, markdownContainer, scrollContainer) {
if (!anchorId || !markdownContainer) return;
const decodedId = safeDecodeAnchorId(anchorId);
const escapedId =
typeof CSS !== "undefined" && CSS.escape
? CSS.escape(decodedId)
: decodedId.replace(/["\\]/g, "\\$&");
Comment on lines +57 to +60
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, "\\$&");

const target = markdownContainer.querySelector(`#${escapedId}`);
if (!target) return;

if (!scrollContainer) {
target.scrollIntoView({ behavior: "smooth", block: "start" });
return;
}

const offsetTop =
target.getBoundingClientRect().top - scrollContainer.getBoundingClientRect().top;
scrollContainer.scrollTo({
top: scrollContainer.scrollTop + offsetTop - 8,
behavior: "smooth",
});
}

const props = defineProps({
show: { type: Boolean, default: false },
pluginName: { type: String, default: "" },
Expand All @@ -46,6 +92,7 @@ const content = ref(null);
const error = ref(null);
const loading = ref(false);
const isEmpty = ref(false);
const contentContainer = ref(null);
const copyFeedbackTimer = ref(null);
const lastRequestId = ref(0);

Expand Down Expand Up @@ -79,7 +126,7 @@ const renderedHtml = computed(() => {
</div>`;
};

const rawHtml = md.render(content.value);
const rawHtml = md.render(content.value, { slugger: new GithubSlugger() });

const cleanHtml = DOMPurify.sanitize(rawHtml, {
ALLOWED_TAGS: [
Expand Down Expand Up @@ -251,17 +298,34 @@ watch(

function handleContainerClick(event) {
const btn = event.target.closest(".copy-code-btn");
if (!btn) return;
const code = btn.closest(".code-block-wrapper")?.querySelector("code");
if (code) {
if (navigator.clipboard?.writeText) {
navigator.clipboard
.writeText(code.textContent)
.then(() => showCopyFeedback(btn, true))
.catch(() => tryFallbackCopy(code.textContent, btn));
} else {
tryFallbackCopy(code.textContent, btn);
if (btn) {
const code = btn.closest(".code-block-wrapper")?.querySelector("code");
if (code) {
if (navigator.clipboard?.writeText) {
navigator.clipboard
.writeText(code.textContent)
.then(() => showCopyFeedback(btn, true))
.catch(() => tryFallbackCopy(code.textContent, btn));
} else {
tryFallbackCopy(code.textContent, btn);
}
}
return;
}

const link = event.target.closest("a");
if (!link) return;
const href = link.getAttribute("href")?.trim();
if (!isInDialogAnchor(href)) return;

event.preventDefault();
const markdownContainer = event.currentTarget;
if (markdownContainer) {
scrollToHeadingInDialog(
href.slice(1),
markdownContainer,
contentContainer.value,
);
}
}

Expand Down Expand Up @@ -326,7 +390,7 @@ const showActionArea = computed(() => {
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text style="overflow-y: auto">
<v-card-text ref="contentContainer" style="overflow-y: auto">
<div v-if="showActionArea" class="d-flex justify-space-between mb-4">
<v-btn
v-if="modeConfig.showGithubButton && repoUrl"
Expand Down