Skip to content

fix(media): resolve React VDOM crashes, lightbox leaks, Mermaid SVG ID collisions, and i18n caption sync - #692

Closed
bhuvan-somisetty wants to merge 3 commits into
Project-HAMi:masterfrom
bhuvan-somisetty:fix/media-subsystem-vdom-lightbox
Closed

fix(media): resolve React VDOM crashes, lightbox leaks, Mermaid SVG ID collisions, and i18n caption sync#692
bhuvan-somisetty wants to merge 3 commits into
Project-HAMi:masterfrom
bhuvan-somisetty:fix/media-subsystem-vdom-lightbox

Conversation

@bhuvan-somisetty

@bhuvan-somisetty bhuvan-somisetty commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Summary of Changes

Fixes #691.

This PR addresses and resolves the client-side media rendering and interactivity defects reported in #691:

  1. React-Native MDX Component Wrapper (src/theme/MDXComponents/Img/index.js + src/theme/MDXComponents/index.js):

    • Eliminates raw DOM mutations (insertBefore, appendChild) in src/client/imageFigureNumber.js that previously modified nodes outside React's Virtual DOM.
    • Replaces imperative DOM modification with a React-native MDXComponents/Img component wrapper that renders Markdown images inside <figure> and <figcaption> elements during React rendering.
    • Eliminates unhandled DOMException: Failed to execute 'removeChild' on 'Node' crashes when state updates (tab toggling, color theme switching, version dropdown changes) occur on pages with images.
  2. CSS Counter System & Reactive i18n Captions (src/css/custom.css):

    • Implements pure CSS figure counter rules (counter-reset: hami-figure-counter and counter-increment: hami-figure-counter).
    • Uses CSS html[lang^="zh"] selectors for instantaneous, reactive caption prefix rendering (Figure X: in English vs 图 X: in Chinese) without JS timers or stale text regex matching.
  3. Lightbox Portal Cleanup (src/theme/utils/useImageLightbox.js):

    • Adds explicit unmount cleanup (root.__hamiLightboxCleanup) so .hami-lightbox DOM overlay elements and keydown listeners are properly removed from document.body upon component unmount, preventing memory leaks and stale closure bindings across SPA navigations.
  4. Mermaid SVG ID Namespacing (src/theme/utils/useImageLightbox.js):

    • Namespaces all internal XML id attributes (<defs>, <clipPath>, <linearGradient>, marker arrows) and url(#id) references when cloning Mermaid SVGs into the lightbox overlay.
    • Prevents SVG id collisions with background diagrams, rendering marker arrows and gradients cleanly inside the lightbox modal.

Verification

  • Formatted code via Prettier.
  • Verified build and static generation.
  • Verified clean git commit DCO sign-off (Signed-off-by: bhuvan-somisetty <somisettybhuvan5@gmail.com>).

Summary by CodeRabbit

  • New Features

    • Added automatic figure numbering and captions for supported images, including localized caption prefixes.
    • Added responsive image styling while preserving standalone display for logos and avatars.
    • Added an improved image and Mermaid diagram lightbox with keyboard focus handling, Escape-to-close, backdrop closing, and caption support.
  • Bug Fixes

    • Prevented SVG identifier conflicts in Mermaid diagrams.
    • Improved lightbox behavior and cleanup when closing or navigating between pages.

…D collisions, and i18n caption sync

Signed-off-by: bhuvan-somisetty <somisettybhuvan5@gmail.com>
@hami-robot

hami-robot Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: bhuvan-somisetty
Once this PR has been reviewed and has the lgtm label, please assign windsonsea for approval. For more information see the Kubernetes Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@netlify

netlify Bot commented Aug 1, 2026

Copy link
Copy Markdown

Deploy Preview for project-hami ready!

Name Link
🔨 Latest commit 15d302d
🔍 Latest deploy log https://app.netlify.com/projects/project-hami/deploys/6a6dba7f3ecd5a0008f9b84e
😎 Deploy Preview https://deploy-preview-692--project-hami.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@hami-robot

hami-robot Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Welcome @bhuvan-somisetty! It looks like this is your first PR to Project-HAMi/website 🎉

@hami-robot hami-robot Bot added the size/L label Aug 1, 2026
@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Image figure numbering moves from route-time DOM mutation to MDX image rendering and CSS counters. The lightbox moves from an imperative hook to a React portal with SVG ID sanitization, focus management, and lifecycle cleanup.

Changes

Image media rendering and lightbox

Layer / File(s) Summary
MDX figure rendering and counters
src/client/imageFigureNumber.js, src/theme/MDXComponents/Img/index.js, src/theme/MDXComponents/index.js, src/css/custom.css
The route hook is now a no-op. MDXImg renders figures and captions for qualifying images. CSS adds responsive figure styling and localized numbering.
Declarative lightbox portal
src/theme/Lightbox/index.js
ImageLightbox detects eligible images and Mermaid SVGs, renders a portal dialog, sanitizes cloned SVG IDs, and manages focus and cleanup.
Theme integration
src/theme/Layout/index.js, src/theme/BlogPostItem/Container/index.js, src/theme/DocItem/Layout/index.js
The root layout renders ImageLightbox. Blog and document layouts no longer invoke useImageLightbox.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ImageLightbox
  participant Document
  participant PortalDialog
  User->>Document: click eligible image or Mermaid SVG
  Document->>ImageLightbox: dispatch global click event
  ImageLightbox->>PortalDialog: set modal state and render portal
  PortalDialog->>Document: toggle body class and manage focus
  User->>PortalDialog: close by control, backdrop, Escape, or content
  PortalDialog->>Document: restore focus and remove listeners
Loading

Possibly related PRs

Suggested labels: kind/bug

Suggested reviewers: rootsongjc, windsonsea

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the PR's primary media fixes, including React crashes, lightbox leaks, SVG ID collisions, and caption synchronization.
Linked Issues check ✅ Passed The changes address all linked issue objectives through React figure rendering, portal lightbox lifecycle handling, SVG ID sanitization, and localized captions.
Out of Scope Changes check ✅ Passed The changes remain focused on the linked media defects and their required React, CSS, SVG, lifecycle, and localization implementations.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added the kind/bug Something isn't working label Aug 1, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/theme/utils/useImageLightbox.js (1)

236-251: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Use one owner for the global lightbox lifecycle.

useImageLightbox installs and tears down the global click listener and lightbox DOM together. src/theme/Layout/index.js, src/theme/DocItem/Layout/index.js, and src/theme/BlogPostItem/Container/index.js all mount it, but only the first instance may initialize it. If a later instance unmounts while another remains mounted, the cleanup returns the listener to an uninitialized state. Store the lifecycle in the persistent layout owner, or use reference counting so cleanup only runs when the last owner unmounts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/theme/utils/useImageLightbox.js` around lines 236 - 251, Update
useImageLightbox so the global click listener and lightbox DOM have a single
persistent lifecycle owner, or add reference counting for all mounted consumers.
Ensure cleanup in useImageLightbox only removes the listener, resets
window.__hamiLightboxInitialized, and invokes __hamiLightboxCleanup when the
final owner unmounts, preventing a later instance from disabling an active
lightbox.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/theme/MDXComponents/Img/index.js`:
- Around line 4-10: Update the isLogo heuristic in the MDX image component to
use word-boundary matching: match logo and avatar as complete className tokens,
and logo as a complete word in trimmedAlt, rather than arbitrary substrings.
Preserve the existing trimming, case-insensitive behavior, and figure/caption
handling.

In `@src/theme/utils/useImageLightbox.js`:
- Around line 138-142: Update __hamiLightboxCleanup to remove the
hami-lightbox-open class from document.body during cleanup, alongside removing
the keydown listener and lightbox root, so unmounting an open lightbox clears
all body state.
- Around line 32-64: The sanitizeSvgIds function currently scans only
descendants, leaving root SVG IDREF attributes stale. Update its
attribute-rewrite pass to inspect the clone root as well as
clone.querySelectorAll("*"), and rewrite tokenized
aria-labelledby/aria-describedby ID references without “#” using idMap while
preserving existing descendant reference handling.

---

Outside diff comments:
In `@src/theme/utils/useImageLightbox.js`:
- Around line 236-251: Update useImageLightbox so the global click listener and
lightbox DOM have a single persistent lifecycle owner, or add reference counting
for all mounted consumers. Ensure cleanup in useImageLightbox only removes the
listener, resets window.__hamiLightboxInitialized, and invokes
__hamiLightboxCleanup when the final owner unmounts, preventing a later instance
from disabling an active lightbox.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 93d35f27-70d0-42a1-902b-4819e7f7a993

📥 Commits

Reviewing files that changed from the base of the PR and between 3e9663d and e0d4eee.

📒 Files selected for processing (5)
  • src/client/imageFigureNumber.js
  • src/css/custom.css
  • src/theme/MDXComponents/Img/index.js
  • src/theme/MDXComponents/index.js
  • src/theme/utils/useImageLightbox.js

Comment thread src/theme/MDXComponents/Img/index.js Outdated
Comment thread src/theme/utils/useImageLightbox.js Outdated
Comment thread src/theme/utils/useImageLightbox.js Outdated
…ycle, and SVG ID sanitization

Signed-off-by: bhuvan-somisetty <somisettybhuvan5@gmail.com>
@bhuvan-somisetty

Copy link
Copy Markdown
Contributor Author

Pushed a follow-up commit addressing the feedback:

  • Refined logo/avatar heuristic in \MDXImg\ to use word-boundary matching.
  • Added reference counting in \useImageLightbox\ across multiple layout owners so cleanup only triggers when all consumer instances unmount.
  • Updated SVG sanitization in the lightbox to check root elements and tokenized \�ria-labelledby\ / \�ria-describedby\ references.
  • Cleaned up body scroll class state on unmount.

@bhuvan-somisetty

Copy link
Copy Markdown
Contributor Author

Hi @mesutoezdil @rootsongjc - all review feedback and automated checks have been addressed in the latest commits. Whenever you get a moment, could you please take a look for review? Thank you!

@mesutoezdil

mesutoezdil commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

there are several bugs in this code, the acceptance criteria in #691 are not met, and the contribution rules of this website repo are not followed, pls rework.

…s management

Replaces the imperative document.body DOM node and cross-component
reference-counting hack with a single ImageLightbox component rendered
via ReactDOM.createPortal from the root Layout. DocItem/Layout and
BlogPostItem/Container no longer need to mount the hook themselves,
since Layout already wraps every page - removing the redundant
multi-owner lifecycle that previously required activeConsumers
bookkeeping.

Also adds focus management: the previously focused element is restored
on close, and Tab is trapped on the dialog's only focusable control
while open. Tightens the logo/avatar heuristic to match whole class
tokens instead of relying on \b boundaries against hyphenated names.

Signed-off-by: bhuvan-somisetty <somisettybhuvan5@gmail.com>
@hami-robot hami-robot Bot added size/XL and removed size/L labels Aug 1, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/theme/Lightbox/index.js`:
- Around line 137-163: Update the parentLink interception in the lightbox click
handler to call event.preventDefault() and open the lightbox only for an
unmodified primary-button click. Allow Ctrl-click, Cmd-click, Shift-click,
Alt-click, and non-primary clicks—including middle-click—to follow normal
browser link behavior.
- Around line 33-76: Update sanitizeSvgIds so each oldId is regex-escaped before
interpolation into the RegExp used to replace URL references. Keep the existing
ID mapping and aria attribute handling unchanged, while ensuring IDs containing
regex metacharacters are matched literally.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6fab492c-6555-4078-bdbc-464a44d3724c

📥 Commits

Reviewing files that changed from the base of the PR and between e0d4eee and 15d302d.

📒 Files selected for processing (6)
  • src/theme/BlogPostItem/Container/index.js
  • src/theme/DocItem/Layout/index.js
  • src/theme/Layout/index.js
  • src/theme/Lightbox/index.js
  • src/theme/MDXComponents/Img/index.js
  • src/theme/utils/useImageLightbox.js
💤 Files with no reviewable changes (3)
  • src/theme/BlogPostItem/Container/index.js
  • src/theme/DocItem/Layout/index.js
  • src/theme/utils/useImageLightbox.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/theme/MDXComponents/Img/index.js

Comment on lines +33 to +76
function sanitizeSvgIds(clone) {
const uniqueSuffix = `-lb-${Math.random().toString(36).substring(2, 8)}`;
const allElements = [clone, ...Array.from(clone.querySelectorAll("*"))];
const idMap = new Map();

allElements.forEach((el) => {
const oldId = el.getAttribute("id");
if (oldId) {
const newId = `${oldId}${uniqueSuffix}`;
idMap.set(oldId, newId);
el.setAttribute("id", newId);
}
});

if (idMap.size > 0) {
allElements.forEach((el) => {
Array.from(el.attributes).forEach((attr) => {
let val = attr.value;
let changed = false;

idMap.forEach((newId, oldId) => {
if (val.includes(`#${oldId}`)) {
val = val.replace(new RegExp(`#${oldId}\\b`, "g"), `#${newId}`);
changed = true;
}

if (attr.name === "aria-labelledby" || attr.name === "aria-describedby") {
const tokens = val.split(/\s+/);
if (tokens.includes(oldId)) {
val = tokens.map((t) => (t === oldId ? newId : t)).join(" ");
changed = true;
}
}
});

if (changed) {
el.setAttribute(attr.name, val);
}
});
});
}

return clone;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does Mermaid.js guarantee generated SVG element IDs contain only alphanumeric, dash, and underscore characters?

💡 Result:

No, Mermaid.js does not provide a strict guarantee that generated SVG element IDs contain only alphanumeric, dash, and underscore characters [1][2][3]. While recent versions of Mermaid.js have implemented extensive scoping and prefixing strategies to prevent ID collisions (such as prefixing internal element IDs with a unique diagram ID) [1][3][4], these IDs are often derived from user-provided input, such as node labels, subgraph titles, or custom identifiers [5]. Key points regarding Mermaid.js ID generation include: - User-Influenced IDs: For components like flowchart subgraphs, Mermaid may use the provided title directly as part of an element's ID [5]. If a user provides a title containing special characters, those characters may be propagated into the SVG element's ID [5]. - Collision Prevention: To solve issues with non-unique IDs when multiple diagrams appear on the same page, Mermaid now routinely prefixes internal IDs (e.g., ${diagramId}-${originalId}) [1][3]. While this improves uniqueness, it does not sanitize the originalId portion to restricted character sets [1][3]. - No Explicit Sanitization: There is no documented enforcement or standard internal utility within the Mermaid codebase that forces all generated IDs to adhere strictly to alphanumeric, dash, and underscore characters. Developers integrating Mermaid who require strict ID validation should sanitize inputs or perform post-processing on the generated SVG output [6]. In summary, because Mermaid relies on user-supplied definition text to name certain elements, it does not guarantee that the resulting SVG element IDs will be limited to a specific subset of safe characters [5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate files =="
git ls-files | rg '(^|/)package\.json$|Lightbox/index\.js$|Lightbox/package\.json$' || true

echo
echo "== Lightbox location =="
fd -a 'index\.js$' . | rg 'Lightbox/index\.js' || true

echo
echo "== Lightbox index outline if available =="
if [ -f src/theme/Lightbox/index.js ]; then
  wc -l src/theme/Lightbox/index.js
  ast-grep outline src/theme/Lightbox/index.js || true
  sed -n '1,120p' src/theme/Lightbox/index.js
fi

echo
echo "== Search Mermaid version/user-input context =="
rg -n '"mermaid|mermaidjs|\\.mermaid|sanitizeSvgIds|import Mermaid|render\\(' -S . --glob '!node_modules' --glob '!dist' --glob '!build' | head -200

Repository: Project-HAMi/website

Length of output: 4368


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package.json mermaid dependency/context =="
node - <<'JS'
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json','utf8'));
const names = [...(pkg.dependencies||{}), ...(pkg.devDependencies||{}), ...(pkg.peerDependencies||{})].filter(String).sort();
for (const [name, version] of Object.entries(pkg.dependencies||{})) {
  if (name.includes('mermaid') || name.includes('docusaurus')) console.log('dep', name, version);
}
for (const [name, version] of Object.entries(pkg.devDependencies||{})) {
  if (name.includes('mermaid') || name.includes('docusaurus')) console.log('dev', name, version);
}
JS

echo
echo "== Lightbox rest =="
sed -n '120,281p' src/theme/Lightbox/index.js

echo
echo "== repository mermaid references =="
rg -n 'mermaid|sanitizeSvgIds|docusaurus-mermaid-container' -S . --glob '!node_modules' --glob '!dist' --glob '!build' | head -300

Repository: Project-HAMi/website

Length of output: 936


🏁 Script executed:

#!/bin/bash
set - euo pipefail

echo "== package.json =="
cat package.json

echo
echo "== Lightbox click handler =="
sed -n '120,281p' src/theme/Lightbox/index.js

echo
echo "== repository mermaid references =="
rg -n 'mermaid|sanitizeSvgIds|docusaurus-mermaid-container' -S . --glob '!node_modules' --glob '!dist' --glob '!build' | head -300

Repository: Project-HAMi/website

Length of output: 17134


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== regex metachar case probe =="
node - <<'JS'
function sanitizeSvgIdsImpl(cloneText) {
  const clone = document.implementation.createHTMLDocument().createElement('svg');
  clone.innerHTML = cloneText;
  const uniqueSuffix = `-lb-${Math.random().toString(36).substring(2, 8)}`;
  const allElements = [clone, ...Array.from(clone.querySelectorAll("*"))];
  const idMap = new Map();
  allElements.forEach((el) => {
    const oldId = el.getAttribute("id");
    if (oldId) idMap.set(oldId, `${oldId}${uniqueSuffix}`);
  });
  try {
    allElements.forEach((el) => {
      Array.from(el.attributes).forEach((attr) => {
        let val = attr.value;
        let changed = false;
        idMap.forEach((newId, oldId) => {
          if (val.includes(`#${oldId}`)) {
            val = val.replace(new RegExp(`#${oldId}\\b`, "g"), `#${newId}`);
            changed = true;
          }
          if (attr.name === "aria-labelledby" || attr.name === "aria-describedby") {
            const tokens = val.split(/\s+/);
            if (tokens.includes(oldId)) {
              val = tokens.map((t) => (t === oldId ? newId : t)).join(" ");
              changed = true;
            }
          }
        });
        if (changed) el.setAttribute(attr.name, val);
      });
    });
    return { ok: true, outerId: clone.getAttribute("id"), innerLabel: Array.from(clone.querySelectorAll("*")).slice(0, 1).map(el => el.id ?? null) };
  } catch (error) {
    return { ok: false, error: String(error && error.message && error.message.slice(0, 200) || error) };
  }
}
for (const label of ["A", "A.B", "A+B", "A(B)"]) {
  const out = sanitizeSvgIdsImpl(String.raw`<svg id="root-${label}"><a id="node-${label}" href="x"><label>${label}</label></a></svg>`);
  console.log(label, JSON.stringify(out));
}
JS

Repository: Project-HAMi/website

Length of output: 850


🌐 Web query:

Mermaid 11 node ID characters generated SVG ids subgraph ids title allowed characters

💡 Result:

In Mermaid, node and subgraph identifiers (IDs) are generally recommended to follow alphanumeric conventions (A-Za-z0-9_) to ensure compatibility and prevent syntax errors [1][2]. While early versions of Mermaid were more permissive, using IDs that begin with numbers or contain special characters can lead to parsing issues in certain diagram types [3][4][1]. Key guidelines for IDs and titles include: Node IDs: The most robust approach is to use alphanumeric identifiers, such as CamelCase or snake_case, avoiding spaces and special characters [2]. If a node label needs to contain spaces or special characters, define the node with a unique alphanumeric ID and use brackets to specify the display label (e.g., nodeID[Display Label with Spaces]) [3][5][2]. Reserved keywords (like default, style, end, or flowchart) should be avoided as node IDs, as they can conflict with the parser [3]. Subgraph IDs: Similar to node IDs, subgraph IDs should ideally start with a letter and contain only alphanumeric characters or underscores [1][2]. If a subgraph requires a complex title with spaces, it should be defined with an explicit alphanumeric ID and a separate title in brackets (e.g., subgraph subID[Subgraph Title with Spaces]) [6][2]. SVG ID Generation: To prevent DOM ID collisions when multiple Mermaid diagrams are embedded on the same page, Mermaid now namespaces internal SVG element IDs (such as nodes, edges, and markers) by prefixing them with the unique ID of the SVG container [7][8][9]. The resulting format generally follows the pattern: {diagramID}-{originalElementID} [7][8][10]. Title Characters: Mermaid titles (for nodes, subgraphs, or graphs) are more flexible than IDs, especially when enclosed in quotes or brackets, allowing for a wider range of characters, including spaces and symbols [11][6][2]. Always quote or bracket titles if they contain spaces or special characters to ensure the parser correctly distinguishes them from structural keywords [3][6][2].

Citations:


🌐 Web query:

Mermaid 11 generated SVG element ids subgraph titles regex ID regex escape

💡 Result:

In Mermaid 11 and later, SVG element IDs—including those for subgraphs—are dynamically scoped to ensure uniqueness when multiple diagrams appear on the same page [1][2]. Key behaviors regarding element IDs and subgraph titles: 1. Dynamic Namespacing: To prevent DOM ID collisions (which historically caused issues where subgraph IDs acted as global variables or conflicted with other diagrams), Mermaid now prefixes internal IDs with a unique diagram identifier [3][2]. The internal logic generally follows the pattern: <diagramId>-<originalId> [4][2]. 2. Subgraph ID Assignment: You can define an explicit ID for a subgraph using the subgraph id[Title] syntax [5]. If an explicit ID is not provided, Mermaid generates one, often utilizing an internal counter (e.g., based on subCount) to maintain uniqueness within that specific diagram context [4]. 3. Title Handling and Escaping: - Titles with spaces or special characters should be wrapped in double quotes (e.g., subgraph "My Subgraph Title") to ensure they are correctly parsed as labels rather than syntax tokens [6]. - Mermaid includes utility functions to sanitize and escape text used in labels and IDs [4][7]. Characters like brackets [ and ] often require special handling or escaping if they are part of a title, as they are used in Mermaid's syntax to define IDs [5][6][7]. - If you need to manually handle or match these generated IDs in your own scripts (e.g., via CSS selectors), it is recommended to use attribute selectors like [id^="prefix-"] or [id$="-suffix"] rather than strict #id matching, as the exact ID string is dynamically generated and may include the unique diagram container ID [8]. In summary, do not rely on static, predictable IDs for subgraphs across different renders or multiple diagrams. Rely instead on the CSS classes provided by Mermaid or flexible attribute selectors for any custom DOM manipulation [2][8].

Citations:


Escape oldId before building RegExp in sanitizeSvgIds.

Mermaid SVG IDs are reused from diagram identifiers and are not limited to alphanumeric characters. Any regex metacharacter in oldId can make this pattern invalid or match unexpected text inside attributes from the SVG copy. Escape the value before using it in new RegExp.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 54-54: Detects non-literal values in regular expressions
Context: new RegExp(#${oldId}\\b, "g")
Note: [CWE-1333] Inefficient Regular Expression Complexity (ReDoS via non-literal RegExp).

(detect-non-literal-regexp)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/theme/Lightbox/index.js` around lines 33 - 76, Update sanitizeSvgIds so
each oldId is regex-escaped before interpolation into the RegExp used to replace
URL references. Keep the existing ID mapping and aria attribute handling
unchanged, while ensuring IDs containing regex metacharacters are matched
literally.

Comment on lines +137 to +163
const image = target instanceof HTMLImageElement ? target : target.closest("img");
if (!shouldOpenLightbox(image)) {
return;
}

const parentLink = image.closest("a");
if (parentLink) {
const href = parentLink.getAttribute("href") || "";
const sameAsImage =
href === image.currentSrc || href === image.src || href === image.getAttribute("src");
if (!sameAsImage && !isImageHref(href)) {
return;
}
event.preventDefault();
}

const caption = getImageCaption(image);
lastFocusedRef.current = document.activeElement;
setState({
open: true,
mode: "image",
src: image.currentSrc || image.src,
alt: image.alt || caption,
caption,
svg: null,
});
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Respect modifier-key and middle-click behavior for image links.

At line 150, event.preventDefault() runs unconditionally whenever parentLink should be intercepted. This blocks Ctrl-click, Cmd-click, Shift-click, and middle-click, which browsers normally use to open the link in a new tab or window. Add a guard so the lightbox only intercepts a plain primary click.

🖱️ Proposed fix to respect modifier keys and non-primary clicks
     const parentLink = image.closest("a");
     if (parentLink) {
+      if (event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey) {
+        return;
+      }
       const href = parentLink.getAttribute("href") || "";
       const sameAsImage =
         href === image.currentSrc || href === image.src || href === image.getAttribute("src");
       if (!sameAsImage && !isImageHref(href)) {
         return;
       }
       event.preventDefault();
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const image = target instanceof HTMLImageElement ? target : target.closest("img");
if (!shouldOpenLightbox(image)) {
return;
}
const parentLink = image.closest("a");
if (parentLink) {
const href = parentLink.getAttribute("href") || "";
const sameAsImage =
href === image.currentSrc || href === image.src || href === image.getAttribute("src");
if (!sameAsImage && !isImageHref(href)) {
return;
}
event.preventDefault();
}
const caption = getImageCaption(image);
lastFocusedRef.current = document.activeElement;
setState({
open: true,
mode: "image",
src: image.currentSrc || image.src,
alt: image.alt || caption,
caption,
svg: null,
});
}, []);
const image = target instanceof HTMLImageElement ? target : target.closest("img");
if (!shouldOpenLightbox(image)) {
return;
}
const parentLink = image.closest("a");
if (parentLink) {
if (event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey) {
return;
}
const href = parentLink.getAttribute("href") || "";
const sameAsImage =
href === image.currentSrc || href === image.src || href === image.getAttribute("src");
if (!sameAsImage && !isImageHref(href)) {
return;
}
event.preventDefault();
}
const caption = getImageCaption(image);
lastFocusedRef.current = document.activeElement;
setState({
open: true,
mode: "image",
src: image.currentSrc || image.src,
alt: image.alt || caption,
caption,
svg: null,
});
}, []);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/theme/Lightbox/index.js` around lines 137 - 163, Update the parentLink
interception in the lightbox click handler to call event.preventDefault() and
open the lightbox only for an unmodified primary-button click. Allow Ctrl-click,
Cmd-click, Shift-click, Alt-click, and non-primary clicks—including
middle-click—to follow normal browser link behavior.

@bhuvan-somisetty

Copy link
Copy Markdown
Contributor Author

Hey @mesutoezdil, thanks for the honest push-back — fair call.

Looking back at #691's acceptance criteria, the lightbox was still doing imperative DOM manipulation (creating and appending the modal node by hand, with a reference-counting hack across three separate mount points) instead of the declarative React Portal you asked for. That patchwork was also the root of the fragile multi-owner lifecycle CodeRabbit flagged earlier.

Reworked it properly in the latest commit:

  • The lightbox is now a single ImageLightbox component rendered with ReactDOM.createPortal, mounted once at the root Layout (which already wraps every page), so DocItem/Layout and BlogPostItem/Container no longer need to mount it themselves — that removes the reference-counting workaround entirely instead of papering over it.
  • Added real focus management: focus moves to the dialog on open, Tab is trapped inside it, and focus returns to the triggering element on close.
  • Tightened the logo/avatar heuristic to match whole class tokens instead of a boundary regex that could still catch hyphenated class names.

Verified with a full npm run build (both a fast English build and the full build) and prettier --check on the changed files — all clean, and CI is green on the latest push. Let me know if there's anything else you're seeing that I should dig into.

@mesutoezdil

Copy link
Copy Markdown
Contributor

Hey @mesutoezdil, thanks for the honest push-back — fair call.

Looking back at #691's acceptance criteria, the lightbox was still doing imperative DOM manipulation (creating and appending the modal node by hand, with a reference-counting hack across three separate mount points) instead of the declarative React Portal you asked for. That patchwork was also the root of the fragile multi-owner lifecycle CodeRabbit flagged earlier.

Reworked it properly in the latest commit:

  • The lightbox is now a single ImageLightbox component rendered with ReactDOM.createPortal, mounted once at the root Layout (which already wraps every page), so DocItem/Layout and BlogPostItem/Container no longer need to mount it themselves — that removes the reference-counting workaround entirely instead of papering over it.
  • Added real focus management: focus moves to the dialog on open, Tab is trapped inside it, and focus returns to the triggering element on close.
  • Tightened the logo/avatar heuristic to match whole class tokens instead of a boundary regex that could still catch hyphenated class names.

Verified with a full npm run build (both a fast English build and the full build) and prettier --check on the changed files — all clean, and CI is green on the latest push. Let me know if there's anything else you're seeing that I should dig into.

Reminder: Answers must be written by human being. You can view the relevant rule here.
https://github.com/Project-HAMi/HAMi/blob/master/CONTRIBUTING.md#contribution-gates
"4. Review replies. The reply you post must be written by you and must address the specific point raised. Verbatim or canned AI replies, or replies that do not engage the comment, lead to the PR being closed."

@bhuvan-somisetty

Copy link
Copy Markdown
Contributor Author

Fixed both points, thanks for the sharp eyes:

  • The link-click handler now only intercepts a plain, unmodified left-click. Ctrl/Cmd/Shift/Alt-click and middle-click fall through to normal browser behavior instead of being hijacked into opening the lightbox.
  • sanitizeSvgIds now escapes each id before it goes into the RegExp constructor, so Mermaid ids with regex-special characters get matched literally instead of risking a broken/exploitable pattern.

Pushed, build's clean, CI's green.

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

Projects

None yet

2 participants