You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2026-04-24 19:55:13 +02:00
160 lines
4.7 KiB
TypeScript
160 lines
4.7 KiB
TypeScript
import makeInlineReplaceExtension from './utils/makeInlineReplaceExtension';
|
|
import { Decoration } from '@codemirror/view';
|
|
import htmlNodeInfo, { HtmlNodeInfo } from '../../utils/htmlNodeInfo';
|
|
import { SyntaxNodeRef } from '@lezer/common';
|
|
import { EditorSelection, EditorState } from '@codemirror/state';
|
|
|
|
const hideDecoration = Decoration.replace({});
|
|
|
|
type OnRenderTagContent = (openingTag: HtmlNodeInfo)=> Decoration;
|
|
const createHtmlReplacementExtension = (tagName: string, onRenderContent: OnRenderTagContent) => {
|
|
const isMatchingTag = (info: HtmlNodeInfo) => {
|
|
return info.tagName().toLowerCase() === tagName;
|
|
};
|
|
const isMatchingOpeningTag = (info: HtmlNodeInfo) => {
|
|
return isMatchingTag(info) && info.opening;
|
|
};
|
|
const isMatchingClosingTag = (info: HtmlNodeInfo) => {
|
|
return isMatchingTag(info) && info.closing;
|
|
};
|
|
|
|
const findClosingTag = (openingTag: SyntaxNodeRef, state: EditorState) => {
|
|
const openingTagInfo = htmlNodeInfo(openingTag, state);
|
|
// Self-closing?
|
|
if (openingTagInfo.closing) {
|
|
return openingTag;
|
|
}
|
|
|
|
let cursor = openingTag.node.nextSibling;
|
|
let nestedTagCounter = 1;
|
|
|
|
// Find the matching closing tag
|
|
for (; !!cursor && nestedTagCounter > 0; cursor = cursor.nextSibling) {
|
|
const info = htmlNodeInfo(cursor, state);
|
|
if (info && isMatchingOpeningTag(info)) {
|
|
nestedTagCounter ++;
|
|
} else if (info && isMatchingClosingTag(info)) {
|
|
nestedTagCounter --;
|
|
}
|
|
|
|
if (nestedTagCounter === 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return cursor;
|
|
};
|
|
|
|
const findOpeningTag = (closingTag: SyntaxNodeRef, state: EditorState) => {
|
|
const closingTagInfo = htmlNodeInfo(closingTag, state);
|
|
// Self-closing?
|
|
if (closingTagInfo.opening) {
|
|
return closingTag;
|
|
}
|
|
|
|
let cursor = closingTag.node.prevSibling;
|
|
let nestedTagCounter = 1;
|
|
|
|
// Find the matching opening tag
|
|
for (; !!cursor && nestedTagCounter > 0; cursor = cursor.prevSibling) {
|
|
const info = htmlNodeInfo(cursor, state);
|
|
if (info && isMatchingClosingTag(info)) {
|
|
nestedTagCounter ++;
|
|
} else if (info && isMatchingOpeningTag(info)) {
|
|
nestedTagCounter --;
|
|
}
|
|
|
|
if (nestedTagCounter === 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return cursor;
|
|
};
|
|
|
|
const selectionIntersectsRange = (selection: EditorSelection, from: number, to: number) => {
|
|
const rangeContains = (point: number) => point >= from && point <= to;
|
|
const selectionContains = (point: number) => point >= selection.main.from && point <= selection.main.to;
|
|
return rangeContains(selection.main.from) || rangeContains(selection.main.to)
|
|
|| selectionContains(from) || selectionContains(to);
|
|
};
|
|
|
|
const getMatchingTagRange = (node: SyntaxNodeRef, state: EditorState): [number, number] | null => {
|
|
const info = htmlNodeInfo(node, state);
|
|
if (!info || !isMatchingTag(info)) return null;
|
|
|
|
if (info.opening && info.closing) {
|
|
return null;
|
|
}
|
|
|
|
if (info.opening) {
|
|
const closingTag = findClosingTag(node, state);
|
|
if (!closingTag) return null;
|
|
return [node.from, closingTag.to];
|
|
}
|
|
|
|
if (info.closing) {
|
|
const openingTag = findOpeningTag(node, state);
|
|
if (!openingTag) return null;
|
|
return [openingTag.from, node.to];
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
const selectionTouchesTag = (node: SyntaxNodeRef, state: EditorState) => {
|
|
const range = getMatchingTagRange(node, state);
|
|
if (!range) return false;
|
|
return selectionIntersectsRange(state.selection, range[0], range[1]);
|
|
};
|
|
|
|
const hideTags = makeInlineReplaceExtension({
|
|
getRevealStrategy: (node, state) => {
|
|
return selectionTouchesTag(node, state);
|
|
},
|
|
createDecoration: (node, state) => {
|
|
return getMatchingTagRange(node, state) ? hideDecoration : null;
|
|
},
|
|
});
|
|
|
|
const styleContent = makeInlineReplaceExtension({
|
|
getRevealStrategy: (node, state) => {
|
|
return selectionTouchesTag(node, state);
|
|
},
|
|
createDecoration: (node, state) => {
|
|
const info = htmlNodeInfo(node, state);
|
|
if (!info || !isMatchingOpeningTag(info)) return null;
|
|
if (!getMatchingTagRange(node, state)) return null;
|
|
return onRenderContent(info);
|
|
},
|
|
getDecorationRange(node, state) {
|
|
const closingTag = findClosingTag(node, state);
|
|
|
|
if (closingTag) {
|
|
return [node.to, closingTag.from];
|
|
} else {
|
|
return null;
|
|
}
|
|
},
|
|
});
|
|
|
|
return [hideTags, styleContent];
|
|
};
|
|
|
|
|
|
export default [
|
|
createHtmlReplacementExtension('sub', () => Decoration.mark({ tagName: 'sub' })),
|
|
createHtmlReplacementExtension('sup', () => Decoration.mark({ tagName: 'sup' })),
|
|
createHtmlReplacementExtension('strike', () => Decoration.mark({ tagName: 'strike' })),
|
|
createHtmlReplacementExtension('span', (info) => {
|
|
const styles = info.getAttr('style') ?? '';
|
|
const colorMatch = styles.match(/color:\s*(#?[a-z0-9A-Z]+|rgba?\([0-9, ]+\))(;|$)/);
|
|
|
|
return Decoration.mark({
|
|
attributes: {
|
|
style: colorMatch ? `color: ${colorMatch[1]};` : '',
|
|
},
|
|
});
|
|
}),
|
|
].flat();
|