feat(docs): add "Copy page as Markdown" button and related functionality#59
feat(docs): add "Copy page as Markdown" button and related functionality#59syedayanali28 wants to merge 2 commits intoAnmolSaini16:mainfrom
Conversation
|
@syedayanali28 is attempting to deploy a commit to the mapcn oss program Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughA new "Copy page as Markdown" feature has been implemented, allowing users to export documentation and view pages as Markdown. The changes include adding the Changes
Sequence DiagramsequenceDiagram
actor User
participant Button as CopyPageAsMarkdownButton
participant DOM as docsPageElementToMarkdown
participant Clipboard as navigator.clipboard
User->>Button: Click "Copy Markdown"
Button->>Button: Find element by PAGE_MARKDOWN_ROOT_ID
alt Element found
Button->>DOM: Convert HTMLElement to Markdown
DOM->>DOM: Clone and strip non-copyable elements
DOM->>DOM: Traverse DOM & convert nodes<br/>(headings, lists, code, tables, etc.)
DOM-->>Button: Return Markdown string
Button->>Clipboard: writeText(markdown)
Clipboard-->>Button: Success
Button->>Button: Show "Copied" feedback<br/>(2s timeout)
else Element not found
Button->>Button: Exit silently
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
src/lib/docs-dom-to-markdown.ts (1)
91-100: List item rendering should indent continuation lines for valid nested/multiline markdown.Current
- ${blockMarkdown(li)}/${i+1}. ${blockMarkdown(li)}only prefixes first line, so nested content can render incorrectly.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/docs-dom-to-markdown.ts` around lines 91 - 100, The list rendering prefixes only the first line of each item, breaking nested/multiline markdown; update the ul/ol branches to compute the item marker (e.g., marker = "- " for ul or marker = `${i+1}. ` for ol), call blockMarkdown(li) to get the full item text, split that text on "\n", prefix the first line with marker and prefix each continuation line with spaces equal to marker.length (e.g., .map((line, idx) => (idx === 0 ? marker + line : " ".repeat(marker.length) + line))). Join the lines back with "\n" so nested and multiline content is correctly indented for both blockMarkdown and the ul/ol cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@package.json`:
- Line 27: Remove the unused dependency "turndown" from package.json by deleting
the "turndown": "^7.2.4" entry, then update your lockfile (run npm install or
yarn install) to remove it from node_modules and the lockfile; also scan the
repo for any imports/usages of turndown (e.g., require('turndown') or import
TurndownService) to confirm it's not used, and run the test/build scripts to
ensure no regressions.
In `@src/app/`(main)/docs/_components/copy-page-markdown-button.tsx:
- Around line 19-25: Wrap the clipboard write in a try/catch inside handleCopy:
call docsPageElementToMarkdown as before, then try awaiting
navigator.clipboard.writeText(md) and only on success call setCopied(true) and
schedule setCopied(false); on error catch the rejection, attempt a safe fallback
copy (create offscreen textarea, select, document.execCommand('copy')) or at
minimum log/report the error and do not leave an unhandled promise; reference
symbols: handleCopy, DOCS_PAGE_MARKDOWN_ROOT_ID, docsPageElementToMarkdown,
navigator.clipboard.writeText, setCopied.
In `@src/lib/docs-dom-to-markdown.ts`:
- Around line 26-29: The current code in the code-node handling (the branch that
checks tag === "code" and uses variable t) mutates inline code by replacing
backticks with apostrophes; instead compute the longest run of backticks in t
and wrap the code with a backtick fence of length (maxRun+1) instead of
performing t.replace(/`/g, "'"); i.e., keep t unchanged and generate a wrapper
like "`".repeat(maxRun+1) then return wrapper + t + wrapper so backticks in the
source are preserved while producing valid Markdown code spans.
---
Nitpick comments:
In `@src/lib/docs-dom-to-markdown.ts`:
- Around line 91-100: The list rendering prefixes only the first line of each
item, breaking nested/multiline markdown; update the ul/ol branches to compute
the item marker (e.g., marker = "- " for ul or marker = `${i+1}. ` for ol), call
blockMarkdown(li) to get the full item text, split that text on "\n", prefix the
first line with marker and prefix each continuation line with spaces equal to
marker.length (e.g., .map((line, idx) => (idx === 0 ? marker + line : "
".repeat(marker.length) + line))). Join the lines back with "\n" so nested and
multiline content is correctly indented for both blockMarkdown and the ul/ol
cases.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: bc616a83-eefb-4fec-a3af-05753fdb9dbe
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.jsonpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (6)
package.jsonsrc/app/(main)/docs/_components/component-preview-client.tsxsrc/app/(main)/docs/_components/copy-page-markdown-button.tsxsrc/app/(main)/docs/_components/docs.tsxsrc/lib/docs-dom-to-markdown.tssrc/lib/docs-markdown-ids.ts
| "shiki": "^3.20.0", | ||
| "tailwind-merge": "^2.6.0" | ||
| "tailwind-merge": "^2.6.0", | ||
| "turndown": "^7.2.4" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Verify whether turndown is used anywhere in the repo.
rg -n -P "\\bfrom\\s+[\"']turndown[\"']|require\\(\\s*[\"']turndown[\"']\\s*\\)|\\bTurndownService\\b"Repository: AnmolSaini16/mapcn
Length of output: 44
🏁 Script executed:
cat -n package.json | grep -A 5 -B 5 "turndown"Repository: AnmolSaini16/mapcn
Length of output: 411
🏁 Script executed:
# Check if turndown might be referenced in any comments or configuration
rg -i "turndown" --type jsonRepository: AnmolSaini16/mapcn
Length of output: 291
🏁 Script executed:
# Look for any dynamic imports or eval patterns that might use turndown
rg -E "(import\(|require\.resolve|eval)" -A 2 -B 2Repository: AnmolSaini16/mapcn
Length of output: 458
🏁 Script executed:
# Search all source files for any reference to turndown
find . -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \) ! -path "./node_modules/*" ! -path "./.next/*" ! -path "./dist/*" | head -20Repository: AnmolSaini16/mapcn
Length of output: 1132
🏁 Script executed:
# Search for turndown case-insensitively across the entire repo (excluding node_modules and build dirs)
rg "turndown" --type-list | head -20Repository: AnmolSaini16/mapcn
Length of output: 451
🏁 Script executed:
# Direct search for turndown in all text files excluding node_modules
rg "turndown" --glob "!node_modules" --glob "!.next" --glob "!dist" --glob "!build"Repository: AnmolSaini16/mapcn
Length of output: 291
Remove the unused turndown dependency from package.json.
The package is declared as a runtime dependency but has no imports or usages anywhere in the codebase, unnecessarily increasing bundle size and supply-chain surface area.
"turndown": "^7.2.4"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@package.json` at line 27, Remove the unused dependency "turndown" from
package.json by deleting the "turndown": "^7.2.4" entry, then update your
lockfile (run npm install or yarn install) to remove it from node_modules and
the lockfile; also scan the repo for any imports/usages of turndown (e.g.,
require('turndown') or import TurndownService) to confirm it's not used, and run
the test/build scripts to ensure no regressions.
| const handleCopy = useCallback(async () => { | ||
| const el = document.getElementById(DOCS_PAGE_MARKDOWN_ROOT_ID); | ||
| if (!el) return; | ||
| const md = docsPageElementToMarkdown(el); | ||
| await navigator.clipboard.writeText(md); | ||
| setCopied(true); | ||
| setTimeout(() => setCopied(false), 2000); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="$(fd -a 'copy-page-markdown-button.tsx$' | head -n 1)"
rg -n -C4 'handleCopy|navigator\.clipboard\.writeText|try\s*\{|catch\s*\(' "$FILE"Repository: AnmolSaini16/mapcn
Length of output: 743
Add error handling for clipboard write operations to prevent unhandled rejections.
The navigator.clipboard.writeText() call can reject due to permissions or insecure context, leaving the promise unhandled and breaking the copy UI.
💡 Proposed fix
const handleCopy = useCallback(async () => {
const el = document.getElementById(DOCS_PAGE_MARKDOWN_ROOT_ID);
if (!el) return;
- const md = docsPageElementToMarkdown(el);
- await navigator.clipboard.writeText(md);
- setCopied(true);
- setTimeout(() => setCopied(false), 2000);
+ if (!navigator.clipboard?.writeText) return;
+ try {
+ const md = docsPageElementToMarkdown(el);
+ await navigator.clipboard.writeText(md);
+ setCopied(true);
+ window.setTimeout(() => setCopied(false), 2000);
+ } catch {
+ setCopied(false);
+ }
}, []);📝 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.
| const handleCopy = useCallback(async () => { | |
| const el = document.getElementById(DOCS_PAGE_MARKDOWN_ROOT_ID); | |
| if (!el) return; | |
| const md = docsPageElementToMarkdown(el); | |
| await navigator.clipboard.writeText(md); | |
| setCopied(true); | |
| setTimeout(() => setCopied(false), 2000); | |
| const handleCopy = useCallback(async () => { | |
| const el = document.getElementById(DOCS_PAGE_MARKDOWN_ROOT_ID); | |
| if (!el) return; | |
| if (!navigator.clipboard?.writeText) return; | |
| try { | |
| const md = docsPageElementToMarkdown(el); | |
| await navigator.clipboard.writeText(md); | |
| setCopied(true); | |
| window.setTimeout(() => setCopied(false), 2000); | |
| } catch { | |
| setCopied(false); | |
| } | |
| }, []); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/`(main)/docs/_components/copy-page-markdown-button.tsx around lines
19 - 25, Wrap the clipboard write in a try/catch inside handleCopy: call
docsPageElementToMarkdown as before, then try awaiting
navigator.clipboard.writeText(md) and only on success call setCopied(true) and
schedule setCopied(false); on error catch the rejection, attempt a safe fallback
copy (create offscreen textarea, select, document.execCommand('copy')) or at
minimum log/report the error and do not leave an unhandled promise; reference
symbols: handleCopy, DOCS_PAGE_MARKDOWN_ROOT_ID, docsPageElementToMarkdown,
navigator.clipboard.writeText, setCopied.
| if (tag === "code") { | ||
| const t = e.textContent ?? ""; | ||
| result += t.includes("`") ? "`" + t.replace(/`/g, "'") + "`" : "`" + t + "`"; | ||
| } else if (tag === "a") { |
There was a problem hiding this comment.
Do not mutate inline code content when it contains backticks.
Replacing ` with ' changes the source text and breaks “copy page content as markdown” fidelity.
💡 Proposed fix
if (tag === "code") {
const t = e.textContent ?? "";
- result += t.includes("`") ? "`" + t.replace(/`/g, "'") + "`" : "`" + t + "`";
+ const longestRun = Math.max(
+ 0,
+ ...(t.match(/`+/g) ?? []).map((run) => run.length),
+ );
+ const fence = "`".repeat(longestRun + 1);
+ result += `${fence}${t}${fence}`;
} else if (tag === "a") {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/docs-dom-to-markdown.ts` around lines 26 - 29, The current code in
the code-node handling (the branch that checks tag === "code" and uses variable
t) mutates inline code by replacing backticks with apostrophes; instead compute
the longest run of backticks in t and wrap the code with a backtick fence of
length (maxRun+1) instead of performing t.replace(/`/g, "'"); i.e., keep t
unchanged and generate a wrapper like "`".repeat(maxRun+1) then return wrapper +
t + wrapper so backticks in the source are preserved while producing valid
Markdown code spans.
…or improved functionality
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/components/copy-page-markdown-button.tsx (1)
19-25:⚠️ Potential issue | 🟠 MajorHandle clipboard failures to avoid unhandled promise rejections.
Line 23 awaits
navigator.clipboard.writeTextwithout a guard/try...catch. In insecure contexts or denied permissions this rejects and breaks the copy flow. (This was raised in prior review rounds as well.)Proposed fix
const handleCopy = useCallback(async () => { const el = document.getElementById(PAGE_MARKDOWN_ROOT_ID); if (!el) return; - const md = docsPageElementToMarkdown(el); - await navigator.clipboard.writeText(md); - setCopied(true); - setTimeout(() => setCopied(false), 2000); + if (!navigator.clipboard?.writeText) return; + try { + const md = docsPageElementToMarkdown(el); + await navigator.clipboard.writeText(md); + setCopied(true); + window.setTimeout(() => setCopied(false), 2000); + } catch { + setCopied(false); + } }, []);#!/bin/bash set -euo pipefail FILE="$(fd -a 'copy-page-markdown-button.tsx$' | head -n 1)" rg -n -C3 'handleCopy|clipboard\.writeText|try\s*\{|catch\s*\(' "$FILE"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/copy-page-markdown-button.tsx` around lines 19 - 25, handleCopy currently awaits navigator.clipboard.writeText without error handling which can cause unhandled rejections; wrap the clipboard write in a try...catch inside the handleCopy callback (after retrieving el and computing md via docsPageElementToMarkdown and before calling setCopied) so failures are caught, log or silently ignore the error as appropriate, and only setCopied(true) when writeText succeeds; ensure you still reset the copied state with the existing setTimeout and keep PAGE_MARKDOWN_ROOT_ID logic intact.
🧹 Nitpick comments (2)
src/app/(view)/layout.tsx (1)
10-17: Extract the copy-markdown shell into a shared layout wrapper.Lines 11-15 duplicate the same
CopyPageAsMarkdownButton+PAGE_MARKDOWN_ROOT_IDcontainer structure already present insrc/app/(main)/layout.tsx(Lines 13-20). A shared wrapper component would prevent style/behavior drift.As per coding guidelines "Prefer iteration and modularization over code duplication".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`(view)/layout.tsx around lines 10 - 17, Duplicate UI shell containing CopyPageAsMarkdownButton and the PAGE_MARKDOWN_ROOT_ID container should be extracted into a shared wrapper component (e.g., MarkdownShell or CopyMarkdownWrapper) that renders the button and the div with id={PAGE_MARKDOWN_ROOT_ID} and accepts children; create this component, export it, replace the duplicated blocks in both src/app/(view)/layout.tsx and src/app/(main)/layout.tsx by importing and wrapping their children with the new MarkdownShell, and remove the now-duplicated JSX from both layout files while preserving className props and z-index/styling.src/app/(main)/layout.tsx (1)
13-15: WrapCopyPageAsMarkdownButtoninSuspensein this server layout.Line 14 mounts a client component directly; wrap it with a fallback for consistent client-boundary behavior.
Proposed fix
+import { Suspense } from "react"; import { Header } from "@/components/header"; import { CopyPageAsMarkdownButton } from "@/components/copy-page-markdown-button"; import { PAGE_MARKDOWN_ROOT_ID } from "@/lib/page-markdown-ids"; @@ - <CopyPageAsMarkdownButton className="absolute top-4 right-4 z-40 max-w-[calc(100%-1rem)] sm:right-6" /> + <Suspense fallback={null}> + <CopyPageAsMarkdownButton className="absolute top-4 right-4 z-40 max-w-[calc(100%-1rem)] sm:right-6" /> + </Suspense>As per coding guidelines "Wrap client components in Suspense with fallback".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`(main)/layout.tsx around lines 13 - 15, The CopyPageAsMarkdownButton client component is mounted directly in the server layout; wrap the CopyPageAsMarkdownButton element in a React.Suspense with an appropriate fallback (e.g., null or a small placeholder) to create a client boundary. Edit the layout component where CopyPageAsMarkdownButton is used (the main render in layout.tsx) and replace the direct JSX with <Suspense fallback={...}> around CopyPageAsMarkdownButton, importing Suspense from React if needed and keeping the existing className prop on the button.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/components/copy-page-markdown-button.tsx`:
- Around line 19-25: handleCopy currently awaits navigator.clipboard.writeText
without error handling which can cause unhandled rejections; wrap the clipboard
write in a try...catch inside the handleCopy callback (after retrieving el and
computing md via docsPageElementToMarkdown and before calling setCopied) so
failures are caught, log or silently ignore the error as appropriate, and only
setCopied(true) when writeText succeeds; ensure you still reset the copied state
with the existing setTimeout and keep PAGE_MARKDOWN_ROOT_ID logic intact.
---
Nitpick comments:
In `@src/app/`(main)/layout.tsx:
- Around line 13-15: The CopyPageAsMarkdownButton client component is mounted
directly in the server layout; wrap the CopyPageAsMarkdownButton element in a
React.Suspense with an appropriate fallback (e.g., null or a small placeholder)
to create a client boundary. Edit the layout component where
CopyPageAsMarkdownButton is used (the main render in layout.tsx) and replace the
direct JSX with <Suspense fallback={...}> around CopyPageAsMarkdownButton,
importing Suspense from React if needed and keeping the existing className prop
on the button.
In `@src/app/`(view)/layout.tsx:
- Around line 10-17: Duplicate UI shell containing CopyPageAsMarkdownButton and
the PAGE_MARKDOWN_ROOT_ID container should be extracted into a shared wrapper
component (e.g., MarkdownShell or CopyMarkdownWrapper) that renders the button
and the div with id={PAGE_MARKDOWN_ROOT_ID} and accepts children; create this
component, export it, replace the duplicated blocks in both
src/app/(view)/layout.tsx and src/app/(main)/layout.tsx by importing and
wrapping their children with the new MarkdownShell, and remove the
now-duplicated JSX from both layout files while preserving className props and
z-index/styling.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4b53a2aa-751f-4e18-be74-45198ae6e1d8
📒 Files selected for processing (7)
package.jsonsrc/app/(main)/docs/_components/docs.tsxsrc/app/(main)/layout.tsxsrc/app/(view)/layout.tsxsrc/components/copy-page-markdown-button.tsxsrc/lib/docs-dom-to-markdown.tssrc/lib/page-markdown-ids.ts
✅ Files skipped from review due to trivial changes (1)
- src/lib/page-markdown-ids.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- package.json
- src/app/(main)/docs/_components/docs.tsx
- src/lib/docs-dom-to-markdown.ts
This change adds a Copy Markdown action on documentation pages so readers can copy the full page as Markdown for pasting into tools such as LLMs.
Open any /docs/... page, click Copy Markdown, paste into an editor, and confirm headings, paragraphs, code fences, and tables look correct and match the page content.
Summary by CodeRabbit
New Features
Chores