UNPKG

prosemirror-highlight

Version:

A ProseMirror plugin to highlight code blocks

189 lines (187 loc) 5.73 kB
// src/cache.ts var DecorationCache = class _DecorationCache { constructor(cache) { this.cache = new Map(cache); } /** * Gets the cache entry at the given doc position, or null if it doesn't exist * @param pos The doc position of the node you want the cache for */ get(pos) { return this.cache.get(pos); } /** * Sets the cache entry at the given position with the give node/decoration * values * @param pos The doc position of the node to set the cache for * @param node The node to place in cache * @param decorations The decorations to place in cache */ set(pos, node, decorations) { if (pos < 0) { return; } this.cache.set(pos, [node, decorations]); } /** * Removes the value at the oldPos (if it exists) and sets the new position to * the given values * @param oldPos The old node position to overwrite * @param newPos The new node position to set the cache for * @param node The new node to place in cache * @param decorations The new decorations to place in cache */ replace(oldPos, newPos, node, decorations) { this.remove(oldPos); this.set(newPos, node, decorations); } /** * Removes the cache entry at the given position * @param pos The doc position to remove from cache */ remove(pos) { this.cache.delete(pos); } /** * Invalidates the cache by removing all decoration entries on nodes that have * changed, updating the positions of the nodes that haven't and removing all * the entries that have been deleted; NOTE: this does not affect the current * cache, but returns an entirely new one * @param tr A transaction to map the current cache to */ invalidate(tr) { const returnCache = new _DecorationCache(this.cache); const mapping = tr.mapping; this.cache.forEach(([node, decorations], pos) => { if (pos < 0) { return; } const result = mapping.mapResult(pos); const mappedNode = tr.doc.nodeAt(result.pos); if (result.deleted || !(mappedNode == null ? void 0 : mappedNode.eq(node))) { returnCache.remove(pos); } else if (pos !== result.pos) { const updatedDecorations = decorations.map((d) => { return d.map(mapping, 0, 0); }).filter((d) => d != null); returnCache.replace(pos, result.pos, mappedNode, updatedDecorations); } }); return returnCache; } }; // src/plugin.ts import { Plugin, PluginKey } from "prosemirror-state"; import { DecorationSet } from "prosemirror-view"; function createHighlightPlugin({ parser, nodeTypes = ["code_block", "codeBlock"], languageExtractor = (node) => node.attrs.language }) { const key = new PluginKey("prosemirror-highlight"); return new Plugin({ key, state: { init(_, instance) { const cache = new DecorationCache(); const [decorations, promises] = calculateDecoration( instance.doc, parser, nodeTypes, languageExtractor, cache ); return { cache, decorations, promises }; }, apply: (tr, data) => { const cache = data.cache.invalidate(tr); const refresh = !!tr.getMeta("prosemirror-highlight-refresh"); if (!tr.docChanged && !refresh) { const decorations2 = data.decorations.map(tr.mapping, tr.doc); const promises2 = data.promises; return { cache, decorations: decorations2, promises: promises2 }; } const [decorations, promises] = calculateDecoration( tr.doc, parser, nodeTypes, languageExtractor, cache ); return { cache, decorations, promises }; } }, view: (view) => { const promises = /* @__PURE__ */ new Set(); const refresh = () => { if (promises.size > 0) { return; } const tr = view.state.tr.setMeta("prosemirror-highlight-refresh", true); view.dispatch(tr); }; const check = () => { var _a; const state = key.getState(view.state); for (const promise of (_a = state == null ? void 0 : state.promises) != null ? _a : []) { promises.add(promise); promise.then(() => { promises.delete(promise); refresh(); }).catch(() => { promises.delete(promise); }); } }; check(); return { update: () => { check(); } }; }, props: { decorations(state) { var _a; return (_a = this.getState(state)) == null ? void 0 : _a.decorations; } } }); } function calculateDecoration(doc, parser, nodeTypes, languageExtractor, cache) { const result = []; const promises = []; doc.descendants((node, pos) => { if (!node.type.isTextblock) { return true; } if (nodeTypes.includes(node.type.name)) { const language = languageExtractor(node); const cached = cache.get(pos); if (cached) { const [_, decorations] = cached; result.push(...decorations); } else { const decorations = parser({ content: node.textContent, language: language || void 0, pos, size: node.nodeSize }); if (decorations && Array.isArray(decorations)) { cache.set(pos, node, decorations); result.push(...decorations); } else if (decorations instanceof Promise) { cache.remove(pos); promises.push(decorations); } } } return false; }); return [DecorationSet.create(doc, result), promises]; } export { DecorationCache, createHighlightPlugin };