diff --git a/pages/index.html b/pages/index.html index c01e8c3..757f6d7 100644 --- a/pages/index.html +++ b/pages/index.html @@ -3,7 +3,7 @@ - Open Code Review — AI Code Review + Open Code Review — Agent Native Code Review diff --git a/pages/package.json b/pages/package.json index c2ce253..66acb5e 100644 --- a/pages/package.json +++ b/pages/package.json @@ -9,6 +9,9 @@ }, "dependencies": { "@agentscope-ai/icons": "^1.0.68", + "dompurify": "^3.4.11", + "marked": "^18.0.5", + "mermaid": "^11.16.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.8.0", @@ -22,6 +25,7 @@ "@babel/preset-env": "^7.29.5", "@babel/preset-react": "^7.23.5", "@babel/preset-typescript": "^7.23.3", + "@types/dompurify": "^3.0.5", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", "@types/three": "^0.185.0", diff --git a/pages/src/App.tsx b/pages/src/App.tsx index 73de908..ffe893e 100644 --- a/pages/src/App.tsx +++ b/pages/src/App.tsx @@ -22,7 +22,7 @@ const App: React.FC = () => { } /> } /> } /> - } /> + } /> ); diff --git a/pages/src/assets/icons/icon-search.svg b/pages/src/assets/icons/icon-search.svg new file mode 100644 index 0000000..3e5f898 --- /dev/null +++ b/pages/src/assets/icons/icon-search.svg @@ -0,0 +1 @@ + diff --git a/pages/src/components/MarkdownRenderer.tsx b/pages/src/components/MarkdownRenderer.tsx new file mode 100644 index 0000000..c3d6899 --- /dev/null +++ b/pages/src/components/MarkdownRenderer.tsx @@ -0,0 +1,187 @@ +import React, { useMemo, useEffect, useRef, useState, useCallback, useId } from 'react'; +import ReactDOM from 'react-dom'; +import { Marked, Renderer } from 'marked'; +import DOMPurify from 'dompurify'; +import mermaid from 'mermaid'; +import { useTranslation } from '../i18n'; +import copyIcon from '../assets/icons/icon-copy.svg'; +import { generateHeadingId } from '../utils/headingId'; + +// Initialize mermaid with dark theme +mermaid.initialize({ + startOnLoad: false, + theme: 'dark', + themeVariables: { + primaryColor: '#1a1a2e', + primaryTextColor: 'rgba(255,255,255,0.85)', + primaryBorderColor: 'rgba(255,255,255,0.2)', + lineColor: 'rgba(255,255,255,0.4)', + secondaryColor: '#16213e', + tertiaryColor: '#0f3460', + background: '#000000', + mainBkg: 'rgba(255,255,255,0.04)', + nodeBorder: 'rgba(255,255,255,0.16)', + clusterBkg: 'rgba(255,255,255,0.02)', + titleColor: '#FFFFFF', + edgeLabelBackground: '#000000', + }, + flowchart: { + htmlLabels: true, + curve: 'basis', + }, +}); + +interface MarkdownRendererProps { + content: string; +} + +/** + * Renders markdown content with dark theme styling matching the existing DocsPage design. + * Uses `marked` to parse markdown into HTML, then renders with styled container. + * Mermaid diagrams are rendered client-side after mount. + */ +const MarkdownRenderer: React.FC = ({ content }) => { + const containerRef = useRef(null); + const { t } = useTranslation(); + const [toastVisible, setToastVisible] = useState(false); + + const handleCopy = useCallback((text: string) => { + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(text).then(() => { + setToastVisible(true); + }).catch(() => fallbackCopy(text)); + } else { + fallbackCopy(text); + } + }, []); + + const fallbackCopy = (text: string) => { + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + const success = document.execCommand('copy'); + document.body.removeChild(textarea); + if (success) setToastVisible(true); + }; + + useEffect(() => { + if (!toastVisible) return; + const timer = setTimeout(() => setToastVisible(false), 1200); + return () => clearTimeout(timer); + }, [toastVisible]); + + const html = useMemo(() => { + // Custom renderer to generate heading IDs matching the TOC extraction logic + const renderer = new Renderer(); + renderer.heading = function ({ text, depth }: { text: string; depth: number }) { + const id = generateHeadingId(text); + // Escape id attribute value to prevent XSS + const safeId = id.replace(/"/g, '"'); + return `${text}\n`; + }; + // Strip trailing newlines from code blocks to avoid empty line at bottom + renderer.code = function ({ text, lang, escaped }: { text: string; lang?: string; escaped?: boolean }) { + const trimmed = text.replace(/^\n+|\n+$/g, ''); + const content = escaped ? trimmed : trimmed.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); + const langClass = lang ? ` class="language-${lang}"` : ''; + const isMultiline = trimmed.includes('\n'); + const alignItems = isMultiline ? 'flex-start' : 'center'; + return `
${content}
\n`; + }; + const instance = new Marked({ gfm: true, breaks: false, renderer }); + return DOMPurify.sanitize(instance.parse(content) as string); + }, [content]); + + // Render mermaid diagrams and add copy buttons to code blocks after DOM update + useEffect(() => { + if (!containerRef.current) return; + let cancelled = false; + + // Add copy buttons to all pre > code blocks (except mermaid) + const preBlocks = containerRef.current.querySelectorAll('pre'); + preBlocks.forEach((pre) => { + const codeEl = pre.querySelector('code'); + if (!codeEl || codeEl.classList.contains('language-mermaid')) return; + if (pre.querySelector('.code-copy-btn')) return; // already added + + // Create copy button matching reference HTML + const btn = document.createElement('div'); + btn.className = 'code-copy-btn'; + btn.style.cssText = 'display:flex;flex-shrink:0;justify-content:flex-start;align-items:flex-start;flex-direction:column;padding-top:4px;padding-bottom:4px;cursor:pointer;'; + btn.innerHTML = `copy`; + btn.addEventListener('click', () => { + const text = codeEl.textContent || ''; + handleCopy(text); + }); + pre.appendChild(btn); + }); + + // Render mermaid diagrams + const mermaidBlocks = containerRef.current.querySelectorAll('code.language-mermaid'); + if (mermaidBlocks.length === 0) return; + + const renderPromises = Array.from(mermaidBlocks).map(async (block) => { + const pre = block.parentElement; + if (!pre) return; + const code = block.textContent || ''; + try { + const id = `mermaid-diagram-${crypto.randomUUID()}`; + const { svg } = await mermaid.render(id, code); + if (cancelled) return; + // Replace the
 with rendered SVG
+        const wrapper = document.createElement('div');
+        wrapper.className = 'mermaid-rendered';
+        wrapper.innerHTML = svg;
+        pre.replaceWith(wrapper);
+      } catch (e) {
+        if (cancelled) return;
+        // If rendering fails, show the code block normally
+        (block as HTMLElement).style.display = 'block';
+        console.warn('[Mermaid] render failed:', e);
+      }
+    });
+
+    return () => { cancelled = true; };
+  }, [html]);
+
+  return (
+    <>
+      
+ {ReactDOM.createPortal( +
+ {t('quickstart.copied')} +
, + document.body + )} + + ); +}; + +export default MarkdownRenderer; diff --git a/pages/src/components/Navbar.tsx b/pages/src/components/Navbar.tsx index 4488d75..76ac72e 100644 --- a/pages/src/components/Navbar.tsx +++ b/pages/src/components/Navbar.tsx @@ -187,7 +187,7 @@ const Navbar: React.FC = () => { rel="noopener noreferrer" style={{ display: 'flex', alignItems: 'center', opacity: 0.6 }} > - Social + Social - {/* ─── Overview ─── */} -
-

{t('docs.overviewTitle')}

-

- Open Code Review {t('docs.overviewDesc').replace(/<\/?code>/g, '')} -

-

- {t('docs.overviewFeatures')} -

-
- - {'✔\n✔\n✔\n✔\n✔\n✔'.split('\n').map((c, i) => {c}
)} -
- - {t('docs.overviewFeat1')}
- {t('docs.overviewFeat2')}
- {t('docs.overviewFeat3')}
- {t('docs.overviewFeat4')}
- {t('docs.overviewFeat5')}
- {t('docs.overviewFeat6')} -
-
-
- - {/* ─── Install ─── */} -
-

{t('docs.installTitle')}

- {/* Install item */} -
-
- - {t('docs.installLabel')} + {sidebarTree.map((group, gi) => ( +
+ {/* Group header */} +
+ + {t(group.groupLabelKey)} +
- handleCopy('npm i -g @alibaba-group/open-code-review')} /> -
- {/* Verify item */} -
-
- - {t('docs.installVerifyLabel')} -
- handleCopy('ocr version')} /> -
-
- - {/* ─── Configuration & Verification ─── */} -
-

{t('docs.configTitle')}

-

{t('docs.configDesc').replace(/<\/?code>/g, '')}

- -

{t('docs.configInteractive')}

-

{t('docs.configInteractiveDesc')}

- handleCopy('ocr config provider')} /> - -

{t('docs.configModelSelect')}

-

{t('docs.configModelSelectDesc')}

- handleCopy('ocr config model')} /> - -

{t('docs.configListProviders')}

-

{t('docs.configListProvidersDesc')}

- handleCopy('ocr llm providers')} /> - -

{t('docs.configManual')}

-

{t('docs.configCommand')}

- '} /> - -

{t('docs.configExample')}

- handleCopy(`ocr config set llm.url https://api.anthropic.com \\\n && ocr config set llm.auth_token {{your-api-key}} \\\n && ocr config set llm.model claude-opus-4-6 \\\n && ocr config set llm.use_anthropic true \\\n && ocr config set language Chinese`)} - /> - -

{t('docs.configKeys')}

-
- {/* 2-column grid of config keys */} - {[ - [{ key: 'llm.url', desc: t('docs.configKeyUrl') }, { key: 'llm.auth_token', desc: t('docs.configKeyToken') }], - [{ key: 'llm.model', desc: t('docs.configKeyModel') }, { key: 'llm.use_anthropic', desc: t('docs.configKeyAnthropic') }], - [{ key: 'telemetry.enabled', desc: t('docs.configKeyTelemetry') }, { key: 'language', desc: t('docs.configKeyLanguage') }], - [{ key: 'llm.extra_body', desc: t('docs.configKeyExtraBody') }], - ].map((row, ri) => ( -
- {row.map(({ key, desc: d }) => ( -
-

- {key} - {d} -

+ {/* Group items */} + {group.items.map((item) => { + const isActive = item.slug === activeSlug; + const hasChildren = item.children && item.children.length > 0; + const isExpanded = expandedItems[item.id] ?? false; + return ( + +
{ + navigateToDoc(item.slug); + }} + style={{ + height: 36, + display: 'flex', + alignItems: 'center', + gap: 8, + borderRadius: 6, + padding: '10px 12px', + cursor: 'pointer', + transition: 'background 0.15s', + background: isActive ? 'rgba(43, 222, 94, 0.12)' : 'transparent', + }} + > +
+ + {t(item.labelKey)} + +
+ {hasChildren && ( +
{ + e.stopPropagation(); + toggleExpand(item.id); + }} + style={{ display: 'flex', alignItems: 'center', padding: 2 }} + > + +
+ )}
- ))} -
- ))} + {/* Children (sub-items) */} + {hasChildren && isExpanded && item.children!.map((child) => { + const childActive = child.slug === activeSlug; + return ( +
navigateToDoc(child.slug)} + style={{ + height: 36, + display: 'flex', + alignItems: 'center', + gap: 8, + borderRadius: 6, + padding: '10px 12px 10px 28px', + cursor: 'pointer', + background: childActive ? 'rgba(43, 222, 94, 0.12)' : 'transparent', + }} + > +
+ + {t(child.labelKey)} + +
+
+ ); + })} + + ); + })}
+ ))} + + )} -

{t('docs.configVerify')}

- handleCopy('ocr llm test')} - /> -

{t('docs.configVerifyDesc')}

-
+ {/* ─── Main content area ─── */} +
+ {/* Doc title */} +

+ {docTitle} +

+ {/* Rendered markdown content */} + - {/* ─── ocr review ─── */} -
-

{t('docs.reviewTitle')}

-

{t('docs.reviewDesc').replace(/<\/?code>/g, '')}

- -

{t('docs.reviewModes')}

- {/* Workspace Mode */} -
-
- -
- {t('docs.reviewWorkspace')} -

{t('docs.reviewWorkspaceDesc')}

-
-
- handleCopy('ocr review')} /> -
- {/* Branch Diff Mode */} -
-
- -
- {t('docs.reviewBranch')} -

{t('docs.reviewBranchDesc')}

-
-
- handleCopy('ocr review --from master --to dev-ref')} /> -
- {/* Single Commit Mode */} -
-
- -
- {t('docs.reviewCommit')} -

{t('docs.reviewCommitDesc')}

-
-
- handleCopy('ocr review -c abc123')} /> -
- -

{t('docs.reviewAdvanced')}

- {/* Review with Requirement Context */} -
-
- -
- {t('docs.reviewBackground')} -

{t('docs.reviewBackgroundDesc')}

-
-
- handleCopy('ocr review --background "requirement context"')} /> -
- {/* JSON Output */} -
-
- -
- {t('docs.reviewJson')} -

{t('docs.reviewJsonDesc')}

-
-
- handleCopy('ocr review --format json')} /> -
- {/* Agent Mode */} -
-
- -
- {t('docs.reviewAgent')} -

{t('docs.reviewAgentDesc')}

-
-
- handleCopy('ocr review --audience agent')} /> -
- {/* Dry-Run Preview */} -
-
- -
- {t('docs.reviewPreviewLabel')} -

{t('docs.reviewPreviewDesc')}

-
-
- handleCopy('ocr review --preview')} /> -
- -

{t('docs.reviewFlags')}

- {/* Flags table */} -
- {/* Header */} -
-
{t('docs.reviewFlagCol1')}
-
{t('docs.reviewFlagCol2')}
-
{t('docs.reviewFlagCol3')}
-
- {/* Rows */} - {[ - ['-c, --commit', t('docs.reviewFlag1Desc'), '—'], - ['--from', t('docs.reviewFlag2Desc'), '—'], - ['--to', t('docs.reviewFlag3Desc'), '—'], - ['-f, --format', t('docs.reviewFlag4Desc'), 'text'], - ['--repo', t('docs.reviewFlag5Desc'), t('docs.reviewFlag5Default')], - ['--rule', t('docs.reviewFlag6Desc'), t('docs.reviewFlag6Default')], - ['--concurrency', t('docs.reviewFlag7Desc'), '8'], - ['--timeout', t('docs.reviewFlag8Desc'), '10'], - ['--audience', t('docs.reviewFlag9Desc'), 'human'], - ['--max-tools', t('docs.reviewFlag10Desc'), t('docs.reviewFlag10Default')], - ].map(([flag, d, def], idx, arr) => ( -
-
- {flag} -
-
- {d} -
-
- {def} -
-
- ))} -
-

- {t('docs.reviewNote').replace(/<\/?code>/g, '')} -

-
- - {/* ─── ocr scan ─── */} -
-

{t('docs.scanTitle')}

-

{t('docs.scanDesc').replace(/<\/?code>/g, '')}

- -

{t('docs.scanVsTitle')}

-
-
- -
- {t('docs.scanVsReviewLabel')} -

{t('docs.scanVsReview').replace(/<\/?code>/g, '')}

-
-
-
-
-
- -
- {t('docs.scanVsScanLabel')} -

{t('docs.scanVsScan').replace(/<\/?code>/g, '')}

-
-
-
- -

{t('docs.scanUsage')}

-
-
- -
- {t('docs.scanUsageWhole')} -

{t('docs.scanUsageWholeDesc')}

-
-
- handleCopy('ocr scan')} /> -
-
-
- -
- {t('docs.scanUsagePath')} -

{t('docs.scanUsagePathDesc')}

-
-
- handleCopy('ocr scan --path internal/agent')} /> -
-
-
- -
- {t('docs.scanUsageFile')} -

{t('docs.scanUsageFileDesc')}

-
-
- handleCopy('ocr scan --path internal/agent/agent.go,internal/diff/scan.go')} /> -
-
-
- -
- {t('docs.scanUsagePreviewLabel')} -

{t('docs.scanUsagePreviewDesc')}

-
-
- handleCopy('ocr scan --preview')} /> -
- -

{t('docs.scanBatching')}

-

{t('docs.scanBatchingDesc').replace(/<\/?code>/g, '')}

-
- {[ - [t('docs.scanBatchingNone'), t('docs.scanBatchingNoneDesc')], - [t('docs.scanBatchingLang'), t('docs.scanBatchingLangDesc')], - [t('docs.scanBatchingDir'), t('docs.scanBatchingDirDesc')], - ].map(([name, d]) => ( -
-

- {name} - {d} -

-
- ))} -
- handleCopy('ocr scan --batch by-directory')} /> - -

{t('docs.scanToggles')}

-

{t('docs.scanTogglesDesc')}

-
- {[ - ['--no-plan', t('docs.scanTogglesPlanDesc')], - ['--no-dedup', t('docs.scanTogglesDedupDesc')], - ['--no-summary', t('docs.scanTogglesSummaryDesc')], - ].map(([flag, d]) => ( -
- {flag} - {d} -
- ))} -
- handleCopy('ocr scan --no-plan --no-dedup --no-summary')} /> - -

{t('docs.scanBudget')}

-

{t('docs.scanBudgetDesc').replace(/<\/?code>/g, '')}

- handleCopy('ocr scan --max-tokens-budget 500000')} /> - -

{t('docs.scanFlags')}

-
-
-
{t('docs.scanFlagCol1')}
-
{t('docs.scanFlagCol2')}
-
{t('docs.scanFlagCol3')}
-
- {[ - ['--path', t('docs.scanFlag1Desc'), t('docs.scanFlag1Default')], - ['--exclude', t('docs.scanFlag2Desc'), '—'], - ['-p, --preview', t('docs.scanFlag3Desc'), 'false'], - ['--max-tokens-budget', t('docs.scanFlag4Desc'), '0'], - ['--no-plan', t('docs.scanFlag5Desc'), 'false'], - ['--no-dedup', t('docs.scanFlag6Desc'), 'false'], - ['--no-summary', t('docs.scanFlag7Desc'), 'false'], - ['--batch', t('docs.scanFlag8Desc'), 'by-language'], - ['-f, --format', t('docs.scanFlag9Desc'), 'text'], - ['--concurrency', t('docs.scanFlag10Desc'), '8'], - ['--timeout', t('docs.scanFlag11Desc'), '10'], - ['--audience', t('docs.scanFlag12Desc'), 'human'], - ['-b, --background', t('docs.scanFlag13Desc'), '—'], - ['--max-tools', t('docs.scanFlag14Desc'), t('docs.scanFlag14Default')], - ['--max-git-procs', t('docs.scanFlag15Desc'), '16'], - ['--rule', t('docs.scanFlag16Desc'), '—'], - ['--tools', t('docs.scanFlag17Desc'), t('docs.scanFlag17Default')], - ['--repo', t('docs.scanFlag18Desc'), t('docs.scanFlag18Default')], - ].map(([flag, d, def], idx, arr) => ( -
-
- {flag} -
-
- {d} -
-
- {def} -
-
- ))} -
-

- {t('docs.scanNote').replace(/<\/?code>/g, '')} -

-
- - {/* ─── Viewer ─── */} -
-

{t('docs.viewerTitle')}

-

{t('docs.viewerDesc')}

- handleCopy('ocr viewer')} /> -

{t('docs.viewerNote')}

-
- - {/* ─── MCP Server ─── */} -
-

{t('docs.mcpTitle')}

-

{t('docs.mcpDesc')}

- -

{t('docs.mcpConfig')}

- handleCopy(mcpAddCommands)} /> -

{t('docs.mcpConfigLocation')}

- -

{t('docs.mcpDelete')}

- handleCopy(mcpDeleteCommands)} /> - -

{t('docs.mcpFields')}

-
-
-
{t('docs.mcpFieldCol')}
-
{t('docs.mcpRequiredCol')}
-
{t('docs.mcpDescCol')}
-
- {[ - ['command', t('docs.mcpYes'), t('docs.mcpFieldCommandDesc')], - ['args', t('docs.mcpNo'), t('docs.mcpFieldArgsDesc')], - ['tools', t('docs.mcpNo'), t('docs.mcpFieldToolsDesc')], - ['setup', t('docs.mcpNo'), t('docs.mcpFieldSetupDesc')], - ['env', t('docs.mcpNo'), t('docs.mcpFieldEnvDesc')], - ].map(([field, required, d], idx, arr) => ( -
-
- {field} -
-
- {required} -
-
- {d} -
-
- ))} -
- -

- {t('docs.mcpNote')} -

- -

{t('docs.mcpExample')}

- handleCopy(mcpCodeGraphCommands)} /> -
- - {/* ─── Claude Code Integration ─── */} -
-

{t('docs.envTitle')}

-

- {t('docs.envDesc').replace(/<\/?code>/g, '')} -

- handleCopy('export ANTHROPIC_BASE_URL=https://api.anthropic.com\nexport ANTHROPIC_AUTH_TOKEN=sk-ant-xxxxx\nexport ANTHROPIC_MODEL=claude-opus-4-6')} - /> -

- {t('docs.envNote').replace(/<\/?code>/g, '')} -

-
+ {/* ─── Prev / Next pagination ─── */} +
+ {prevDoc ? ( + + ) : } + {nextDoc ? ( + + ) : }
- {/* ─── Right sidebar: CONTENTS (fixed) ─── */} - {!isMobile && ( -
+ {/* ─── Right sidebar: page TOC ─── */} + {!isMobile && headings.length > 0 && ( +
- - {t('docs.toc')} + + + {t('docs.toc')} +
- {sections.map((s) => ( - - ))} + {headings.map((h, i) => { + const isActive = h.id === activeHeadingId; + const isHovered = h.id === hoveredHeadingId; + return ( + + ); + })}
)}
- + + {/* Search Modal */} + {searchOpen && ( +
setSearchOpen(false)} + > +
e.stopPropagation()} + > + {/* Search input */} +
+ + setSearchQuery(e.target.value)} + onKeyDown={handleSearchKeyDown} + placeholder={t('docs.search.placeholder')} + style={{ + flex: 1, + marginLeft: 12, + background: 'transparent', + border: 'none', + outline: 'none', + color: '#ffffff', + fontSize: 14, + fontFamily, + }} + /> +
+ {/* Results */} +
+ {searchQuery && searchResults.length === 0 && ( +
+ {t('docs.search.noResults')} +
+ )} + {searchResults.map((result, idx) => ( + + ))} +
+ {/* Footer hints */} + {searchResults.length > 0 && ( +
+
+ + + + {t('docs.search.hint.select')} + + + + {t('docs.search.hint.open')} + +
+ + esc + {t('docs.search.hint.close')} + +
+ )} +
+
+ )}
); }; diff --git a/pages/src/styles/docs-markdown.css b/pages/src/styles/docs-markdown.css new file mode 100644 index 0000000..6a8968e --- /dev/null +++ b/pages/src/styles/docs-markdown.css @@ -0,0 +1,242 @@ +/* Docs Markdown dark theme styles — matching DocsPage design */ +.docs-markdown { + font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Segoe UI', Roboto, sans-serif; + font-size: 14px; + line-height: 24px; + color: rgba(255, 255, 255, 0.7); +} + +.docs-markdown h1 { + font-size: 28px; + font-weight: 700; + color: #FFFFFF; + margin: 0 0 24px 0; + line-height: 36px; +} + +.docs-markdown h2 { + font-size: 20px; + font-weight: 600; + color: #FFFFFF; + margin: 40px 0 16px 0; + line-height: 28px; +} + +.docs-markdown h3 { + font-size: 16px; + font-weight: 600; + color: #FFFFFF; + margin: 28px 0 12px 0; + line-height: 24px; +} + +.docs-markdown h4 { + font-size: 14px; + font-weight: 600; + color: rgba(255, 255, 255, 0.9); + margin: 20px 0 8px 0; + line-height: 22px; +} + +.docs-markdown p { + margin: 0 0 12px 0; + color: rgba(255, 255, 255, 0.6); + line-height: 24px; +} + +.docs-markdown a { + color: #2BDE5E; + text-decoration: none; + transition: opacity 0.2s; +} + +.docs-markdown a:hover { + opacity: 0.8; + text-decoration: underline; +} + +/* Inline code */ +.docs-markdown code { + font-family: 'Menlo', 'Monaco', 'Consolas', monospace; + font-size: 13px; + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: 4px; + padding: 2px 6px; + color: rgba(255, 255, 255, 0.85); +} + +/* Code blocks */ +.docs-markdown pre { + background: #000000; + border: 1px solid rgba(255, 255, 255, 0.16); + border-radius: 6px; + padding: 4px 16px; + margin: 0 0 16px 0; + overflow-x: auto; + display: flex; + align-self: stretch; + justify-content: space-between; + align-items: center; +} + +.docs-markdown pre code { + background: none; + border: none; + padding: 0; + font-size: 13px; + line-height: 22px; + color: rgba(255, 255, 255, 0.8); + white-space: pre; + display: block; + flex: 1; + min-width: 0; +} + +/* Lists */ +.docs-markdown ul, +.docs-markdown ol { + margin: 0 0 16px 0; + padding-left: 24px; +} + +.docs-markdown li { + margin-bottom: 6px; + color: rgba(255, 255, 255, 0.6); + line-height: 24px; +} + +.docs-markdown li > ul, +.docs-markdown li > ol { + margin-top: 6px; + margin-bottom: 0; +} + +/* Tables */ +.docs-markdown table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + margin: 0 0 16px 0; + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.16); +} + +.docs-markdown thead th { + background: rgba(255, 255, 255, 0.04); + font-size: 13px; + font-weight: 500; + color: rgba(255, 255, 255, 0.6); + text-align: left; + padding: 12px 16px; + border-bottom: 1px solid rgba(255, 255, 255, 0.16); +} + +.docs-markdown thead th:first-child { + border-top-left-radius: 8px; +} + +.docs-markdown thead th:last-child { + border-top-right-radius: 8px; +} + +.docs-markdown tbody td { + font-size: 13px; + color: rgba(255, 255, 255, 0.6); + padding: 12px 16px; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + vertical-align: top; +} + +.docs-markdown tbody tr:last-child td { + border-bottom: none; +} + +.docs-markdown tbody tr:last-child td:first-child { + border-bottom-left-radius: 8px; +} + +.docs-markdown tbody tr:last-child td:last-child { + border-bottom-right-radius: 8px; +} + +/* Blockquotes */ +.docs-markdown blockquote { + margin: 0 0 16px 0; + padding: 12px 16px; + border-left: 2px solid #2BDE5E; + background: rgba(255, 255, 255, 0.06); + border-radius: 0 6px 6px 0; +} + +.docs-markdown blockquote p { + margin: 0; + color: rgba(255, 255, 255, 0.6); + font-size: 13px; +} + +.docs-markdown blockquote p + p { + margin-top: 8px; +} + +/* Horizontal rule */ +.docs-markdown hr { + border: none; + border-top: 1px solid rgba(255, 255, 255, 0.1); + margin: 32px 0; +} + +/* Strong / bold */ +.docs-markdown strong { + color: rgba(255, 255, 255, 0.9); + font-weight: 600; +} + +/* Em / italic */ +.docs-markdown em { + font-style: italic; + color: rgba(255, 255, 255, 0.7); +} + +/* Images */ +.docs-markdown img { + max-width: 100%; + border-radius: 8px; + margin: 8px 0; +} + +/* Mermaid code blocks — hide raw source (will be replaced by rendered SVG) */ +.docs-markdown pre code.language-mermaid { + display: none; +} + +/* Rendered mermaid diagrams */ +.docs-markdown .mermaid-rendered { + margin: 16px 0; + padding: 24px 16px; + background: rgba(255, 255, 255, 0.02); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + overflow-x: auto; + display: flex; + justify-content: center; +} + +.docs-markdown .mermaid-rendered svg { + max-width: 100%; + height: auto; +} + +/* Scrollbar styling for code blocks */ +.docs-markdown pre::-webkit-scrollbar { + height: 6px; +} + +.docs-markdown pre::-webkit-scrollbar-track { + background: transparent; +} + +.docs-markdown pre::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.15); + border-radius: 3px; +} diff --git a/pages/src/utils/headingId.ts b/pages/src/utils/headingId.ts new file mode 100644 index 0000000..0a87854 --- /dev/null +++ b/pages/src/utils/headingId.ts @@ -0,0 +1,10 @@ +/** + * Shared utility to generate heading IDs from text. + * Used by both extractHeadings (DocsPage TOC) and MarkdownRenderer (heading renderer) + * to ensure consistent anchor IDs. + */ +export function generateHeadingId(text: string): string { + // Strip HTML tags first (from marked output), then strip markdown formatting chars + const plain = text.replace(/<[^>]+>/g, '').replace(/[`*_\[\]()]/g, '').trim(); + return plain.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-').replace(/^-|-$/g, ''); +} diff --git a/pages/webpack.config.js b/pages/webpack.config.js index 55816da..2228fb2 100644 --- a/pages/webpack.config.js +++ b/pages/webpack.config.js @@ -36,6 +36,10 @@ module.exports = { { test: /\.(png|jpg|jpeg|gif)$/, type: 'asset/resource' + }, + { + test: /\.md$/, + type: 'asset/source' } ] },