From 189307306ee7706901c73c43f705652a2483dd4c Mon Sep 17 00:00:00 2001 From: P SRIKAR NAIDU Date: Mon, 26 Oct 2026 18:56:39 +0530 Subject: [PATCH 1/8] feat: add Markdown live preview tool --- .../MarkdownPreviewClient.tsx | 89 +++++++++ .../markdown-preview/opengraph-image.tsx | 11 ++ app/(tools)/markdown-preview/page.tsx | 12 ++ app/globals.css | 13 ++ lib/tools.ts | 187 ++++++++++++++++++ package-lock.json | 48 ++++- package.json | 5 + 7 files changed, 362 insertions(+), 3 deletions(-) create mode 100644 app/(tools)/markdown-preview/MarkdownPreviewClient.tsx create mode 100644 app/(tools)/markdown-preview/opengraph-image.tsx create mode 100644 app/(tools)/markdown-preview/page.tsx diff --git a/app/(tools)/markdown-preview/MarkdownPreviewClient.tsx b/app/(tools)/markdown-preview/MarkdownPreviewClient.tsx new file mode 100644 index 0000000..1b5a059 --- /dev/null +++ b/app/(tools)/markdown-preview/MarkdownPreviewClient.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { useMemo, useRef, useState } from "react"; +import { marked } from "marked"; +import DOMPurify from "dompurify"; +import Prism from "prismjs"; +import "prismjs/components/prism-markup"; +import "prismjs/components/prism-css"; +import "prismjs/components/prism-javascript"; +import "prismjs/components/prism-typescript"; +import "prismjs/components/prism-json"; +import "prismjs/components/prism-bash"; +import { CopyButton } from "@/components/shared/CopyButton"; + +const starter = `# Hello, Markdown!\n\nWrite on the left and see **GitHub-flavored Markdown** rendered here.\n\n- [x] Live preview\n- [ ] Copy sanitized HTML\n\n| Feature | Status |\n| --- | --- |\n| Tables | Ready |\n| Code | \`const answer = 42\` |\n\n~~~ts\nconst greeting = "Hello, world!";\n~~~`; + +marked.use({ + renderer: { + code({ text, lang }) { + const language = (lang || "").toLowerCase(); + const grammar = Prism.languages[language] || Prism.languages.markup; + const highlighted = language && Prism.languages[language] + ? Prism.highlight(text, grammar, language) + : Prism.util.encode(text); + return `
${highlighted}
`; + }, + }, +}); +marked.setOptions({ gfm: true, breaks: true }); + +export function MarkdownPreviewClient() { + const [markdown, setMarkdown] = useState(starter); + const [darkPreview, setDarkPreview] = useState(false); + const textareaRef = useRef(null); + const html = useMemo(() => { + const parsed = marked.parse(markdown) as string; + // Client components are still pre-rendered by Next.js. DOMPurify needs a + // browser window, so never call it during the server pass. + return typeof window === "undefined" ? "" : DOMPurify.sanitize(parsed); + }, [markdown]); + const words = markdown.trim() ? markdown.trim().split(/\s+/).length : 0; + const lines = markdown ? markdown.split("\n").length : 0; + + function insert(before: string, after = before, placeholder = "text") { + const input = textareaRef.current; + if (!input) return; + const start = input.selectionStart; + const end = input.selectionEnd; + const selected = markdown.slice(start, end) || placeholder; + const next = markdown.slice(0, start) + before + selected + after + markdown.slice(end); + setMarkdown(next); + requestAnimationFrame(() => { input.focus(); input.setSelectionRange(start + before.length, start + before.length + selected.length); }); + } + + function download() { + const url = URL.createObjectURL(new Blob([markdown], { type: "text/markdown" })); + const link = document.createElement("a"); link.href = url; link.download = "document.md"; link.click(); URL.revokeObjectURL(url); + } + + function insertImage() { + insert("![alt text](", ")", "https://example.com/image.png"); + } + + function insertTable() { + const input = textareaRef.current; + const position = input?.selectionStart ?? markdown.length; + const table = "| Header | Header |\n| --- | --- |\n| Cell | Cell |\n"; + setMarkdown(markdown.slice(0, position) + table + markdown.slice(position)); + requestAnimationFrame(() => input?.focus()); + } + + return
+
+ {[['Bold','**','**'],['Italic','*','*'],['Code','`','`'],['Link','[','](https://example.com)'],['H2','## ','']].map(([label, before, after]) => )} + + + + + + + +
+
+
Markdown