prosemirror-highlight
Version:
A ProseMirror plugin to highlight code blocks
189 lines (187 loc) • 5.73 kB
JavaScript
// 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
};