mirror of https://github.com/laurent22/joplin.git
Desktop,Mobile: Highlight `==marked==` text in the Markdown editor (#11794)
parent
94bff77313
commit
1975ebd438
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 : [],
|
||||
],
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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);
|
||||
|
|
@ -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;
|
|
@ -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 = {
|
||||
|
|
|
@ -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', () => {
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
|
@ -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': {
|
||||
|
|
|
@ -167,6 +167,7 @@ export interface EditorSettings {
|
|||
keymap: EditorKeymap;
|
||||
tabMovesFocus: boolean;
|
||||
|
||||
markdownMarkEnabled: boolean;
|
||||
katexEnabled: boolean;
|
||||
spellcheckEnabled: boolean;
|
||||
readOnly: boolean;
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -385,8 +385,8 @@ export default function(theme: any, options: Options = null) {
|
|||
}
|
||||
|
||||
mark {
|
||||
background: #F7D26E;
|
||||
color: black;
|
||||
background: ${theme.markHighlightBackgroundColor};
|
||||
color: ${theme.searchMarkerColor};
|
||||
}
|
||||
|
||||
/* =============================================== */
|
||||
|
|
Loading…
Reference in New Issue