1
0
mirror of https://github.com/laurent22/joplin.git synced 2026-04-24 19:55:13 +02:00
Files

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();