/* global React, marked, DOMPurify */ const { useState, useEffect, useRef, useLayoutEffect, useMemo } = React; // ── Configure marked + DOMPurify once ──────────────────────────────────────── if (typeof marked !== "undefined") { marked.setOptions({ gfm: true, breaks: true }); } if (typeof DOMPurify !== "undefined" && !DOMPurify.__rndHooked) { DOMPurify.addHook("afterSanitizeAttributes", (node) => { if (node.tagName === "A" && node.getAttribute("href")) { node.setAttribute("target", "_blank"); node.setAttribute("rel", "noopener noreferrer"); } }); DOMPurify.__rndHooked = true; } function renderMarkdown(src) { if (!src) return ""; if (typeof marked === "undefined" || typeof DOMPurify === "undefined") { const escaped = String(src).replace(/[&<>]/g, c => ({ "&": "&", "<": "<", ">": ">" }[c])); return escaped.replace(/\n/g, "
"); } const html = marked.parse(String(src)); return DOMPurify.sanitize(html, { ADD_ATTR: ["target", "rel"], FORBID_TAGS: ["style", "script", "iframe"], }); } // ── Rendered markdown view with collapse / expand ──────────────────────────── function MarkdownView({ source, collapsedHeight = 160, className = "", style }) { const [expanded, setExpanded] = useState(false); const [overflows, setOverflows] = useState(false); const ref = useRef(null); const html = useMemo(() => renderMarkdown(source), [source]); useLayoutEffect(() => { if (!ref.current) return; const measure = () => { const el = ref.current; if (!el) return; setOverflows(el.scrollHeight > collapsedHeight + 4); }; measure(); const ro = new ResizeObserver(measure); ro.observe(ref.current); return () => ro.disconnect(); }, [html, collapsedHeight]); const showCollapse = overflows && !expanded; return (
{overflows && ( )}
); } // ── Markdown editor (toolbar + textarea + preview tab) ─────────────────────── function MarkdownEditor({ value, onChange, onKeyDown, rows = 8, autoFocus = false, placeholder = "Write something… (Markdown supported)" }) { const [tab, setTab] = useState("write"); const ref = useRef(null); useEffect(() => { if (autoFocus && tab === "write" && ref.current) { ref.current.focus(); const el = ref.current; if (el.value) el.setSelectionRange(el.value.length, el.value.length); } }, [autoFocus, tab]); const wrapSelection = (before, after = before, placeholder = "") => { const el = ref.current; if (!el) return; const start = el.selectionStart; const end = el.selectionEnd; const sel = el.value.slice(start, end); const inner = sel || placeholder; const next = el.value.slice(0, start) + before + inner + after + el.value.slice(end); onChange(next); requestAnimationFrame(() => { el.focus(); const pos = start + before.length; el.setSelectionRange(pos, pos + inner.length); }); }; const linePrefix = (prefix) => { const el = ref.current; if (!el) return; const start = el.selectionStart; const end = el.selectionEnd; const v = el.value; const lineStart = v.lastIndexOf("\n", start - 1) + 1; const next = v.slice(0, lineStart) + prefix + v.slice(lineStart); onChange(next); requestAnimationFrame(() => { el.focus(); const pos = end + prefix.length; el.setSelectionRange(pos, pos); }); }; const insertLink = () => { const el = ref.current; if (!el) return; const start = el.selectionStart; const end = el.selectionEnd; const sel = el.value.slice(start, end) || "link text"; const snippet = `[${sel}](https://)`; const next = el.value.slice(0, start) + snippet + el.value.slice(end); onChange(next); requestAnimationFrame(() => { el.focus(); const urlStart = start + sel.length + 3; el.setSelectionRange(urlStart, urlStart + 8); }); }; const handleToolbarMouseDown = (e) => e.preventDefault(); // keep textarea focus const Btn = ({ title, onClick, children }) => ( ); return (
{tab === "write" && (
wrapSelection("**", "**", "bold")}>B wrapSelection("*", "*", "italic")}>I wrapSelection("`", "`", "code")}>{"<>"} linePrefix("## ")}>H linePrefix("- ")}>• linePrefix("1. ")}>1. linePrefix("> ")}>❝ 🔗 wrapSelection("\n```\n", "\n```\n", "code")}>{"{ }"}
)}
{tab === "write" ? (