Skip to content
Merged
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
70 changes: 59 additions & 11 deletions dashboard/src/components/shared/ReadmeDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,24 @@ const loading = ref(false);
const isEmpty = ref(false);
const copyFeedbackTimer = ref(null);
const lastRequestId = ref(0);
const scrollContainer = ref(null);

function slugifyHeading(text, slugCounts) {
const base = (text || "")
.trim()
.toLowerCase()
.normalize("NFKD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/[^\p{Letter}\p{Number}\s-]/gu, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-");
Comment on lines +54 to +61
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The current slug generation logic can produce slugs with leading or trailing hyphens (e.g., from a heading like ## - My Title -). While these are valid in HTML5 IDs, it's common practice to trim them for cleaner and more predictable URLs/anchors. I suggest adding a step to remove any leading or trailing hyphens from the generated slug.

  const base = (text || "")
    .trim()
    .toLowerCase()
    .normalize("NFKD")
    .replace(/[\u0300-\u036f]/g, "")
    .replace(/[^\p{Letter}\p{Number}\s-]/gu, "")
    .replace(/\s+/g, "-")
    .replace(/-+/g, "-")
    .replace(/^-+|-+$/g, "");


if (!base) return "";

const count = slugCounts.get(base) || 0;
slugCounts.set(base, count + 1);
return count === 0 ? base : `${base}-${count}`;
}

onUnmounted(() => {
if (copyFeedbackTimer.value) clearTimeout(copyFeedbackTimer.value);
Expand Down Expand Up @@ -153,6 +171,18 @@ const renderedHtml = computed(() => {
// 3. 后处理方案:完全隔离,安全性最高
const tempDiv = document.createElement("div");
tempDiv.innerHTML = cleanHtml;

const slugCounts = new Map();
tempDiv.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((heading) => {
if (heading.id) {
slugCounts.set(heading.id, (slugCounts.get(heading.id) || 0) + 1);
return;
}

const slug = slugifyHeading(heading.textContent, slugCounts);
if (slug) heading.id = slug;
});

tempDiv.querySelectorAll("a").forEach((link) => {
const href = link.getAttribute("href");
// 强制所有外部链接使用安全的 _blank 策略
Expand Down Expand Up @@ -251,18 +281,35 @@ 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 anchor = event.target.closest('a[href^="#"]');
if (!anchor) return;

const rawHref = anchor.getAttribute("href");
const targetId = rawHref ? decodeURIComponent(rawHref.slice(1)) : "";
Comment on lines +302 to +303
Copy link
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): 在片段(fragment)上调用 decodeURIComponent 时,如果遇到格式错误的编码,会抛出异常,从而导致点击处理函数出错。

这里假设片段总是合法的百分号编码;但一旦出现无效的 % 序列,就会让 decodeURIComponent 抛出异常并中止处理函数。建议用 try/catch 做保护,并在出错时回退到原始子串:

let targetId = "";
if (rawHref) {
  try {
    targetId = decodeURIComponent(rawHref.slice(1));
  } catch {
    targetId = rawHref.slice(1);
  }
}
Original comment in English

issue (bug_risk): decodeURIComponent on the fragment can throw on malformed encodings, which would break the click handler.

This assumes the fragment is always valid percent-encoding; an invalid % sequence will cause decodeURIComponent to throw and stop the handler. Consider guarding with a try/catch and falling back to the raw substring:

let targetId = "";
if (rawHref) {
  try {
    targetId = decodeURIComponent(rawHref.slice(1));
  } catch {
    targetId = rawHref.slice(1);
  }
}

if (!targetId) return;

const target = scrollContainer.value?.querySelector(
`#${CSS.escape(targetId)}`,
);
if (!target) return;

event.preventDefault();
target.scrollIntoView({ behavior: "smooth", block: "start" });
}

function tryFallbackCopy(text, btn) {
Expand Down Expand Up @@ -326,7 +373,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="scrollContainer" 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 Expand Up @@ -436,6 +483,7 @@ const showActionArea = computed(() => {
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
scroll-margin-top: 12px;
}

:deep(.markdown-body h1) {
Expand Down