Desktop,Mobile: Highlight `==marked==` text in the Markdown editor (#11794)

pull/11795/head^2
Henry Heino 2025-02-06 10:04:15 -08:00 committed by GitHub
parent 94bff77313
commit 1975ebd438
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 231 additions and 32 deletions

View File

@ -897,6 +897,10 @@ packages/editor/CodeMirror/editorCommands/sortSelectedLines.test.js
packages/editor/CodeMirror/editorCommands/sortSelectedLines.js
packages/editor/CodeMirror/editorCommands/supportsCommand.js
packages/editor/CodeMirror/getScrollFraction.js
packages/editor/CodeMirror/markdown/MarkdownHighlightExtension.test.js
packages/editor/CodeMirror/markdown/MarkdownHighlightExtension.js
packages/editor/CodeMirror/markdown/MarkdownMathExtension.test.js
packages/editor/CodeMirror/markdown/MarkdownMathExtension.js
packages/editor/CodeMirror/markdown/codeBlockLanguages/allLanguages.js
packages/editor/CodeMirror/markdown/codeBlockLanguages/defaultLanguage.js
packages/editor/CodeMirror/markdown/codeBlockLanguages/lookUpLanguage.js
@ -908,8 +912,6 @@ packages/editor/CodeMirror/markdown/markdownCommands.bulletedVsChecklist.test.js
packages/editor/CodeMirror/markdown/markdownCommands.test.js
packages/editor/CodeMirror/markdown/markdownCommands.toggleList.test.js
packages/editor/CodeMirror/markdown/markdownCommands.js
packages/editor/CodeMirror/markdown/markdownMathParser.test.js
packages/editor/CodeMirror/markdown/markdownMathParser.js
packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.test.js
packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.js
packages/editor/CodeMirror/markdown/utils/stripBlockquote.js
@ -920,6 +922,7 @@ packages/editor/CodeMirror/pluginApi/customEditorCompletion.js
packages/editor/CodeMirror/testUtil/createEditorControl.js
packages/editor/CodeMirror/testUtil/createEditorSettings.js
packages/editor/CodeMirror/testUtil/createTestEditor.js
packages/editor/CodeMirror/testUtil/findNodesWithName.js
packages/editor/CodeMirror/testUtil/forceFullParse.js
packages/editor/CodeMirror/testUtil/loadLanguages.js
packages/editor/CodeMirror/testUtil/pressReleaseKey.js

7
.gitignore vendored
View File

@ -872,6 +872,10 @@ packages/editor/CodeMirror/editorCommands/sortSelectedLines.test.js
packages/editor/CodeMirror/editorCommands/sortSelectedLines.js
packages/editor/CodeMirror/editorCommands/supportsCommand.js
packages/editor/CodeMirror/getScrollFraction.js
packages/editor/CodeMirror/markdown/MarkdownHighlightExtension.test.js
packages/editor/CodeMirror/markdown/MarkdownHighlightExtension.js
packages/editor/CodeMirror/markdown/MarkdownMathExtension.test.js
packages/editor/CodeMirror/markdown/MarkdownMathExtension.js
packages/editor/CodeMirror/markdown/codeBlockLanguages/allLanguages.js
packages/editor/CodeMirror/markdown/codeBlockLanguages/defaultLanguage.js
packages/editor/CodeMirror/markdown/codeBlockLanguages/lookUpLanguage.js
@ -883,8 +887,6 @@ packages/editor/CodeMirror/markdown/markdownCommands.bulletedVsChecklist.test.js
packages/editor/CodeMirror/markdown/markdownCommands.test.js
packages/editor/CodeMirror/markdown/markdownCommands.toggleList.test.js
packages/editor/CodeMirror/markdown/markdownCommands.js
packages/editor/CodeMirror/markdown/markdownMathParser.test.js
packages/editor/CodeMirror/markdown/markdownMathParser.js
packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.test.js
packages/editor/CodeMirror/markdown/utils/renumberSelectedLists.js
packages/editor/CodeMirror/markdown/utils/stripBlockquote.js
@ -895,6 +897,7 @@ packages/editor/CodeMirror/pluginApi/customEditorCompletion.js
packages/editor/CodeMirror/testUtil/createEditorControl.js
packages/editor/CodeMirror/testUtil/createEditorSettings.js
packages/editor/CodeMirror/testUtil/createTestEditor.js
packages/editor/CodeMirror/testUtil/findNodesWithName.js
packages/editor/CodeMirror/testUtil/forceFullParse.js
packages/editor/CodeMirror/testUtil/loadLanguages.js
packages/editor/CodeMirror/testUtil/pressReleaseKey.js

View File

@ -358,6 +358,7 @@ const CodeMirror = (props: NoteBodyEditorProps, ref: ForwardedRef<NoteBodyEditor
return {
language: isHTMLNote ? EditorLanguageType.Html : EditorLanguageType.Markdown,
readOnly: props.disabled,
markdownMarkEnabled: Setting.value('markdown.plugin.mark'),
katexEnabled: Setting.value('markdown.plugin.katex'),
themeData: {
...styles.globalTheme,

View File

@ -335,6 +335,7 @@ function NoteEditor(props: Props, ref: any) {
const editorSettings: EditorSettings = useMemo(() => ({
themeId: props.themeId,
themeData: editorTheme(props.themeId),
markdownMarkEnabled: Setting.value('markdown.plugin.mark'),
katexEnabled: Setting.value('markdown.plugin.katex'),
spellcheckEnabled: Setting.value('editor.mobile.spellcheckEnabled'),
language: EditorLanguageType.Markdown,

View File

@ -5,7 +5,8 @@ import createTheme from './theme';
import { EditorState } from '@codemirror/state';
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
import { GFM as GitHubFlavoredMarkdownExtension } from '@lezer/markdown';
import { MarkdownMathExtension } from './markdown/markdownMathParser';
import MarkdownMathExtension from './markdown/MarkdownMathExtension';
import MarkdownHighlightExtension from './markdown/MarkdownHighlightExtension';
import lookUpLanguage from './markdown/codeBlockLanguages/lookUpLanguage';
import { html } from '@codemirror/lang-html';
import { defaultKeymap, emacsStyleKeymap } from '@codemirror/commands';
@ -24,6 +25,8 @@ const configFromSettings = (settings: EditorSettings) => {
extensions: [
GitHubFlavoredMarkdownExtension,
settings.markdownMarkEnabled ? MarkdownHighlightExtension : [],
// Don't highlight KaTeX if the user disabled it
settings.katexEnabled ? MarkdownMathExtension : [],
],

View File

@ -0,0 +1,73 @@
import { EditorSelection, EditorState } from '@codemirror/state';
import createTestEditor from '../testUtil/createTestEditor';
import findNodesWithName from '../testUtil/findNodesWithName';
import { highlightMarkerTagName, highlightTagName } from './MarkdownHighlightExtension';
const createEditorState = async (initialText: string, expectedTags: string[]): Promise<EditorState> => {
return (await createTestEditor(initialText, EditorSelection.cursor(0), expectedTags)).state;
};
describe('MarkdownHighlightExtension', () => {
jest.retryTimes(2);
it.each([
{ // Should support single-word highlights
text: '==highlight==',
expectedHighlightRanges: [{ from: 0, to: '==highlight=='.length }],
expectedMarkerRanges: [
{ from: 0, to: 2 },
{ from: '==highlight'.length, to: '==highlight=='.length },
],
},
{ // Should support multi-word highlights
text: '==highlight test==',
expectedHighlightRanges: [{ from: 0, to: '==highlight test=='.length }],
expectedMarkerRanges: [
{ from: 0, to: 2 },
{ from: '==highlight test'.length, to: '==highlight test=='.length },
],
},
{ // Should support within-word highlights
text: 'test==ing==',
expectedHighlightRanges: [{ from: 'test'.length, to: 'test==ing=='.length }],
},
{ // Should not highlight if just one =
text: 'test==ing=',
expectedHighlightRanges: [],
},
{ // Should not highlight within code
text: '`==highlight test==`',
expectedHighlightRanges: [],
expectedMarkerRanges: [],
},
{ // Should highlight across line breaks
text: '==highlight\ntest== test',
expectedHighlightRanges: [{ from: 0, to: '==highlight\ntest=='.length }],
},
{ // Should not highlight across paragraph breaks
text: '==highlight\n\ntest== test',
expectedHighlightRanges: [],
expectedMarkerRanges: [],
},
])('should parse inline highlights (case %#: %j)', async ({ text, expectedHighlightRanges, expectedMarkerRanges }) => {
const expectedNodes: string[] = [];
if (expectedHighlightRanges.length) {
expectedNodes.push(highlightTagName);
}
if (expectedMarkerRanges?.length) {
expectedNodes.push(highlightMarkerTagName);
}
const editor = await createEditorState(text, expectedNodes);
const highlightNodes = findNodesWithName(editor, highlightTagName);
expect(highlightNodes).toMatchObject(expectedHighlightRanges);
if (expectedMarkerRanges) {
const markerNodes = findNodesWithName(editor, highlightMarkerTagName);
expect(markerNodes).toMatchObject(expectedMarkerRanges);
}
});
});

View File

@ -0,0 +1,94 @@
// Search for $s and $$s in markdown and mark the regions between them as math.
//
// Text between single $s is marked as InlineMath and text between $$s is marked
// as BlockMath.
import { tags, Tag } from '@lezer/highlight';
// Extend the existing markdown parser
import {
MarkdownConfig, InlineContext,
} from '@lezer/markdown';
const equalsSignCharcode = 61;
const backslashCharcode = 92;
export const highlightTagName = 'Highlight';
export const highlightMarkerTagName = 'HighlightMarker';
export const highlightTag = Tag.define();
export const highlightMarkerTag = Tag.define(tags.meta);
// Markdown extension for recognizing highlighting
const HighlightConfig: MarkdownConfig = {
defineNodes: [
{
name: highlightTagName,
style: highlightTag,
},
{
name: highlightMarkerTagName,
style: highlightMarkerTag,
},
],
parseInline: [{
name: highlightTagName,
after: 'InlineCode',
parse(cx: InlineContext, current: number, pos: number): number {
const nextCharCode = cx.char(pos + 1);
if (current !== equalsSignCharcode
|| nextCharCode !== equalsSignCharcode) {
return -1;
}
const nextNextCharCode = cx.char(pos + 2);
// Don't match if there's a space directly after the '='
if (/\s/.exec(String.fromCharCode(nextNextCharCode))) {
return -1;
}
const start = pos;
const end = cx.end;
let escaped = false;
pos ++;
// Scan ahead for the next '=='
for (; pos < end && (escaped || cx.slice(pos, pos + 2) !== '=='); pos++) {
if (!escaped && cx.char(pos) === backslashCharcode) {
escaped = true;
} else {
escaped = false;
}
}
// Don't match if the ending '=' is preceded by a space.
const prevChar = String.fromCharCode(cx.char(pos - 1));
if (/\s/.exec(prevChar)) {
return -1;
}
// It isn't highlighted if there's no ending '=='
if (pos === end) {
return -1;
}
// Advance to just after the ending '=='
pos += 2;
// Add the nodes
const startMarkerElem = cx.elt(highlightMarkerTagName, start, start + 2);
const endMarkerElem = cx.elt(highlightMarkerTagName, pos - 2, pos);
const highlightElem = cx.elt(highlightTagName, start, pos, [startMarkerElem, endMarkerElem]);
cx.addElement(highlightElem);
return pos + 1;
},
}],
};
const MarkdownHighlightExtension: MarkdownConfig[] = [
HighlightConfig,
];
export default MarkdownHighlightExtension;

View File

@ -1,31 +1,15 @@
import { syntaxTree } from '@codemirror/language';
import { SyntaxNode } from '@lezer/common';
import { EditorSelection, EditorState } from '@codemirror/state';
import { blockMathTagName, inlineMathContentTagName, inlineMathTagName } from './markdownMathParser';
import { blockMathTagName, inlineMathContentTagName, inlineMathTagName } from './MarkdownMathExtension';
import createTestEditor from '../testUtil/createTestEditor';
import findNodesWithName from '../testUtil/findNodesWithName';
// Creates an EditorState with math and markdown extensions
const createEditorState = async (initialText: string, expectedTags: string[]): Promise<EditorState> => {
return (await createTestEditor(initialText, EditorSelection.cursor(0), expectedTags)).state;
};
// Returns a list of all nodes with the given name in the given editor's syntax tree.
// Attempts to create the syntax tree if it doesn't exist.
const findNodesWithName = (editor: EditorState, nodeName: string) => {
const result: SyntaxNode[] = [];
syntaxTree(editor).iterate({
enter: (node) => {
if (node.name === nodeName) {
result.push(node.node);
}
},
});
return result;
};
describe('markdownMathParser', () => {
describe('MarkdownMathExtension', () => {
jest.retryTimes(2);

View File

@ -205,8 +205,10 @@ const BlockMathConfig: MarkdownConfig = {
wrap: wrappedTeXParser(blockMathContentTagName),
};
/** Markdown configuration for block and inline math support. */
export const MarkdownMathExtension: MarkdownConfig[] = [
// Markdown configuration for block and inline math support.
const MarkdownMathExtension: MarkdownConfig[] = [
InlineMathConfig,
BlockMathConfig,
];
export default MarkdownMathExtension;

View File

@ -44,6 +44,10 @@ const htmlTagNameDecoration = Decoration.mark({
attributes: { class: 'cm-htmlTag', ...noSpellCheckAttrs },
});
const markDecoration = Decoration.mark({
attributes: { class: 'cm-highlighted' },
});
const blockQuoteDecoration = Decoration.line({
attributes: { class: 'cm-blockQuote' },
});
@ -136,6 +140,7 @@ const nodeNameToMarkDecoration: Record<string, Decoration> = {
'TagName': htmlTagNameDecoration,
'HorizontalRule': horizontalRuleDecoration,
'TaskMarker': taskMarkerDecoration,
'Highlight': markDecoration,
};
const multilineNodes = {

View File

@ -5,7 +5,7 @@ import {
toggleBolded, toggleCode, toggleHeaderLevel, toggleItalicized, toggleMath, updateLink,
} from './markdownCommands';
import createTestEditor from '../testUtil/createTestEditor';
import { blockMathTagName } from './markdownMathParser';
import { blockMathTagName } from './MarkdownMathExtension';
describe('markdownCommands', () => {

View File

@ -4,6 +4,7 @@ import { EditorKeymap, EditorLanguageType, EditorSettings } from '../../types';
const createEditorSettings = (themeId: number) => {
const themeData = themeStyle(themeId);
const editorSettings: EditorSettings = {
markdownMarkEnabled: true,
katexEnabled: true,
spellcheckEnabled: true,
useExternalSearch: true,

View File

@ -3,9 +3,10 @@ import { GFM as GithubFlavoredMarkdownExt } from '@lezer/markdown';
import { indentUnit, syntaxTree } from '@codemirror/language';
import { SelectionRange, EditorSelection, EditorState, Extension } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import { MarkdownMathExtension } from '../markdown/markdownMathParser';
import MarkdownMathExtension from '../markdown/MarkdownMathExtension';
import forceFullParse from './forceFullParse';
import loadLanguages from './loadLanguages';
import MarkdownHighlightExtension from '../markdown/MarkdownHighlightExtension';
// Creates and returns a minimal editor with markdown extensions. Waits to return the editor
// until all syntax tree tags in `expectedSyntaxTreeTags` exist.
@ -22,7 +23,7 @@ const createTestEditor = async (
selection: EditorSelection.create([initialSelection]),
extensions: [
markdown({
extensions: [MarkdownMathExtension, GithubFlavoredMarkdownExt],
extensions: [MarkdownMathExtension, MarkdownHighlightExtension, GithubFlavoredMarkdownExt],
}),
indentUnit.of('\t'),
EditorState.tabSize.of(4),

View File

@ -0,0 +1,20 @@
import { syntaxTree } from '@codemirror/language';
import { EditorState } from '@codemirror/state';
import { SyntaxNode } from '@lezer/common';
// Returns a list of all nodes with the given name in the given editor's syntax tree.
// Attempts to create the syntax tree if it doesn't exist.
const findNodesWithName = (editor: EditorState, nodeName: string) => {
const result: SyntaxNode[] = [];
syntaxTree(editor).iterate({
enter: (node) => {
if (node.name === nodeName) {
result.push(node.node);
}
},
});
return result;
};
export default findNodesWithName;

View File

@ -8,7 +8,7 @@ import { tags } from '@lezer/highlight';
import { EditorView } from '@codemirror/view';
import { Extension } from '@codemirror/state';
import { inlineMathTag, mathTag } from './markdown/markdownMathParser';
import { inlineMathTag, mathTag } from './markdown/MarkdownMathExtension';
import { EditorTheme } from '../types';
// For an example on how to customize the theme, see:
@ -229,6 +229,11 @@ const createTheme = (theme: EditorTheme): Extension[] => {
fontSize: '1.0em',
},
'& .cm-highlighted': {
color: theme.searchMarkerColor,
backgroundColor: theme.searchMarkerBackgroundColor,
},
// Style the search widget. Use ':root' to increase the selector's precedence
// (override the existing preset styles).
':root & .cm-panel.cm-search': {

View File

@ -167,6 +167,7 @@ export interface EditorSettings {
keymap: EditorKeymap;
tabMovesFocus: boolean;
markdownMarkEnabled: boolean;
katexEnabled: boolean;
spellcheckEnabled: boolean;
readOnly: boolean;

View File

@ -338,6 +338,8 @@ export function extraStyles(theme: ThemeAndDerivedColors) {
// but some times, depending on the theme, it might be too dark or too light, so it can be
// specified directly by the theme too.
highlightedColor: theme.highlightedColor ?? theme.selectedColor2,
markHighlightColor: theme.searchMarkerColor,
markHighlightBackgroundColor: theme.searchMarkerBackgroundColor,
};
}

View File

@ -385,8 +385,8 @@ export default function(theme: any, options: Options = null) {
}
mark {
background: #F7D26E;
color: black;
background: ${theme.markHighlightBackgroundColor};
color: ${theme.searchMarkerColor};
}
/* =============================================== */