From 8ba49c6fdf830dc5b8a34f97b836fb63047d6f1d Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Wed, 17 Mar 2021 09:48:01 +0000 Subject: [PATCH] Desktop: Add support for strikethrough, sub, sup and insert formatting on Rich Text editor --- .eslintignore | 3 + .gitignore | 3 + .../sub_sup_insert_strikethrough.html | 1 + .../sub_sup_insert_strikethrough.md | 1 + .../NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx | 22 +++--- .../TinyMCE/utils/setupToolbarButtons.ts | 67 +++++++++++++++++++ packages/lib/models/Setting.ts | 6 +- .../turndown-plugin-gfm/src/strikethrough.js | 2 +- packages/turndown/src/commonmark-rules.js | 30 +++++++++ 9 files changed, 117 insertions(+), 18 deletions(-) create mode 100644 packages/app-cli/tests/html_to_md/sub_sup_insert_strikethrough.html create mode 100644 packages/app-cli/tests/html_to_md/sub_sup_insert_strikethrough.md create mode 100644 packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.ts diff --git a/.eslintignore b/.eslintignore index 579a67e1d..968abb975 100644 --- a/.eslintignore +++ b/.eslintignore @@ -397,6 +397,9 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js.map packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.d.ts packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js.map +packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.d.ts +packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js +packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js.map packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.d.ts packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js.map diff --git a/.gitignore b/.gitignore index dc713b18a..027dcf6f8 100644 --- a/.gitignore +++ b/.gitignore @@ -384,6 +384,9 @@ packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.js.map packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.d.ts packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/styles/index.js.map +packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.d.ts +packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js +packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.js.map packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.d.ts packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/useContextMenu.js.map diff --git a/packages/app-cli/tests/html_to_md/sub_sup_insert_strikethrough.html b/packages/app-cli/tests/html_to_md/sub_sup_insert_strikethrough.html new file mode 100644 index 000000000..d1149f97c --- /dev/null +++ b/packages/app-cli/tests/html_to_md/sub_sup_insert_strikethrough.html @@ -0,0 +1 @@ +X1 X1 Insert Strike \ No newline at end of file diff --git a/packages/app-cli/tests/html_to_md/sub_sup_insert_strikethrough.md b/packages/app-cli/tests/html_to_md/sub_sup_insert_strikethrough.md new file mode 100644 index 000000000..2a17e25d6 --- /dev/null +++ b/packages/app-cli/tests/html_to_md/sub_sup_insert_strikethrough.md @@ -0,0 +1 @@ +X1 X1 Insert ~~Strike~~ \ No newline at end of file diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx index 91fc3dc94..dbc118750 100644 --- a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/TinyMCE.tsx @@ -18,6 +18,7 @@ const { MarkupToHtml } = require('@joplin/renderer'); const taboverride = require('taboverride'); import { reg } from '@joplin/lib/registry'; import BaseItem from '@joplin/lib/models/BaseItem'; +import setupToolbarButtons from './utils/setupToolbarButtons'; const { themeStyle } = require('@joplin/lib/theme'); const { clipboard } = require('electron'); const supportedLocales = require('./supportedLocales'); @@ -543,7 +544,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { const toolbarPluginButtons = pluginCommandNames.length ? ` | ${pluginCommandNames.join(' ')}` : ''; const toolbar = [ - 'bold', 'italic', 'joplinHighlight', '|', + 'bold', 'italic', 'joplinHighlight', 'joplinStrikethrough', 'formattingExtras', '|', 'link', 'joplinInlineCode', 'joplinCodeBlock', 'joplinAttach', '|', 'bullist', 'numlist', 'joplinChecklist', '|', 'h1', 'h2', 'h3', 'hr', 'blockquote', 'table', `joplinInsertDateTime${toolbarPluginButtons}`, @@ -572,7 +573,11 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { contextmenu: false, browser_spellcheck: true, formats: { - 'joplin-highlight': { inline: 'mark', remove: 'all' }, + joplinHighlight: { inline: 'mark', remove: 'all' }, + joplinStrikethrough: { inline: 's', remove: 'all' }, + joplinInsert: { inline: 'ins', remove: 'all' }, + joplinSub: { inline: 'sub', remove: 'all' }, + joplinSup: { inline: 'sup', remove: 'all' }, }, setup: (editor: any) => { @@ -647,18 +652,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => { }, }); - editor.ui.registry.addToggleButton('joplinHighlight', { - tooltip: _('Highlight'), - icon: 'highlight-bg-color', - onAction: async function() { - editor.execCommand('mceToggleFormat', false, 'joplin-highlight'); - }, - onSetup: function(api: any) { - editor.formatter.formatChanged('joplin-highlight', function(state: boolean) { - api.setActive(state); - }); - }, - }); + setupToolbarButtons(editor); editor.ui.registry.addButton('joplinCodeBlock', { tooltip: _('Code Block'), diff --git a/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.ts b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.ts new file mode 100644 index 000000000..ef0943453 --- /dev/null +++ b/packages/app-desktop/gui/NoteEditor/NoteBody/TinyMCE/utils/setupToolbarButtons.ts @@ -0,0 +1,67 @@ +import { _ } from '@joplin/lib/locale'; + +interface ButtonDefinition { + name: string; + tooltip: string; + icon: string; + grouped?: boolean; +} + +function buttonDefinitions(): ButtonDefinition[] { + return [ + { + name: 'joplinHighlight', + tooltip: _('Highlight'), + icon: 'highlight-bg-color', + }, + { + name: 'joplinStrikethrough', + tooltip: _('Strikethrough'), + icon: 'strike-through', + }, + { + name: 'joplinInsert', + tooltip: _('Insert'), + icon: 'underline', + grouped: true, + }, + { + name: 'joplinSup', + tooltip: _('Superscript'), + icon: 'superscript', + grouped: true, + }, + { + name: 'joplinSub', + tooltip: _('Subscript'), + icon: 'subscript', + grouped: true, + }, + ]; +} + +export default function(editor: any) { + const definitions = buttonDefinitions(); + + for (const def of definitions) { + editor.ui.registry.addToggleButton(def.name, { + tooltip: def.tooltip, + icon: def.icon, + onAction: async function() { + editor.execCommand('mceToggleFormat', false, def.name); + }, + onSetup: function(api: any) { + editor.formatter.formatChanged(def.name, function(state: boolean) { + api.setActive(state); + }); + }, + }); + } + + const items: string[] = definitions.filter(d => !!d.grouped).map(d => d.name); + + editor.ui.registry.addGroupToolbarButton('formattingExtras', { + icon: 'image-options', + items: items.join(' '), + }); +} diff --git a/packages/lib/models/Setting.ts b/packages/lib/models/Setting.ts index 654b460ba..949fe70fc 100644 --- a/packages/lib/models/Setting.ts +++ b/packages/lib/models/Setting.ts @@ -788,12 +788,12 @@ class Setting extends BaseModel { 'markdown.plugin.mark': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable ==mark== syntax')}${wysiwygYes}` }, 'markdown.plugin.footnote': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable footnotes')}${wysiwygNo}` }, 'markdown.plugin.toc': { storage: SettingStorage.File, value: true, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable table of contents extension')}${wysiwygNo}` }, - 'markdown.plugin.sub': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable ~sub~ syntax')}${wysiwygNo}` }, - 'markdown.plugin.sup': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable ^sup^ syntax')}${wysiwygNo}` }, + 'markdown.plugin.sub': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable ~sub~ syntax')}${wysiwygYes}` }, + 'markdown.plugin.sup': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable ^sup^ syntax')}${wysiwygYes}` }, 'markdown.plugin.deflist': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable deflist syntax')}${wysiwygNo}` }, 'markdown.plugin.abbr': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable abbreviation syntax')}${wysiwygNo}` }, 'markdown.plugin.emoji': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable markdown emoji')}${wysiwygNo}` }, - 'markdown.plugin.insert': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable ++insert++ syntax')}${wysiwygNo}` }, + 'markdown.plugin.insert': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable ++insert++ syntax')}${wysiwygYes}` }, 'markdown.plugin.multitable': { storage: SettingStorage.File, value: false, type: SettingItemType.Bool, section: 'markdownPlugins', public: true, appTypes: ['mobile', 'desktop'], label: () => `${_('Enable multimarkdown table extension')}${wysiwygNo}` }, // Tray icon (called AppIndicator) doesn't work in Ubuntu diff --git a/packages/turndown-plugin-gfm/src/strikethrough.js b/packages/turndown-plugin-gfm/src/strikethrough.js index 5cbe40da5..9dfb9d7bb 100644 --- a/packages/turndown-plugin-gfm/src/strikethrough.js +++ b/packages/turndown-plugin-gfm/src/strikethrough.js @@ -2,7 +2,7 @@ export default function strikethrough (turndownService) { turndownService.addRule('strikethrough', { filter: ['del', 's', 'strike'], replacement: function (content) { - return '~' + content + '~' + return '~~' + content + '~~' } }) } diff --git a/packages/turndown/src/commonmark-rules.js b/packages/turndown/src/commonmark-rules.js index f68eb23fe..2d0fa2aaa 100644 --- a/packages/turndown/src/commonmark-rules.js +++ b/packages/turndown/src/commonmark-rules.js @@ -66,6 +66,36 @@ rules.highlight = { } } +// Unlike strikethrough and mark formatting, insert, sup and sub aren't +// widespread enough to automatically convert them to Markdown, but keep them as +// HTML anyway. Another issue is that we use "~" for subscript but that's +// actually the syntax for strikethrough on GitHub, so it's best to keep it as +// HTML to avoid any ambiguity. + +rules.insert = { + filter: 'ins', + + replacement: function (content, node, options) { + return '' + content + '' + } +} + +rules.superscript = { + filter: 'sup', + + replacement: function (content, node, options) { + return '' + content + '' + } +} + +rules.subscript = { + filter: 'sub', + + replacement: function (content, node, options) { + return '' + content + '' + } +} + // ============================== // END Joplin format support // ==============================