feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
这个提交包含在:
@@ -0,0 +1,162 @@
|
||||
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';
|
||||
import makeHideReplaceExtension from './utils/makeHideReplaceExtension';
|
||||
|
||||
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;
|
||||
const content = state.sliceDoc(node.to, closingTag.from);
|
||||
if (!content.trim()) return null;
|
||||
return [node.from, closingTag.to];
|
||||
}
|
||||
|
||||
if (info.closing) {
|
||||
const openingTag = findOpeningTag(node, state);
|
||||
if (!openingTag) return null;
|
||||
const content = state.sliceDoc(openingTag.to, node.from);
|
||||
if (!content.trim()) 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 = makeHideReplaceExtension({
|
||||
getRevealStrategy: (node, state) => {
|
||||
return selectionTouchesTag(node, state);
|
||||
},
|
||||
shouldHide: (node, state) => {
|
||||
return !!getMatchingTagRange(node, state);
|
||||
},
|
||||
});
|
||||
|
||||
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();
|
||||
在新工单中引用
屏蔽一个用户