feat(markdown): add MarkdownRenderer with code/Mermaid/CSV/Survey/HtmlPreview blocks#233
Open
rajkumargaramie wants to merge 2 commits into
Open
feat(markdown): add MarkdownRenderer with code/Mermaid/CSV/Survey/HtmlPreview blocks#233rajkumargaramie wants to merge 2 commits into
rajkumargaramie wants to merge 2 commits into
Conversation
…lPreview blocks
Adds a self-contained Markdown rendering component with:
- marked + DOMPurify pipeline
- highlight.js syntax highlighting (eager: js/ts/py/json/xml/css/bash/sql/yaml/md;
lazy: java/cpp/c/csharp/php/ruby/go/rust/perl/r/makefile/dockerfile)
- Copy-to-clipboard button on fenced code blocks
- React-portal blocks for mermaid diagrams, CSV tables, survey previews, and
sandboxed HTML previews
- atom-one-light / atom-one-dark theme bundled and scoped via [data-theme]
Exports:
- '@mieweb/ui' — { MarkdownRenderer, FenceBlock, MermaidBlock, CsvBlock,
SurveyBlock, HtmlPreviewBlock, useMarkdown }
- '@mieweb/ui/components/Markdown' — same, as tree-shakeable subpath
- '@mieweb/ui/markdown.css' — bundled hljs themes + fence block styles
mermaid, papaparse, and js-yaml are optional peer dependencies so consumers
that don't use those block types don't pay the bundle cost.
Deploying ui with
|
| Latest commit: |
ffb15f2
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://2a107ea6.ui-6d0.pages.dev |
| Branch Preview URL: | https://markdown-components.ui-6d0.pages.dev |
There was a problem hiding this comment.
Pull request overview
Adds a new Markdown rendering subsystem to @mieweb/ui that converts markdown to sanitized HTML, highlights code blocks, and mounts React-powered “special fence blocks” (Mermaid/CSV/Survey/HTML preview) via portals—plus a separate exported stylesheet (@mieweb/ui/markdown.css) for highlight.js + fence styling.
Changes:
- Introduces
MarkdownRenderer+useMarkdownpipeline using marked + DOMPurify + highlight.js, including placeholder emission for special fenced blocks. - Adds block components (
FenceBlock,MermaidBlock,CsvBlock,SurveyBlock,HtmlPreviewBlock) and bundled highlight/fence CSS. - Updates build/package exports and dependency config to ship a tree-shakeable Markdown subpath and a dedicated
markdown.cssexport.
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| tsup.config.ts | Adds a tree-shakeable Markdown entry point and marks optional block deps as externals. |
| src/index.ts | Re-exports Markdown components from the package root. |
| src/components/Markdown/useMarkdown.ts | Implements marked→sanitized HTML rendering, highlight.js integration, caching, placeholder emission, and delegated copy handler. |
| src/components/Markdown/MarkdownRenderer.tsx | Renders sanitized HTML and mounts block components into placeholders via portals. |
| src/components/Markdown/FenceBlock.tsx | Provides a reusable UI wrapper for fenced blocks (copy + raw/rendered toggle + error state). |
| src/components/Markdown/MermaidBlock.tsx | Adds Mermaid diagram rendering (dynamic import) into FenceBlock. |
| src/components/Markdown/CsvBlock.tsx | Adds CSV parsing + sortable/exportable table block. |
| src/components/Markdown/SurveyBlock.tsx | Adds JSON/YAML survey preview block. |
| src/components/Markdown/HtmlPreviewBlock.tsx | Adds sandboxed iframe HTML preview with expand-to-modal behavior. |
| src/components/Markdown/styles.css | Bundles/scopes highlight.js themes and fence-block styles; exported as @mieweb/ui/markdown.css. |
| src/components/Markdown/index.ts | Exposes the Markdown module’s public exports. |
| package.json | Adds markdown.css export + build copy step; adds hard deps (marked/dompurify/highlight.js) and optional peer deps metadata. |
| pnpm-lock.yaml | Locks new dependencies introduced by the Markdown feature. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)
src/components/Markdown/MermaidBlock.tsx:76
dangerouslySetInnerHTMLis used to inject Mermaid’s generated SVG. Even with a safersecurityLevel, it’s worth sanitizing/validating the SVG string (or rendering via a DOM API) before injection to reduce XSS risk from malformed output or upstream changes.
<div
ref={containerRef}
className="flex justify-center p-4"
dangerouslySetInnerHTML={{ __html: svg }}
/>
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+114
to
+129
| function sanitise(html: string): string { | ||
| return DOMPurify.sanitize(html, { | ||
| ADD_TAGS: ['iframe'], | ||
| ADD_ATTR: [ | ||
| 'target', | ||
| 'rel', | ||
| 'allow', | ||
| 'allowfullscreen', | ||
| 'sandbox', | ||
| 'srcdoc', | ||
| 'data-block-type', | ||
| 'data-block-id', | ||
| 'data-code', | ||
| 'data-lang', | ||
| ], | ||
| WHOLE_DOCUMENT: false, |
Comment on lines
+136
to
+142
| DOMPurify.addHook('afterSanitizeAttributes', (node) => { | ||
| if (node instanceof HTMLAnchorElement) { | ||
| node.setAttribute('target', '_blank'); | ||
| node.setAttribute('rel', 'noopener noreferrer'); | ||
| } | ||
| }); | ||
|
|
Comment on lines
+158
to
+182
| if (typeof document !== 'undefined') { | ||
| document.addEventListener('click', (event) => { | ||
| const btn = | ||
| event.target instanceof Element | ||
| ? event.target.closest<HTMLButtonElement>('.fence-copy-btn') | ||
| : null; | ||
| if (!btn) return; | ||
| const encoded = btn.closest('.fence-block')?.getAttribute('data-code'); | ||
| if (!encoded) return; | ||
| const code = decodeURIComponent(encoded); | ||
| const markCopied = () => { | ||
| btn.classList.add('is-copied'); | ||
| btn.setAttribute('aria-label', 'Copied'); | ||
| setTimeout(() => { | ||
| btn.classList.remove('is-copied'); | ||
| btn.setAttribute('aria-label', 'Copy code'); | ||
| }, 1500); | ||
| }; | ||
| if (navigator.clipboard?.writeText) { | ||
| navigator.clipboard | ||
| .writeText(code) | ||
| .then(markCopied) | ||
| .catch(() => {}); | ||
| } | ||
| }); |
Comment on lines
+301
to
+307
| const fenceRegex = /^```(\w+)\s*$/gm; | ||
| let match: RegExpExecArray | null; | ||
| const langs: string[] = []; | ||
| while ((match = fenceRegex.exec(text)) !== null) { | ||
| langs.push(match[1]); | ||
| } | ||
| await Promise.all(langs.map((l) => ensureLanguage(l))); |
Comment on lines
+10
to
+15
| import { CsvBlock } from './CsvBlock'; | ||
| import { HtmlPreviewBlock } from './HtmlPreviewBlock'; | ||
| import { MermaidBlock } from './MermaidBlock'; | ||
| import { SurveyBlock } from './SurveyBlock'; | ||
| import { useMarkdown } from './useMarkdown'; | ||
|
|
Comment on lines
+22
to
+36
| export const CsvBlock: React.FC<CsvBlockProps> = ({ code, id }) => { | ||
| const [sortConfig, setSortConfig] = useState<SortConfig | null>(null); | ||
|
|
||
| const parsed = useMemo(() => { | ||
| try { | ||
| const result = Papa.parse(code, { | ||
| header: true, | ||
| dynamicTyping: true, | ||
| skipEmptyLines: 'greedy' as const, | ||
| }) as Papa.ParseResult<Record<string, string>>; | ||
| return { headers: result.meta.fields ?? [], rows: result.data, error: null }; | ||
| } catch (err) { | ||
| return { | ||
| headers: [] as string[], | ||
| rows: [] as Record<string, string>[], |
Comment on lines
+26
to
+33
| mermaidReady = import(/* @vite-ignore */ 'mermaid').then((mod) => { | ||
| const m = (mod as { default: MermaidApi }).default; | ||
| m.initialize({ | ||
| startOnLoad: false, | ||
| theme: document.documentElement.classList.contains('dark') ? 'dark' : 'default', | ||
| securityLevel: 'loose', | ||
| }); | ||
| mermaidInstance = m; |
Comment on lines
+35
to
+43
| useEffect(() => { | ||
| function handleMessage(event: globalThis.MessageEvent) { | ||
| if (event.data?.type === 'HTML_PREVIEW_RESIZE' && event.data?.id === id) { | ||
| setIframeHeight(Math.min(Math.max(event.data.height, 200), 4000)); | ||
| } | ||
| } | ||
| window.addEventListener('message', handleMessage); | ||
| return () => window.removeEventListener('message', handleMessage); | ||
| }, [id]); |
Comment on lines
+86
to
+103
| /* atom-one-dark (scoped under data-theme="dark") */ | ||
| [data-theme='dark'] .hljs { | ||
| color: #abb2bf; | ||
| background: #282c34; | ||
| } | ||
| [data-theme='dark'] .hljs-comment, | ||
| [data-theme='dark'] .hljs-quote { | ||
| color: #5c6370; | ||
| } | ||
| [data-theme='dark'] .hljs-doctag, | ||
| [data-theme='dark'] .hljs-formula, | ||
| [data-theme='dark'] .hljs-keyword { | ||
| color: #c678dd; | ||
| } | ||
| [data-theme='dark'] .hljs-deletion, | ||
| [data-theme='dark'] .hljs-name, | ||
| [data-theme='dark'] .hljs-section, | ||
| [data-theme='dark'] .hljs-selector-tag, |
Comment on lines
+271
to
+279
| export interface UseMarkdownResult { | ||
| /** Render markdown text to sanitised HTML string */ | ||
| render: (text: string, cacheKey?: string) => string; | ||
| /** Async-render that lazy-loads needed languages first */ | ||
| renderAsync: (text: string, cacheKey?: string) => Promise<string>; | ||
| /** Clear the render cache */ | ||
| clearCache: () => void; | ||
| } | ||
|
|
- Render plain code fences via React portal + FenceBlock (new CodeBlock component) instead of injecting raw HTML + a global click listener. Removes COPY_BTN_HTML, the document-level click handler, and the .fence-block / .fence-copy-btn CSS — code blocks now use the same copy/raw-toggle UX as Mermaid/CSV/Survey/HTML blocks. - useMarkdown: bound the render cache (LRU, 200 entries), reset the block-id counter per render, use marked's RendererThis instead of unsafe `unknown` casts, register js/ts/sh hljs aliases. - MermaidBlock: drop unused containerRef. - HtmlPreviewBlock: drop unused iframeRef and unreachable Escape branch in handleCloseExpanded. - Export CodeBlock and highlightCode helper.
7ff2d4f to
ffb15f2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a self-contained Markdown rendering component so consumer apps (e.g. ozwell-workspace) don't have to re-implement the marked + DOMPurify + highlight.js pipeline themselves.
What's included
MarkdownRenderer— full pipeline (marked GFM + DOMPurify sanitisation + hljs highlighting + React portals for special blocks)FenceBlock— base wrapper (copy button, raw/rendered toggle, error state)MermaidBlock— sandboxed Mermaid diagram renderingCsvBlock— sortable / exportable CSV tableSurveyBlock— JSON/YAML survey previewHtmlPreviewBlock— sandboxed iframe HTML preview with expanduseMarkdown— hook (render/renderAsync/clearCache)[data-theme]Exports
@mieweb/ui— all of the above (named exports)@mieweb/ui/components/Markdown— tree-shakeable subpath@mieweb/ui/markdown.css— bundled hljs themes + fence-block stylesDependencies
marked,dompurify,highlight.js— small, always-needed for the markdown pipelinemermaid,papaparse,js-yaml— only loaded when the matching block type is used. Apps that never render Mermaid/CSV/Survey blocks don't pay the bundle cost.Why
Originally landed in ozwell-workspace; centralising here so other products consuming
@mieweb/ui'sAIChat(viarenderTextContent) can get full Markdown + code-block rendering for free.Usage