prism-react-editor
Version:
Lightweight, extensible code editor component for React apps
399 lines (398 loc) • 13 kB
JavaScript
"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