UNPKG

prism-react-editor

Version:

Lightweight, extensible code editor component for React apps

399 lines (398 loc) 13 kB
"use client"; import { jsx, jsxs } from "react/jsx-runtime"; import { useEffect, useCallback, useMemo } from "react"; import { u as useStableRef, a as addListener, d as doc } from "../core-Dm5I6BkG.js"; import { t as testBracket, q as addOverlay, k as createTemplate, b as addListener2, l as getPosition, v as voidlessLangs, o as voidTags } from "../local-Cq-4Fajb.js"; import { createCopyButton } from "../extensions/copy-button/index.js"; import { t as tokenizeText, l as languages, h as highlightTokens } from "../index-k28m3HFc.js"; let stack = []; let sp$1; const addAlias = (token, newAlias = "bracket-error") => { let alias = token.alias; token.alias = (alias ? alias + " " : "") + newAlias; }; const matchRecursive = (tokens, pairs) => { let token; let i = 0; for (; token = tokens[i++]; ) { if (typeof token == "string") continue; let content = token.content; let alias = token.alias; if (Array.isArray(content)) { matchRecursive(content, pairs); } else if ((alias || token.type) == "punctuation") { let last = token.length - 1; let bracketType = testBracket(content, pairs, last); if (bracketType) { if (bracketType % 2) stack[sp$1++] = [token, bracketType + 1]; else { let i2 = sp$1; let found; while (i2) { let entry = stack[--i2]; if (bracketType == entry[1]) { let alias2 = "bracket-level-" + i2 % 12; let j = i2; while (++j < sp$1) { addAlias(stack[j][0]); } addAlias(token, alias2); addAlias(entry[0], alias2); sp$1 = i2; i2 = 0; found = true; } } if (!found) addAlias(token); } } } } }; const rainbowBrackets = (pairs = "()[]{}") => { return (tokens) => { sp$1 = 0; matchRecursive(tokens, pairs); stack = []; }; }; const CopyButton = ({ codeBlock, props }) => { const code = useStableRef([]); code[0] = props.code; useEffect(() => { const container = createCopyButton(); const btn = container.firstChild; addListener(btn, "click", () => { btn.setAttribute("aria-label", "Copied!"); if (!navigator.clipboard?.writeText(code[0])) { const selection = getSelection(); const range = new Range(); selection.removeAllRanges(); selection.addRange(range); range.setStartAfter(codeBlock.lines[0]); range.setEndAfter(codeBlock.wrapper); doc.execCommand("copy"); range.collapse(); } }); addListener(btn, "pointerenter", () => btn.setAttribute("aria-label", "Copy")); addOverlay(codeBlock, container); return () => container.remove(); }, []); }; let counter = 0; let sp; const createTooltip = /* @__PURE__ */ createTemplate( "<div class=pce-tooltip style=z-index:5;top:auto;display:flex><div></div><div class=pce-hover-tooltip style=flex-shrink:0>" ); const getLanguageAt = (token) => { return /language-(\S*)/.exec(token.closest("[class*=language-").className)[1]; }; const HoverDescriptions = ({ callback, codeBlock, above, maxWidth, maxHeight }) => { const props = useStableRef( [] ); props[0] = maxWidth; props[1] = maxHeight; props[2] = !!above; props[3] = callback; useEffect(() => { let current; const container = createTooltip(); const pre = codeBlock.container; const style = container.style; const [spacer, tooltip] = container.children; const wrapper = codeBlock.wrapper; const id = tooltip.id = "pce-hover-" + counter++; const show = (target) => { const types = target.className.slice(6).split(" "); const text = target.textContent; const [maxWidth2, maxHeight2, above2, callback2] = props; const content = callback2(types, getLanguageAt(target), text, target); if (content) { let { left, right, top, bottom, height } = getPosition(codeBlock, target); let { clientHeight, clientWidth } = pre; let max = bottom > top ? bottom : top; tooltip.style.maxWidth = `min(${maxWidth2 ? maxWidth2 + "," : ""}${clientWidth}px - var(--padding-left) - 1em)`; tooltip.style.maxHeight = `min(${maxHeight2 ? maxHeight2 + "," : ""}${max}px, ${clientHeight * 0.6}px - 2em)`; spacer.style.width = (pre.matches(".pce-rtl") ? right : left) + "px"; tooltip.textContent = ""; tooltip.append(...content); container.parentNode || addOverlay(codeBlock, container); let placeAbove = !above2 == top > bottom && (above2 ? top : bottom) < container.clientHeight ? !above2 : above2; style[placeAbove ? "bottom" : "top"] = height + (placeAbove ? bottom : top) + "px"; style[placeAbove ? "top" : "bottom"] = "auto"; current?.removeAttribute("aria-describedby"); target.setAttribute("aria-describedby", id); current = target; } else hide(); }; const hide = () => { current?.removeAttribute("aria-describedby"); container.remove(); }; const cleanUp = addListener2(wrapper, "pointerover", (e) => { const target = e.target; if (!tooltip.contains(target)) { if (target.matches(".token") && (e.pointerType != "mouse" || !e.buttons) && !target.childElementCount) { show(target); } else hide(); } }); addListener2(tooltip, "pointerleave", hide); return () => { hide(), cleanUp(); }; }, []); }; const HighlightBracketPairsOnHover = ({ pairs = "()[]{}", codeBlock, props }) => { useHighlightOnHover( codeBlock, props, "active-bracket", "punctuation", useCallback( (token, stack2, map) => { const text = token.textContent; const last = text.length - 1; const bracketType = testBracket(text, pairs, last); if (bracketType) { if (bracketType % 2) stack2[sp++] = [token, bracketType + 1]; else { for (let i = sp; i; ) { let [el, type] = stack2[--i]; if (bracketType == type) { map.set(token, el); map.set(el, token); if (el.nextSibling == token) { el.textContent += token.textContent; token.textContent = ""; } sp = i; i = 0; } } } } }, [pairs] ) ); }; const HighlightTagPairsOnHover = ({ codeBlock, props }) => { const partialTags = []; const matchTag = (nameEl, isClosing, lastChild, stack2, map) => { const tagName = nameEl.textContent; const notSelfClosing = !lastChild.textContent[1] && (voidlessLangs.has(getLanguageAt(nameEl)) || !voidTags.test(tagName)); if (notSelfClosing) { if (isClosing) { for (let i = sp; i; ) { let entry = stack2[--i]; if (tagName == entry[1]) { map.set(nameEl, entry[0]); map.set(entry[0], nameEl); sp = i; i = 0; } } } else { stack2[sp++] = [nameEl, tagName]; } } }; useHighlightOnHover( codeBlock, props, "active-tagname", "tag", useStableRef((token, stack2, map) => { const children = token.children; const text = token.textContent; const lastChild = children[children.length - 1]; const second = children[1]; const hasClosingPunctuation = lastChild?.matches(".punctuation"); if (second?.matches(".tag")) { if (hasClosingPunctuation) { matchTag(second, text[1] == "/", lastChild, stack2, map); } else { partialTags.push([second, text[1] == "/"]); } } else if (hasClosingPunctuation && partialTags[0]) { matchTag(...partialTags.pop(), lastChild, stack2, map); } }) ); }; const useHighlightOnHover = (codeBlock, props, highlightClass, tokenName, forEachToken) => { useEffect(() => { let cache; const active = [[], []]; const wrapper = codeBlock.wrapper; const toggleHighlight = (index, add) => active[index].forEach((el) => el.classList.toggle(highlightClass, !!add)); const setCache = () => { cache = /* @__PURE__ */ new WeakMap(); let tokens = wrapper.getElementsByClassName(tokenName); let i = sp = 0; let stack2 = []; let token; while (token = tokens[i++]) { forEachToken(token, stack2, cache); } }; const handler = (e) => { const target = e.target.closest("." + tokenName); const index = e.type == "click" ? 0 : 1; if (active[0].includes(target)) return; if (index && (e.pointerType != "mouse" || e.buttons)) return; if (!cache) setCache(); toggleHighlight(index); active[index] = []; const other = cache.get(target); if (other) { active[1] = []; active[index] = [target, other]; toggleHighlight(index, true); } }; const cleanUps = [ // @ts-expect-error Allow PointerEvent addListener2(wrapper, "click", handler), addListener2(wrapper, "pointerover", handler), addListener2(wrapper, "pointerleave", () => { toggleHighlight(1); active[1] = []; }) ]; return () => cleanUps.forEach((c) => c()); }, [ props.code, props.language, // (props.preserveIndent ?? !!props.wordWrap) && (props.tabSize ?? 2), props.preserveIndent, props.wordWrap, props.tabSize, props.onTokenize, forEachToken ]); }; const CodeBlock = (props) => { const { onTokenize, guideIndents, rtl, wordWrap, preserveIndent = !!wordWrap, code, language, tabSize = 2, lineNumbers, lineNumberStart, className } = props; const hasGuides = !!guideIndents && !rtl; const lnOffset = lineNumberStart - 1 || 0; const normalizedCode = useMemo(() => { return preserveIndent ? code.replace(/\t/g, " ".repeat(tabSize)) : code; }, [code, preserveIndent && tabSize]); const lines = useMemo(() => { let tokens = tokenizeText( normalizedCode.includes("\r") ? normalizedCode.replace(/\r\n?/g, "\n") : normalizedCode, languages[language] || {} ); onTokenize?.(tokens); return highlightTokens(tokens).split("\n"); }, [onTokenize, language, normalizedCode]); const indents = useMemo(() => { if (preserveIndent || hasGuides) { const lines2 = code.split("\n"); const l = lines2.length; const result = Array(l).fill(0); for (let prevIndent = 0, emptyPos = -1, i = 0; i < l; i++) { let line = lines2[i]; let l2 = line.search(/\S/); let indent = 0; if (l2 < 0) { if (emptyPos < 0) emptyPos = i; } else { for (let i2 = 0; i2 < l2; ) { indent += line[i2++] == " " ? tabSize - indent % tabSize : 1; } if (emptyPos + 1) { if (indent != prevIndent) prevIndent = Math.min(indent, prevIndent) + 1; while (emptyPos < i) { result[emptyPos++] = prevIndent; } } result[i] = prevIndent = indent; emptyPos = -1; } } return result; } }, [preserveIndent || hasGuides, code, tabSize]); const codeLines = useMemo(() => { const keymap = {}; const getKey = (html) => { return html + "\n" + (keymap[html] = (keymap[html] || 0) + 1); }; return lines.map((html, i) => /* @__PURE__ */ jsx( "div", { className: "pce-line", style: { ["--indent"]: `${indents?.[i] || 0}ch` }, dangerouslySetInnerHTML: { __html: html + "\n" } }, getKey(html) )); }, [lines, indents]); const codeBlock = useStableRef({}); return /* @__PURE__ */ jsx( "pre", { className: `prism-code-editor language-${language}${lineNumbers ? " show-line-numbers" : ""} pce-${wordWrap ? "" : "no"}wrap${rtl ? " pce-rtl" : ""}${preserveIndent ? " pce-preserve" : ""}${hasGuides ? " pce-guides" : ""}${className ? " " + className : ""}`, ref: useStableRef((el) => { if (el) { codeBlock.container = el; codeBlock.lines = (codeBlock.wrapper = el.firstChild).children; } }), style: { ...props.style, ["--tab-size"]: tabSize, ["--number-width"]: (0 | Math.log10(lines.length + lnOffset)) + 1 + ".001ch", counterReset: `line ${lnOffset}` }, children: /* @__PURE__ */ jsxs("code", { className: "pce-wrapper", children: [ /* @__PURE__ */ jsx("div", { className: "pce-overlays", children: props.children?.(codeBlock, props) }), codeLines ] }) } ); }; export { CodeBlock, CopyButton, HighlightBracketPairsOnHover, HighlightTagPairsOnHover, HoverDescriptions, rainbowBrackets }; //# sourceMappingURL=index.js.map