1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-08-10 22:11:50 +02:00

Desktop: Rich Text Editor: Add KaTeX to supported auto-replacements (#12081)

This commit is contained in:
Henry Heino
2025-04-12 03:46:55 -07:00
committed by GitHub
parent 5ad891e1f3
commit 9638cab9ea
8 changed files with 106 additions and 22 deletions

View File

@@ -266,6 +266,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useKeyboardRefocusHan
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useLinkTooltips.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useTabIndenter.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useTextPatternsLookup.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.js
packages/app-desktop/gui/NoteEditor/NoteEditor.js
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js

1
.gitignore vendored
View File

@@ -241,6 +241,7 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useKeyboardRefocusHan
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useLinkTooltips.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useScroll.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useTabIndenter.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useTextPatternsLookup.js
packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useWebViewApi.js
packages/app-desktop/gui/NoteEditor/NoteEditor.js
packages/app-desktop/gui/NoteEditor/NoteTitle/NoteTitleBar.js

View File

@@ -6,10 +6,10 @@ import * as exportDeletionLog from './exportDeletionLog';
import * as exportFolders from './exportFolders';
import * as exportNotes from './exportNotes';
import * as focusElement from './focusElement';
import * as openSecondaryAppInstance from './openSecondaryAppInstance';
import * as openPrimaryAppInstance from './openPrimaryAppInstance';
import * as openNoteInNewWindow from './openNoteInNewWindow';
import * as openPrimaryAppInstance from './openPrimaryAppInstance';
import * as openProfileDirectory from './openProfileDirectory';
import * as openSecondaryAppInstance from './openSecondaryAppInstance';
import * as replaceMisspelling from './replaceMisspelling';
import * as restoreNoteRevision from './restoreNoteRevision';
import * as startExternalEditing from './startExternalEditing';
@@ -30,10 +30,10 @@ const index: any[] = [
exportFolders,
exportNotes,
focusElement,
openSecondaryAppInstance,
openPrimaryAppInstance,
openNoteInNewWindow,
openPrimaryAppInstance,
openProfileDirectory,
openSecondaryAppInstance,
replaceMisspelling,
restoreNoteRevision,
startExternalEditing,

View File

@@ -43,6 +43,7 @@ import useKeyboardRefocusHandler from './utils/useKeyboardRefocusHandler';
import useDocument from '../../../hooks/useDocument';
import useEditDialog from './utils/useEditDialog';
import useEditDialogEventListeners from './utils/useEditDialogEventListeners';
import useTextPatternsLookup from './utils/useTextPatternsLookup';
const logger = Logger.create('TinyMCE');
@@ -654,6 +655,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
// Create and setup the editor
// -----------------------------------------------------------------------------------------
const textPatternsLookupRef = useTextPatternsLookup({ enabled: props.enableTextPatterns, enableMath: props.mathEnabled });
useEffect(() => {
if (!scriptLoaded) return;
if (!editorContainer) return;
@@ -740,25 +742,38 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
// button to work. See https://github.com/tinymce/tinymce/issues/5026.
forecolor: { inline: 'span', styles: { color: '%value' }, remove_similar: true },
},
text_patterns: props.enableTextPatterns ? [
// See https://www.tiny.cloud/docs/tinymce/latest/content-behavior-options/#text_patterns
// for the default value
{ start: '==', end: '==', format: 'joplinHighlight' },
{ start: '`', end: '`', format: 'code' },
{ start: '*', end: '*', format: 'italic' },
{ start: '**', end: '**', format: 'bold' },
{ start: '#', format: 'h1' },
{ start: '##', format: 'h2' },
{ start: '###', format: 'h3' },
{ start: '####', format: 'h4' },
{ start: '#####', format: 'h5' },
{ start: '######', format: 'h6' },
{ start: '1.', cmd: 'InsertOrderedList' },
{ start: '*', cmd: 'InsertUnorderedList' },
{ start: '-', cmd: 'InsertUnorderedList' },
] : [],
text_patterns: [],
text_patterns_lookup: () => textPatternsLookupRef.current(),
setup: (editor: Editor) => {
editor.addCommand('joplinMath', async () => {
const katex = editor.selection.getContent();
const md = `$${katex}$`;
// Save and clear the selection -- when this command is activated by a text pattern,
// TinyMCE:
// 1. Adjusts the selection just before calling the command to include the to-be-formatted text.
// 2. Calls the command.
// 3. Removes the "$" characters and restores the selection.
//
// As a result, the selection needs to be saved and restored.
const mathSelection = editor.selection.getBookmark();
const result = await markupToHtml.current(MarkupLanguage.Markdown, md, { bodyOnly: true });
// Replace the math...
const finalSelection = editor.selection.getBookmark();
editor.selection.moveToBookmark(mathSelection);
editor.selection.setContent(result.html);
editor.selection.moveToBookmark(finalSelection); // ...then move the selection back.
// Fire update events
editor.fire(TinyMceEditorEvents.JoplinChange);
dispatchDidUpdate(editor);
// The last replacement seems to need to be manually added to the undo history
editor.undoManager.add();
});
editor.addCommand('joplinAttach', () => {
insertResourcesIntoContentRef.current();
});

View File

@@ -0,0 +1,40 @@
import { useRef } from 'react';
interface TextPatternOptions {
enabled: boolean;
enableMath: boolean;
}
const useTextPatternsLookup = ({ enabled, enableMath }: TextPatternOptions) => {
const getTextPatterns = () => {
if (!enabled) return [];
return [
// See https://www.tiny.cloud/docs/tinymce/latest/content-behavior-options/#text_patterns
// for the default TinyMCE text patterns
{ start: '==', end: '==', format: 'joplinHighlight' },
// Only replace math if math rendering is enabled.
enableMath && { start: '$', end: '$', cmd: 'joplinMath' },
{ start: '`', end: '`', format: 'code' },
{ start: '*', end: '*', format: 'italic' },
{ start: '**', end: '**', format: 'bold' },
{ start: '#', format: 'h1' },
{ start: '##', format: 'h2' },
{ start: '###', format: 'h3' },
{ start: '####', format: 'h4' },
{ start: '#####', format: 'h5' },
{ start: '######', format: 'h6' },
{ start: '1.', cmd: 'InsertOrderedList' },
{ start: '*', cmd: 'InsertUnorderedList' },
{ start: '-', cmd: 'InsertUnorderedList' },
].filter(pattern => !!pattern);
};
// Store the lookup callback in a ref so that the editor doesn't need to be reloaded
// to use the new patterns:
const patternLookupRef = useRef(getTextPatterns);
patternLookupRef.current = getTextPatterns;
return patternLookupRef;
};
export default useTextPatternsLookup;

View File

@@ -423,6 +423,7 @@ function NoteEditorContent(props: NoteEditorProps) {
const searchMarkers = useSearchMarkers(showLocalSearch, localSearchMarkerOptions, props.searches, props.selectedSearchId, props.highlightedWords);
const markupLanguage = formNote.markup_language;
const editorProps: NoteBodyEditorProps = {
ref: editorRef,
contentKey: formNote.id,
@@ -432,7 +433,7 @@ function NoteEditorContent(props: NoteEditorProps) {
onWillChange: onBodyWillChange,
onMessage: onMessage,
content: formNote.body,
contentMarkupLanguage: formNote.markup_language,
contentMarkupLanguage: markupLanguage,
contentOriginalCss: formNote.originalCss,
resourceInfos: resourceInfos,
resourceDirectory: Setting.value('resourceDir'),
@@ -457,6 +458,8 @@ function NoteEditorContent(props: NoteEditorProps) {
onDrop: onDrop,
noteToolbarButtonInfos: props.toolbarButtonInfos,
plugins: props.plugins,
// KaTeX isn't supported in HTML notes
mathEnabled: markupLanguage === MarkupLanguage.Markdown && Setting.value('markdown.plugin.katex'),
fontSize: Setting.value('style.editor.fontSize'),
contentMaxWidth: props.contentMaxWidth,
scrollbarSize: props.scrollbarSize,

View File

@@ -132,6 +132,7 @@ export interface NoteBodyEditorProps {
onDrop: DropHandler;
noteToolbarButtonInfos: ToolbarItem[];
plugins: PluginStates;
mathEnabled: boolean;
fontSize: number;
contentMaxWidth: number;
isSafeMode: boolean;

View File

@@ -6,6 +6,8 @@ At its core, Joplin stores notes in [Markdown format](https://github.com/laurent
In some cases however, the extra markup format that appears in notes can be seen as a drawback. Bold text will `look **like this**` for example, and tables might not be particularly readable. For that reason, Joplin also features a Rich Text editor, which allows you to edit notes with a [WYSIWYG](https://en.wikipedia.org/wiki/WYSIWYG) editing experience. Bold text will "look **like this**" and tables will be more readable, among others.
## Limitations
However **there is a catch**: in Joplin, notes, even when edited with this Rich Text editor, are **still Markdown** under the hood. This is generally a good thing, because it means you can switch at any time between Markdown and Rich Text editor, and the note is still readable. It is also good if you sync with the mobile application, which doesn't have a rich text editor. The catch is that since Markdown is used under the hood, it means the rich text editor has a number of limitations it inherits from that format:
- For a start, **most Markdown plugins will not be compatible**. If you open a Markdown note that makes use of such plugin in the Rich Text editor, it is likely you will lose the plugin special formatting. The only supported plugins are the "fenced" plugins - those that wrap a section of text in triple backticks (for example, KaTeX, Mermaid, etc. are working). You can see a plugin's compatibility on the Markdown config screen.
@@ -21,3 +23,24 @@ However **there is a catch**: in Joplin, notes, even when edited with this Rich
- All reference links (`[title][link-name]`) are converted to inline links (`[title](https://example.com)`) when Joplin saves changes from the Rich Text editor.
Those are the known limitations but if you notice any other issue not listed here, please let us know [in the forum](https://discourse.joplinapp.org/).
## Markup autocompletion
By default, the Rich Text Editor automatically replaces certain text patterns with formatted content. Replacements are applied after each pattern is typed.
By default, the following patterns are replaced:
- `**bold**`: Formats `bold` as **bold**.
- `*italic*`: Formats `italic` as *italic*.
- `==highlighted==`: Highlights `highlighted`.
- <code>`code`</code>: Formats `code` as inline code.
- `$math$`: Auto-formats to inline math (using KaTeX math syntax). After rendering, equations can be edited by double-clicking or with the "edit" option in the right click menu.
- `# Heading 1`: Creates a level 1 heading. The `#` should be at the start of the line.
- `## Heading 2`: Creates a level 2 heading.
- `## Heading 3`: Creates a level 3 heading.
- `- List`: Creates a bulleted list.
- `1. List`: Creates a numbered list.
Most replacements require pressing the <kbd>space</kbd> or <kbd>enter</kbd> key after the closing formatting character. For example, typing `==test==` does not highlight "test", but pressing a space after the last `=` does.
These replacements can be disabled in settings &gt; note, using the "Auto-format Markdown" setting.