UNPKG

prism-code-editor

Version:

Lightweight, extensible code editor component for the web using Prism

254 lines (253 loc) 10.3 kB
import { i as doc, r as createTemplate, t as addListener } from "../core-E7btWBqK.js"; import { C as voidlessLangs, S as voidTags, t as addOverlay, v as getPosition } from "../utils-BffvWiz1.js"; import { t as testBracket } from "../bracket-Dr-UYgrN.js"; import { createCopyButton } from "../extensions/copyButton/index.js"; //#region src/client/hover.ts var counter = 0; var sp; var template = /* @__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>"); var getLanguageAt = (token) => { return /language-(\S*)/.exec(token.closest("[class*=language-]")?.className || "language-text")[1]; }; /** * Utility that makes it easier to add hover descriptions to tokens. * @param codeBlock Code block to add the functionality to. * @param callback Function called when a token with only textual children is hovered. * * The function gets called with the following arguments: * - `types`: Array with the token's type as the first element, followed by any alises. * - `language`: The language at the token's position. * - `text`: The `textContent` of the token. * - `element`: The `<span>` element of the hovered token. * * Lastly, the function should return an array of children that get added to the tooltip. * If `null` or `undefined` is returned, no tooltip is shown for the token. * @param options Options for configuring the size and position of the tooltip. */ var addHoverDescriptions = (codeBlock, callback, options = {}) => { let current; const { above, maxHeight, maxWidth } = options; const container = template(); 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 content = callback(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(${maxWidth ? maxWidth + "," : ""}${clientWidth}px - var(--padding-left) - 1em)`; tooltip.style.maxHeight = `min(${maxHeight ? maxHeight + "," : ""}${max}px, ${clientHeight * .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 = !above == top > bottom && (above ? top : bottom) < container.clientHeight ? !above : above; 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(); }; addListener(tooltip, "pointerleave", hide); addListener(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(); }); }; /** * Highlights bracket pairs when hovered. Clicking on a pair keeps it highlighted. * Clicking anywhere inside the container removes the highlight. * * This will match all brackets under the container together. If there are multiple code * blocks underneath, then brackets from different code blocks might get matched together. * * Note that there should not be any editors inside your container as this can cause * issues. * * @param container Container to add bracket pair highlighting to. * @param pairs Which characters to match together. The opening character must be followed * by the corresponding closing character. Defaults to "()[]{}". */ var highlightBracketPairsOnHover = (container, pairs = "()[]{}") => { highlightPairsOnHover(container, "active-bracket", "punctuation", (token, stack, map) => { const text = token.textContent; const bracketType = testBracket(text, pairs, text.length - 1); if (bracketType) if (bracketType % 2) stack[sp++] = [token, bracketType + 1]; else for (let i = sp; i;) { let [el, type] = stack[--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; } } }); }; /** * Highlights tag pairs when a tag name is hovered. Clicking on a pair keeps it * highlighted. Clicking anywhere inside the container removes the highlight. * * This will match all tags under the container together. If there are multiple code * blocks underneath, then tags from different code blocks might get matched together. * * Note that there should not be any editors inside your container as this can cause * issues. * * @param container Container to add tag pair highlighting to. */ var highlightTagPairsOnHover = (container) => { const partialTags = []; const matchTag = (nameEl, isClosing, lastChild, stack, map) => { const noVoidTags = voidlessLangs.has(getLanguageAt(nameEl)); const name = nameEl.textContent; const tagName = noVoidTags ? name : name.toLowerCase(); if (!lastChild.textContent[1] && (noVoidTags || !voidTags.test(tagName))) if (isClosing) { for (let i = sp, entry; entry = stack[--i];) if (tagName == entry[1]) { map.set(nameEl, entry[0]); map.set(entry[0], nameEl); sp = i; break; } } else stack[sp++] = [nameEl, tagName]; }; highlightPairsOnHover(container, "active-tagname", "tag", (token, stack, 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, stack, map); else partialTags.push([second, text[1] == "/"]); else if (hasClosingPunctuation && partialTags[0]) matchTag(...partialTags.pop(), lastChild, stack, map); }); }; var highlightPairsOnHover = (container, highlightClass, tokenName, forEachToken) => { let cache; const active = [[], []]; const element = container instanceof Node ? container : container.wrapper; const toggleHighlight = (index, add) => active[index].forEach((el) => el.classList.toggle(highlightClass, !!add)); const setCache = () => { cache = /* @__PURE__ */ new WeakMap(); let tokens = element.getElementsByClassName(tokenName); let i = sp = 0; let stack = []; let token; while (token = tokens[i++]) forEachToken(token, stack, 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); } }; addListener(element, "click", handler); addListener(element, "pointerover", handler); addListener(element, "pointerleave", () => { toggleHighlight(1); active[1] = []; }); }; //#endregion //#region src/client/code-block.ts /** @module code-blocks */ /** * Runs a callback function for each code block under the root in document order. If a * callback has previously been run for a code block, it's skipped. * * The callback function takes the code block as the first argument. The second parameter * is any additional properties passed when the code block was created. These options * were stringified to JSON and parsed. * @param root Root to search for code blocks under. * @param callback Function to run for each code block. * @returns An array with all visited code blocks in document order. */ var forEachCodeBlock = (root, callback) => { let els = root.getElementsByClassName("prism-code-editor"); let i = 0; let result = []; while (i < els.length) { const element = els[i++]; const json = element.dataset.props; if (!json) continue; const wrapper = element.firstChild; const codeBlock = { container: element, wrapper, lines: wrapper.children, code: wrapper.textContent.slice(0, -1), language: /language-(\S*)/.exec(element.className)[1] }; element.removeAttribute("data-props"); callback(codeBlock, JSON.parse(json)); result.push(codeBlock); } return result; }; /** * @param selector Selector used to specify which lines to omit from the resulting code. * @returns A function that returns the code inside a code block without any lines that * match the specified selector. */ var omitLines = (selector) => (codeBlock) => { let result = ""; let lines = codeBlock.lines; let i = 0; let line; while (line = lines[++i]) if (!line.matches(selector)) result += line.textContent; return result.slice(0, -1); }; /** * Adds a copy button to a code block. Requires styles from * `prism-code-editor/copy-button.css`. * @param codeBlock Code block to add the copy button to. * @param getCode Function used to get the copied code. Can be used to e.g. omit deleted * lines. */ var addCopyButton = (codeBlock, getCode) => { const container = createCopyButton(); const btn = container.firstChild; addListener(btn, "click", () => { btn.setAttribute("aria-label", "Copied!"); if (!navigator.clipboard?.writeText(getCode ? getCode(codeBlock) : codeBlock.code)) { 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); }; //#endregion export { addCopyButton, addHoverDescriptions, forEachCodeBlock, highlightBracketPairsOnHover, highlightTagPairsOnHover, omitLines }; //# sourceMappingURL=code-block.js.map