From d3b0d6ca57ed787fa8d46db0292d2a10ea379ecd Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Sat, 14 Sep 2024 13:37:31 +0200 Subject: [PATCH 01/57] Debug Turndown ES2020 #1604 --- vendors/turndown/turndown.js | 147 ++++++++++++++++++----------------- 1 file changed, 75 insertions(+), 72 deletions(-) diff --git a/vendors/turndown/turndown.js b/vendors/turndown/turndown.js index bc5acf95c..3b6d3f6e8 100644 --- a/vendors/turndown/turndown.js +++ b/vendors/turndown/turndown.js @@ -211,7 +211,7 @@ const TurndownService = (() => { ) , - replacement: function (content, node, options) { + replacement(content, node, options) { var href = node.getAttribute('href'); var title = cleanAttribute(node.getAttribute('title')); if (title) title = ' "' + title + '"'; @@ -239,7 +239,7 @@ const TurndownService = (() => { references: [], - append: function () { + append() { var references = ''; if (this.references.length) { references = '\n\n' + this.references.join('\n') + '\n\n'; @@ -298,6 +298,11 @@ const TurndownService = (() => { var titlePart = title ? ' "' + title + '"' : ''; return src ? '![' + alt + ']' + '(' + src + titlePart + ')' : '' } + }, + + style: { + filter: 'style', + replacement: () => '' } }, @@ -309,7 +314,7 @@ const TurndownService = (() => { class Rules { - constructor(options) { + constructor(options) { this.options = options; this._keep = []; this._remove = []; @@ -639,9 +644,7 @@ const TurndownService = (() => { class TurndownService { - constructor(options) { - if (!(this instanceof TurndownService)) return new TurndownService(options) - + constructor(options) { this.options = Object.assign({ rules: rules, headingStyle: 'setext', @@ -679,8 +682,8 @@ const TurndownService = (() => { if (input === '') return '' - var output = process.call(this, new RootNode(input, this.options)); - return postProcess.call(this, output) + var output = this.process(RootNode(input, this.options)); + return this.postProcess(output) } /** @@ -753,71 +756,71 @@ const TurndownService = (() => { escape(string) { return escapes.reduce((accumulator, escape) => accumulator.replace(escape[0], escape[1]), string) } + + /** + * Reduces a DOM node down to its Markdown string equivalent + * @private + * @param {HTMLElement} parentNode The node to convert + * @returns A Markdown representation of the node + * @type String + */ + + process(parentNode) { + var self = this; + return reduce.call(parentNode.childNodes, (output, node) => { + node = Node(node, self.options); + + var replacement = ''; + if (node.nodeType === 3) { + replacement = node.isCode ? node.nodeValue : self.escape(node.nodeValue); + } else if (node.nodeType === 1) { + replacement = self.replacementForNode(node); + } + + return join(output, replacement) + }, '') + } + + /** + * Appends strings as each rule requires and trims the output + * @private + * @param {String} output The conversion output + * @returns A trimmed version of the ouput + * @type String + */ + + postProcess(output) { + var self = this; + this.rules.forEach((rule) => { + if (typeof rule.append === 'function') { + output = join(output, rule.append(self.options)); + } + }); + + return output.replace(/^[\t\r\n]+/, '').replace(/[\t\r\n\s]+$/, '') + } + + /** + * Converts an element node to its Markdown equivalent + * @private + * @param {HTMLElement} node The node to convert + * @returns A Markdown representation of the node + * @type String + */ + + replacementForNode(node) { + var rule = this.rules.forNode(node); + var content = this.process(node); + var whitespace = node.flankingWhitespace; + if (whitespace.leading || whitespace.trailing) content = content.trim(); + return ( + whitespace.leading + + rule.replacement(content, node, this.options) + + whitespace.trailing + ) + } } - /** - * Reduces a DOM node down to its Markdown string equivalent - * @private - * @param {HTMLElement} parentNode The node to convert - * @returns A Markdown representation of the node - * @type String - */ - - const process = (parentNode) => { - var self = this; - return reduce.call(parentNode.childNodes, (output, node) => { - node = new Node(node, self.options); - - var replacement = ''; - if (node.nodeType === 3) { - replacement = node.isCode ? node.nodeValue : self.escape(node.nodeValue); - } else if (node.nodeType === 1) { - replacement = replacementForNode.call(self, node); - } - - return join(output, replacement) - }, '') - }, - - /** - * Appends strings as each rule requires and trims the output - * @private - * @param {String} output The conversion output - * @returns A trimmed version of the ouput - * @type String - */ - - postProcess = (output) => { - var self = this; - this.rules.forEach((rule) => { - if (typeof rule.append === 'function') { - output = join(output, rule.append(self.options)); - } - }); - - return output.replace(/^[\t\r\n]+/, '').replace(/[\t\r\n\s]+$/, '') - }, - - /** - * Converts an element node to its Markdown equivalent - * @private - * @param {HTMLElement} node The node to convert - * @returns A Markdown representation of the node - * @type String - */ - - replacementForNode = (node) => { - var rule = this.rules.forNode(node); - var content = process.call(this, node); - var whitespace = node.flankingWhitespace; - if (whitespace.leading || whitespace.trailing) content = content.trim(); - return ( - whitespace.leading + - rule.replacement(content, node, this.options) + - whitespace.trailing - ) - }, - /** * Joins replacement to the current output with appropriate number of new lines * @private @@ -827,7 +830,7 @@ const TurndownService = (() => { * @type String */ - join = (output, replacement) => { + const join = (output, replacement) => { var s1 = trimTrailingNewlines(output); var s2 = trimLeadingNewlines(replacement); var nls = Math.max(output.length - s1.length, replacement.length - s2.length); From 8fb706753153d79af49a568397773d3d75814ab0 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Sat, 14 Sep 2024 14:00:36 +0200 Subject: [PATCH 02/57] Added option to convert HTML to Markdown instead of plain, using Turndown #1604 --- dev/Common/Html.js | 17 +- dev/Common/Links.js | 1 + dev/Settings/User/General.js | 4 +- dev/Stores/User/Settings.js | 4 +- .../0.0.0/app/libraries/RainLoop/Actions.php | 4 +- .../app/libraries/RainLoop/Actions/User.php | 1 + .../v/0.0.0/app/localization/ar/user.json | 3 +- .../v/0.0.0/app/localization/be/user.json | 3 +- .../v/0.0.0/app/localization/bg/user.json | 3 +- .../v/0.0.0/app/localization/cs/user.json | 3 +- .../v/0.0.0/app/localization/da/user.json | 3 +- .../v/0.0.0/app/localization/de/user.json | 3 +- .../v/0.0.0/app/localization/el/user.json | 3 +- .../v/0.0.0/app/localization/en/user.json | 3 +- .../v/0.0.0/app/localization/es/user.json | 3 +- .../v/0.0.0/app/localization/et/user.json | 3 +- .../v/0.0.0/app/localization/eu/user.json | 3 +- .../v/0.0.0/app/localization/fa/user.json | 3 +- .../v/0.0.0/app/localization/fi/user.json | 3 +- .../v/0.0.0/app/localization/fr/user.json | 3 +- .../v/0.0.0/app/localization/hu/user.json | 3 +- .../v/0.0.0/app/localization/id/user.json | 3 +- .../v/0.0.0/app/localization/is/user.json | 3 +- .../v/0.0.0/app/localization/it/user.json | 3 +- .../v/0.0.0/app/localization/ja/user.json | 3 +- .../v/0.0.0/app/localization/ko/user.json | 3 +- .../v/0.0.0/app/localization/lt/user.json | 3 +- .../v/0.0.0/app/localization/lv/user.json | 3 +- .../v/0.0.0/app/localization/nb/user.json | 3 +- .../v/0.0.0/app/localization/nl/user.json | 3 +- .../v/0.0.0/app/localization/pl/user.json | 3 +- .../v/0.0.0/app/localization/pt-BR/user.json | 3 +- .../v/0.0.0/app/localization/pt/user.json | 3 +- .../v/0.0.0/app/localization/ro/user.json | 3 +- .../v/0.0.0/app/localization/ru/user.json | 3 +- .../v/0.0.0/app/localization/sk/user.json | 3 +- .../v/0.0.0/app/localization/sl/user.json | 3 +- .../v/0.0.0/app/localization/sv/user.json | 3 +- .../v/0.0.0/app/localization/tr/user.json | 3 +- .../v/0.0.0/app/localization/uk/user.json | 3 +- .../v/0.0.0/app/localization/vi/user.json | 3 +- .../v/0.0.0/app/localization/zh-TW/user.json | 3 +- .../v/0.0.0/app/localization/zh/user.json | 3 +- .../templates/Views/User/SettingsGeneral.html | 193 +++++++++--------- tasks/config.js | 1 + 45 files changed, 201 insertions(+), 135 deletions(-) diff --git a/dev/Common/Html.js b/dev/Common/Html.js index f3a9d82c7..07d975224 100644 --- a/dev/Common/Html.js +++ b/dev/Common/Html.js @@ -4,6 +4,9 @@ import { SettingsUserStore } from 'Stores/User/Settings'; const tmpl = createElement('template'), + + turndown = new TurndownService(), + htmlre = /[&<>"']/g, httpre = /^(https?:)?\/\//i, htmlmap = { @@ -604,6 +607,9 @@ export const * @returns {string} */ htmlToPlain = html => { + if (SettingsUserStore.markdown()) { + return htmlToMarkdown(html); + } const hr = '⎯'.repeat(64), forEach = (selector, fn) => tmpl.content.querySelectorAll(selector).forEach(fn), @@ -640,6 +646,8 @@ export const .replace(//gi, '\t') .replace(/<\/tr(\s[\s\S]*?)?>/gi, '\n'); + forEach('style', node => node.remove()); + // lines forEach('hr', node => node.replaceWith(`\n\n${hr}\n\n`)); @@ -700,6 +708,11 @@ export const return (tmpl.content.textContent || '').trim(); }, + htmlToMarkdown = html => { + tmpl.innerHTML = html; + return turndown.turndown(tmpl.content); + }, + /** * @param {string} plain * @param {boolean} findEmailAndLinksInText = false @@ -771,5 +784,7 @@ export const rl.Utils = { htmlToPlain: htmlToPlain, - plainToHtml: plainToHtml + plainToHtml: plainToHtml, + htmlToMarkdown: htmlToMarkdown +// markdownToHtml: md => marked.parse(md) }; diff --git a/dev/Common/Links.js b/dev/Common/Links.js index d5a8f6416..c0923ed58 100644 --- a/dev/Common/Links.js +++ b/dev/Common/Links.js @@ -45,6 +45,7 @@ export const proxy = url => BASE + '?/ProxyExternal/' +// + btoa(JSON.stringify([token,url]).replace(/ /g, '%20')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''), + btoa(url.replace(/ /g, '%20')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''), // + b64EncodeJSONSafe(url.replace(/ /g, '%20')), diff --git a/dev/Settings/User/General.js b/dev/Settings/User/General.js index edeb3e2de..a05a3227d 100644 --- a/dev/Settings/User/General.js +++ b/dev/Settings/User/General.js @@ -62,7 +62,7 @@ export class UserSettingsGeneral extends AbstractViewSettings { 'viewHTML', 'viewImages', 'viewImagesWhitelist', 'removeColors', 'allowStyles', 'allowDraftAutosave', 'hideDeleted', 'listInlineAttachments', 'simpleAttachmentsList', 'collapseBlockquotes', 'useCheckboxesInList', 'listGrouped', 'replySameFolder', 'allowSpellcheck', - 'messageReadAuto', 'showNextMessage', 'messageNewWindow' + 'messageReadAuto', 'showNextMessage', 'messageNewWindow', 'markdown' ].forEach(name => this[name] = SettingsUserStore[name]); this.allowLanguagesOnSettings = !!SettingsGet('allowLanguagesOnSettings'); @@ -123,7 +123,7 @@ export class UserSettingsGeneral extends AbstractViewSettings { 'ViewHTML', 'ViewImages', 'ViewImagesWhitelist', 'RemoveColors', 'AllowStyles', 'AllowDraftAutosave', 'HideDeleted', 'ListInlineAttachments', 'simpleAttachmentsList', 'CollapseBlockquotes', 'UseCheckboxesInList', 'listGrouped', 'ReplySameFolder', 'allowSpellcheck', - 'messageReadAuto', 'showNextMessage', 'messageNewWindow', + 'messageReadAuto', 'showNextMessage', 'messageNewWindow', 'markdown', 'DesktopNotifications', 'SoundNotification']); const fReloadLanguageHelper = (saveSettingsStep) => () => { diff --git a/dev/Stores/User/Settings.js b/dev/Stores/User/Settings.js index 98d9620c6..382c74289 100644 --- a/dev/Stores/User/Settings.js +++ b/dev/Stores/User/Settings.js @@ -50,6 +50,7 @@ export const SettingsUserStore = new class { layout: 1, editorDefaultType: 'Html', editorWysiwyg: 'Squire', + markdown: 0, msgDefaultAction: 1 }); @@ -113,7 +114,8 @@ export const SettingsUserStore = new class { 'requireTLS', 'pgpSign', 'pgpEncrypt', - 'allowSpellcheck' + 'allowSpellcheck', + 'markdown' /* 'MessagesPerPage', 'MessageReadDelay', diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php index bed876eb2..e1f93e00b 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php @@ -595,6 +595,7 @@ class Actions 'listGrouped' => $oConfig->Get('defaults', 'mail_list_grouped', false), 'MessagesPerPage' => \max(10, \intval($oConfig->Get('webmail', 'messages_per_page', 25)) ?: 25), 'messageNewWindow' => false, + 'markdown' => false, 'messageReadAuto' => true, // (bool) $oConfig->Get('webmail', 'message_read_auto', true), 'MessageReadDelay' => (int) $oConfig->Get('webmail', 'message_read_delay', 5), 'MsgDefaultAction' => (int) $oConfig->Get('defaults', 'msg_default_action', 1), @@ -690,7 +691,8 @@ class Actions $aResult['listGrouped'] = (bool)$oSettings->GetConf('listGrouped', $aResult['listGrouped']); $aResult['ContactsAutosave'] = (bool)$oSettings->GetConf('ContactsAutosave', $aResult['ContactsAutosave']); $aResult['MessagesPerPage'] = \max(10, \intval($oSettings->GetConf('MessagesPerPage', $aResult['MessagesPerPage']) ?: $aResult['MessagesPerPage'])); - $aResult['messageNewWindow'] = (int)$oSettings->GetConf('messageNewWindow', $aResult['messageNewWindow']); + $aResult['messageNewWindow'] = (bool)$oSettings->GetConf('messageNewWindow', $aResult['messageNewWindow']); + $aResult['markdown'] = (bool)$oSettings->GetConf('markdown', $aResult['markdown']); $aResult['messageReadAuto'] = (int)$oSettings->GetConf('messageReadAuto', $aResult['messageReadAuto']); $aResult['MessageReadDelay'] = (int)$oSettings->GetConf('MessageReadDelay', $aResult['MessageReadDelay']); $aResult['MsgDefaultAction'] = (int)$oSettings->GetConf('MsgDefaultAction', $aResult['MsgDefaultAction']); diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php index 6367cd79e..014f403e7 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php @@ -198,6 +198,7 @@ trait User $this->setSettingsFromParams($oSettings, 'MessageReadDelay', 'int'); $this->setSettingsFromParams($oSettings, 'MsgDefaultAction', 'int'); $this->setSettingsFromParams($oSettings, 'showNextMessage', 'bool'); + $this->setSettingsFromParams($oSettings, 'markdown', 'bool'); $this->setSettingsFromParams($oSettings, 'Resizer4Width', 'int'); $this->setSettingsFromParams($oSettings, 'Resizer5Width', 'int'); diff --git a/snappymail/v/0.0.0/app/localization/ar/user.json b/snappymail/v/0.0.0/app/localization/ar/user.json index e6ecc4ad6..dd8e7edd0 100644 --- a/snappymail/v/0.0.0/app/localization/ar/user.json +++ b/snappymail/v/0.0.0/app/localization/ar/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_WHITELIST": "Images whitelist", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "قم باضافة المستلمين تلقائيا الى دفتر العناوين", diff --git a/snappymail/v/0.0.0/app/localization/be/user.json b/snappymail/v/0.0.0/app/localization/be/user.json index 7ad8e2544..43fa3e68c 100644 --- a/snappymail/v/0.0.0/app/localization/be/user.json +++ b/snappymail/v/0.0.0/app/localization/be/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Показваць заўсёды", "IMAGES_WHITELIST": "Фарматы з белага спісу", "MESSAGE_POPUP_WINDOW": "Усплываючае вакно ў новым вакне замест карткі", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Аўтаматычна дадаваць атрымальнікаў лістоў у адрасную кнігу", diff --git a/snappymail/v/0.0.0/app/localization/bg/user.json b/snappymail/v/0.0.0/app/localization/bg/user.json index 55c1035e6..04039fb6e 100644 --- a/snappymail/v/0.0.0/app/localization/bg/user.json +++ b/snappymail/v/0.0.0/app/localization/bg/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_WHITELIST": "Images whitelist", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Автоматично добавяне на получателите към адресната ви книга", diff --git a/snappymail/v/0.0.0/app/localization/cs/user.json b/snappymail/v/0.0.0/app/localization/cs/user.json index faac7fc0c..c394aa303 100644 --- a/snappymail/v/0.0.0/app/localization/cs/user.json +++ b/snappymail/v/0.0.0/app/localization/cs/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_WHITELIST": "Images whitelist", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Automaticky přidávat adresy příjemců do Adresáře", diff --git a/snappymail/v/0.0.0/app/localization/da/user.json b/snappymail/v/0.0.0/app/localization/da/user.json index 03ffa43e2..8f31cfc14 100644 --- a/snappymail/v/0.0.0/app/localization/da/user.json +++ b/snappymail/v/0.0.0/app/localization/da/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_WHITELIST": "Images whitelist", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Tilføj automatisk modtager til adressebog", diff --git a/snappymail/v/0.0.0/app/localization/de/user.json b/snappymail/v/0.0.0/app/localization/de/user.json index 952536106..d18b083cb 100644 --- a/snappymail/v/0.0.0/app/localization/de/user.json +++ b/snappymail/v/0.0.0/app/localization/de/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Immer anzeigen", "IMAGES_WHITELIST": "Ausnahmeliste für Bilder", "MESSAGE_POPUP_WINDOW": "In neuem Fenster statt Tab anzeigen", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Empfänger automatisch zu Ihrem Adressbuch hinzuzufügen", diff --git a/snappymail/v/0.0.0/app/localization/el/user.json b/snappymail/v/0.0.0/app/localization/el/user.json index 02bdecfe3..570f48680 100644 --- a/snappymail/v/0.0.0/app/localization/el/user.json +++ b/snappymail/v/0.0.0/app/localization/el/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_WHITELIST": "Images whitelist", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Αυτόματη προσθήκη επαφών στον κατάλογο διευθύνσεων", diff --git a/snappymail/v/0.0.0/app/localization/en/user.json b/snappymail/v/0.0.0/app/localization/en/user.json index 44bf9b67c..c2db3f1d7 100644 --- a/snappymail/v/0.0.0/app/localization/en/user.json +++ b/snappymail/v/0.0.0/app/localization/en/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_WHITELIST": "Images whitelist", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Automatically add recipients to your address book", diff --git a/snappymail/v/0.0.0/app/localization/es/user.json b/snappymail/v/0.0.0/app/localization/es/user.json index 08e327d27..602602120 100644 --- a/snappymail/v/0.0.0/app/localization/es/user.json +++ b/snappymail/v/0.0.0/app/localization/es/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Mostrar siempre", "IMAGES_WHITELIST": "Lista blanca de imágenes", "MESSAGE_POPUP_WINDOW": "Popup en nueva ventana en lugar de pestaña", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Agregar automáticamente destinatarios a la libreta de direcciones", diff --git a/snappymail/v/0.0.0/app/localization/et/user.json b/snappymail/v/0.0.0/app/localization/et/user.json index e6d17f204..a8ded8627 100644 --- a/snappymail/v/0.0.0/app/localization/et/user.json +++ b/snappymail/v/0.0.0/app/localization/et/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_WHITELIST": "Images whitelist", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Lisa saajad automaatselt aadressiraamatusse", diff --git a/snappymail/v/0.0.0/app/localization/eu/user.json b/snappymail/v/0.0.0/app/localization/eu/user.json index 008449b37..663a20d5b 100644 --- a/snappymail/v/0.0.0/app/localization/eu/user.json +++ b/snappymail/v/0.0.0/app/localization/eu/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_WHITELIST": "Images whitelist", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Automatikoki gehitu hartzaileak helbide-liburura", diff --git a/snappymail/v/0.0.0/app/localization/fa/user.json b/snappymail/v/0.0.0/app/localization/fa/user.json index e11831efc..0a556ddc3 100644 --- a/snappymail/v/0.0.0/app/localization/fa/user.json +++ b/snappymail/v/0.0.0/app/localization/fa/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_WHITELIST": "Images whitelist", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "اطلاعات تماس گیرندگان را بصورت خودکار در آدرس‌بوک ذخیره کن", diff --git a/snappymail/v/0.0.0/app/localization/fi/user.json b/snappymail/v/0.0.0/app/localization/fi/user.json index 94606bf84..7a17501b6 100644 --- a/snappymail/v/0.0.0/app/localization/fi/user.json +++ b/snappymail/v/0.0.0/app/localization/fi/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_WHITELIST": "Images whitelist", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Lisää yhetystieto automaattisesti osoitekirjaan", diff --git a/snappymail/v/0.0.0/app/localization/fr/user.json b/snappymail/v/0.0.0/app/localization/fr/user.json index 0f594a582..8daeb8aa0 100644 --- a/snappymail/v/0.0.0/app/localization/fr/user.json +++ b/snappymail/v/0.0.0/app/localization/fr/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Toujours afficher", "IMAGES_WHITELIST": "Liste blanche Images", "MESSAGE_POPUP_WINDOW": "Popup dans une nouvelle fenêtre au lieu d'un onglet", - "MAILTO": "Inscrivez-vous en tant que gestionnaire de liens 'mailto:'" + "MAILTO": "Inscrivez-vous en tant que gestionnaire de liens 'mailto:'", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Ajouter automatiquement les destinataires à votre carnet d'adresses", diff --git a/snappymail/v/0.0.0/app/localization/hu/user.json b/snappymail/v/0.0.0/app/localization/hu/user.json index de51b1f8b..d36e40d19 100644 --- a/snappymail/v/0.0.0/app/localization/hu/user.json +++ b/snappymail/v/0.0.0/app/localization/hu/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_WHITELIST": "Images whitelist", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Címzettek automatikus hozzáadása a címtárhoz", diff --git a/snappymail/v/0.0.0/app/localization/id/user.json b/snappymail/v/0.0.0/app/localization/id/user.json index b08590901..4a529c0b2 100644 --- a/snappymail/v/0.0.0/app/localization/id/user.json +++ b/snappymail/v/0.0.0/app/localization/id/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_WHITELIST": "Images whitelist", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Tambah penerima ke daftar alamat secara otomatis", diff --git a/snappymail/v/0.0.0/app/localization/is/user.json b/snappymail/v/0.0.0/app/localization/is/user.json index 4eb403353..bdd931ee0 100644 --- a/snappymail/v/0.0.0/app/localization/is/user.json +++ b/snappymail/v/0.0.0/app/localization/is/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_WHITELIST": "Images whitelist", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Bæta viðtakendum sjálfkrafa í nafnaskrá", diff --git a/snappymail/v/0.0.0/app/localization/it/user.json b/snappymail/v/0.0.0/app/localization/it/user.json index d365bbacd..ddfe3cba0 100644 --- a/snappymail/v/0.0.0/app/localization/it/user.json +++ b/snappymail/v/0.0.0/app/localization/it/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Visualizza sempre", "IMAGES_WHITELIST": "Whitelist per immagini", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Aggiungi automaticamente le persone che ti inviano mail alla rubrica", diff --git a/snappymail/v/0.0.0/app/localization/ja/user.json b/snappymail/v/0.0.0/app/localization/ja/user.json index 4421a531d..4bd5c018c 100644 --- a/snappymail/v/0.0.0/app/localization/ja/user.json +++ b/snappymail/v/0.0.0/app/localization/ja/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_WHITELIST": "Images whitelist", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "返信したアドレスをアドレス帳へ自動的に追加する", diff --git a/snappymail/v/0.0.0/app/localization/ko/user.json b/snappymail/v/0.0.0/app/localization/ko/user.json index 39fbcafa3..ec5ab9ddc 100644 --- a/snappymail/v/0.0.0/app/localization/ko/user.json +++ b/snappymail/v/0.0.0/app/localization/ko/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_WHITELIST": "Images whitelist", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "수신인을 주소록에 자동으로 추가", diff --git a/snappymail/v/0.0.0/app/localization/lt/user.json b/snappymail/v/0.0.0/app/localization/lt/user.json index 24fce3bc6..591d0b6f3 100644 --- a/snappymail/v/0.0.0/app/localization/lt/user.json +++ b/snappymail/v/0.0.0/app/localization/lt/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_WHITELIST": "Images whitelist", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Automatiškai pridėti gavėjus į adresų knygą", diff --git a/snappymail/v/0.0.0/app/localization/lv/user.json b/snappymail/v/0.0.0/app/localization/lv/user.json index 4fddba713..af114823a 100644 --- a/snappymail/v/0.0.0/app/localization/lv/user.json +++ b/snappymail/v/0.0.0/app/localization/lv/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_WHITELIST": "Images whitelist", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Automatically add recipients to your address book", diff --git a/snappymail/v/0.0.0/app/localization/nb/user.json b/snappymail/v/0.0.0/app/localization/nb/user.json index 1b65e91fd..58d815411 100644 --- a/snappymail/v/0.0.0/app/localization/nb/user.json +++ b/snappymail/v/0.0.0/app/localization/nb/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_WHITELIST": "Images whitelist", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Legg til mottakere i adresseboka automatisk", diff --git a/snappymail/v/0.0.0/app/localization/nl/user.json b/snappymail/v/0.0.0/app/localization/nl/user.json index bc4e95b09..2654a7d73 100644 --- a/snappymail/v/0.0.0/app/localization/nl/user.json +++ b/snappymail/v/0.0.0/app/localization/nl/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Altijd tonen", "IMAGES_WHITELIST": "Afbeeldingen whitelist", "MESSAGE_POPUP_WINDOW": "Openen in een nieuw venster i.p.v. tab", - "MAILTO": "Registreer als 'mailto:'-linkhandler" + "MAILTO": "Registreer als 'mailto:'-linkhandler", + "HTML_TO_MD": "Converteer HTML naar Markdown i.p.v. platte tekst" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Automatisch ontvangers toevoegen aan uw adresboek", diff --git a/snappymail/v/0.0.0/app/localization/pl/user.json b/snappymail/v/0.0.0/app/localization/pl/user.json index e60028201..853adf389 100644 --- a/snappymail/v/0.0.0/app/localization/pl/user.json +++ b/snappymail/v/0.0.0/app/localization/pl/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Zawsze pokazuj", "IMAGES_WHITELIST": "Biała lista obrazów", "MESSAGE_POPUP_WINDOW": "Pokaż w nowym oknie zamiast w nowej karcie", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Automatycznie dodawaj odbiorców do książki adresowej", diff --git a/snappymail/v/0.0.0/app/localization/pt-BR/user.json b/snappymail/v/0.0.0/app/localization/pt-BR/user.json index bad9a4578..935e9aca9 100644 --- a/snappymail/v/0.0.0/app/localization/pt-BR/user.json +++ b/snappymail/v/0.0.0/app/localization/pt-BR/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_WHITELIST": "Images whitelist", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Adicionar automaticamente os destinatários na lista de endereços", diff --git a/snappymail/v/0.0.0/app/localization/pt/user.json b/snappymail/v/0.0.0/app/localization/pt/user.json index e93ca5d08..df7a8f9c6 100644 --- a/snappymail/v/0.0.0/app/localization/pt/user.json +++ b/snappymail/v/0.0.0/app/localization/pt/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Mostrar sempre", "IMAGES_WHITELIST": "Lista Branca de imagens", "MESSAGE_POPUP_WINDOW": "Abrir em nova janela em vez de um separador", - "MAILTO": "Registar como manipulador de ligações 'mailto:'" + "MAILTO": "Registar como manipulador de ligações 'mailto:'", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Adicionar destinatários automaticamente à sua lista de endereços", diff --git a/snappymail/v/0.0.0/app/localization/ro/user.json b/snappymail/v/0.0.0/app/localization/ro/user.json index 665c1ce51..e411c3671 100644 --- a/snappymail/v/0.0.0/app/localization/ro/user.json +++ b/snappymail/v/0.0.0/app/localization/ro/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_WHITELIST": "Images whitelist", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Adaugă automat destinatarii la agenda de scrisori", diff --git a/snappymail/v/0.0.0/app/localization/ru/user.json b/snappymail/v/0.0.0/app/localization/ru/user.json index 37bcd5f51..fb9e5340c 100644 --- a/snappymail/v/0.0.0/app/localization/ru/user.json +++ b/snappymail/v/0.0.0/app/localization/ru/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Показать всегда", "IMAGES_WHITELIST": "Форматы из белого списка", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Автоматически добавлять получателей писем в адресную книгу", diff --git a/snappymail/v/0.0.0/app/localization/sk/user.json b/snappymail/v/0.0.0/app/localization/sk/user.json index 7e3a39d15..67f4f3e67 100644 --- a/snappymail/v/0.0.0/app/localization/sk/user.json +++ b/snappymail/v/0.0.0/app/localization/sk/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_WHITELIST": "Images whitelist", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Automaticky pridávať príjemcov správ do Adresára", diff --git a/snappymail/v/0.0.0/app/localization/sl/user.json b/snappymail/v/0.0.0/app/localization/sl/user.json index f0c49e6fa..30dd28e15 100644 --- a/snappymail/v/0.0.0/app/localization/sl/user.json +++ b/snappymail/v/0.0.0/app/localization/sl/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_WHITELIST": "Images whitelist", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Samodejno dodaj prejemnike v imenik", diff --git a/snappymail/v/0.0.0/app/localization/sv/user.json b/snappymail/v/0.0.0/app/localization/sv/user.json index b6ae83b90..def3a526c 100644 --- a/snappymail/v/0.0.0/app/localization/sv/user.json +++ b/snappymail/v/0.0.0/app/localization/sv/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_WHITELIST": "Images whitelist", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Lägg till mottagare automatiskt i din adressbok", diff --git a/snappymail/v/0.0.0/app/localization/tr/user.json b/snappymail/v/0.0.0/app/localization/tr/user.json index ba422f154..25f25c806 100644 --- a/snappymail/v/0.0.0/app/localization/tr/user.json +++ b/snappymail/v/0.0.0/app/localization/tr/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_WHITELIST": "Images whitelist", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Otomatik olarak adres defterinize alıcıyı ekle", diff --git a/snappymail/v/0.0.0/app/localization/uk/user.json b/snappymail/v/0.0.0/app/localization/uk/user.json index 2258a8cbf..cdc5886e8 100644 --- a/snappymail/v/0.0.0/app/localization/uk/user.json +++ b/snappymail/v/0.0.0/app/localization/uk/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_WHITELIST": "Images whitelist", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Автоматично додавати отримувачів у адресну книгу", diff --git a/snappymail/v/0.0.0/app/localization/vi/user.json b/snappymail/v/0.0.0/app/localization/vi/user.json index 1761aa08f..cd92440a6 100644 --- a/snappymail/v/0.0.0/app/localization/vi/user.json +++ b/snappymail/v/0.0.0/app/localization/vi/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "Show always", "IMAGES_WHITELIST": "Images whitelist", "MESSAGE_POPUP_WINDOW": "Popup in new window instead of tab", - "MAILTO": "Register as 'mailto:' links handler" + "MAILTO": "Register as 'mailto:' links handler", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Tự động thêm người nhận thư vào sổ địa chỉ của bạn", diff --git a/snappymail/v/0.0.0/app/localization/zh-TW/user.json b/snappymail/v/0.0.0/app/localization/zh-TW/user.json index a014582dc..906216ad5 100644 --- a/snappymail/v/0.0.0/app/localization/zh-TW/user.json +++ b/snappymail/v/0.0.0/app/localization/zh-TW/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "永遠顯示", "IMAGES_WHITELIST": "圖片白名單", "MESSAGE_POPUP_WINDOW": "在新彈出窗口而非分頁中打開", - "MAILTO": "注冊成 mailto: 連結處理器" + "MAILTO": "注冊成 mailto: 連結處理器", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "自動新增收件者至通訊錄", diff --git a/snappymail/v/0.0.0/app/localization/zh/user.json b/snappymail/v/0.0.0/app/localization/zh/user.json index 753092ec6..898618152 100644 --- a/snappymail/v/0.0.0/app/localization/zh/user.json +++ b/snappymail/v/0.0.0/app/localization/zh/user.json @@ -499,7 +499,8 @@ "IMAGES_OPTION_ALWAYS": "总是显示", "IMAGES_WHITELIST": "图片白名单", "MESSAGE_POPUP_WINDOW": "弹出页面使用新窗口代替新选项卡", - "MAILTO": "注册成 mailto: 链接处理器" + "MAILTO": "注册成 mailto: 链接处理器", + "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "自动添加到您的地址簿", diff --git a/snappymail/v/0.0.0/app/templates/Views/User/SettingsGeneral.html b/snappymail/v/0.0.0/app/templates/Views/User/SettingsGeneral.html index 425219a7c..233149db3 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/SettingsGeneral.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/SettingsGeneral.html @@ -49,15 +49,8 @@
-
@@ -109,90 +102,6 @@ -
-
- -
-
-
- -
-
- -
-
-
-
-
-
-
-
-
-
-
- - -
+
+
+ +
+
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+ + +
diff --git a/tasks/config.js b/tasks/config.js index 01bd89b2a..b407e6457 100644 --- a/tasks/config.js +++ b/tasks/config.js @@ -65,6 +65,7 @@ config.paths.js = { // 'vendors/knockout/build/output/knockout-latest.debug.js', 'vendors/squire/build/squire-raw.js', 'vendors/mathiasbynens/punycode.js', + 'vendors/turndown/turndown.js', 'dev/External/SquireUI.js' ] }, From c58b89d74ab56f8f3bf5575a86e1c940e9a4e330 Mon Sep 17 00:00:00 2001 From: hguilbert <51283484+hguilbert@users.noreply.github.com> Date: Sat, 14 Sep 2024 14:59:30 +0200 Subject: [PATCH 03/57] Update user.json --- snappymail/v/0.0.0/app/localization/fr/user.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snappymail/v/0.0.0/app/localization/fr/user.json b/snappymail/v/0.0.0/app/localization/fr/user.json index 8daeb8aa0..b8999ff2e 100644 --- a/snappymail/v/0.0.0/app/localization/fr/user.json +++ b/snappymail/v/0.0.0/app/localization/fr/user.json @@ -500,7 +500,7 @@ "IMAGES_WHITELIST": "Liste blanche Images", "MESSAGE_POPUP_WINDOW": "Popup dans une nouvelle fenêtre au lieu d'un onglet", "MAILTO": "Inscrivez-vous en tant que gestionnaire de liens 'mailto:'", - "HTML_TO_MD": "Convert HTML to Markdown instead of plain text" + "HTML_TO_MD": "Convertir HTML en Markdown au lieu du texte brut" }, "SETTINGS_CONTACTS": { "LABEL_CONTACTS_AUTOSAVE": "Ajouter automatiquement les destinataires à votre carnet d'adresses", From 947ce59eb9df42d7af5a7e30a24f377d06afa56a Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Sun, 15 Sep 2024 12:56:07 +0200 Subject: [PATCH 04/57] RTL language improvement from #1744 by @rezaei92 --- .../v/0.0.0/app/templates/Views/User/MailMessageView.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snappymail/v/0.0.0/app/templates/Views/User/MailMessageView.html b/snappymail/v/0.0.0/app/templates/Views/User/MailMessageView.html index 689530966..1b5742244 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/MailMessageView.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/MailMessageView.html @@ -330,7 +330,7 @@
-
+
From deae36d8c83f82de9eedd8e4b02b3fb0c903c495 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Sun, 15 Sep 2024 12:59:58 +0200 Subject: [PATCH 05/57] Use Squire 2.3.2 from https://github.com/the-djmaze/Squire/commits/snappymail/ --- vendors/squire/build/squire-raw.js | 3348 +++++++++++++++++----------- 1 file changed, 2072 insertions(+), 1276 deletions(-) diff --git a/vendors/squire/build/squire-raw.js b/vendors/squire/build/squire-raw.js index cecf71428..0f824dae1 100644 --- a/vendors/squire/build/squire-raw.js +++ b/vendors/squire/build/squire-raw.js @@ -1,19 +1,11 @@ -/* Copyright © 2011-2015 by Neil Jenkins. MIT Licensed. */ -/* eslint max-len: 0 */ - -/** - TODO: modifyBlocks function doesn't work very good. - For example you have: UL > LI > [cursor here in text] - Then create blockquote at cursor, the result is: BLOCKQUOTE > UL > LI - not UL > LI > BLOCKQUOTE -*/ - +"use strict"; (() => { // source/node/TreeIterator.ts var SHOW_ELEMENT = 1; var SHOW_TEXT = 4; var SHOW_ELEMENT_OR_TEXT = 5; - var filterAccept = NodeFilter.FILTER_ACCEPT; + + // source/node/TreeWalker.ts TreeWalker.prototype.previousPONode = function() { let current = this.currentNode; let node = current.lastChild; @@ -29,9 +21,13 @@ node && (this.currentNode = node); return node; }; - var createTreeWalker = (root, whatToShow, filter) => document.createTreeWalker(root, whatToShow, filter ? { - acceptNode: (node) => filter(node) ? filterAccept : NodeFilter.FILTER_SKIP - } : null); + var createTreeWalker = (root, whatToShow, filter) => document.createTreeWalker( + root, + whatToShow, + filter ? { + acceptNode: (node) => filter(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP + } : null + ); // source/Constants.ts var ELEMENT_NODE = 1; @@ -40,24 +36,28 @@ var ZWS = "\u200B"; var ua = navigator.userAgent; var isMac = /Mac OS X/.test(ua); - var isIOS = /iP(?:ad|hone)/.test(ua) || (isMac && !!navigator.maxTouchPoints); + var isWin = /Windows NT/.test(ua); + var isIOS = /iP(?:ad|hone)/.test(ua) || isMac && !!navigator.maxTouchPoints; var isAndroid = /Android/.test(ua); var isWebKit = /WebKit\//.test(ua); var ctrlKey = isMac || isIOS ? "Meta-" : "Ctrl-"; var cantFocusEmptyTextNodes = isWebKit; var notWS = /[^ \t\r\n]/; - var indexOf = (array, value) => Array.prototype.indexOf.call(array, value); // source/node/Category.ts - var inlineNodeNames = /^(?:#text|A|ABBR|ACRONYM|B|BR|BD[IO]|CITE|CODE|DATA|DEL|DFN|EM|FONT|HR|I|IMG|INPUT|INS|KBD|Q|RP|RT|RUBY|S|SAMP|SMALL|SPAN|STR(IKE|ONG)|SU[BP]|TIME|U|VAR|WBR)$/; - var leafNodeNames = new Set(["BR", "HR", "IMG"]); - var listNodeNames = new Set(["OL", "UL"]); + var inlineNodeNames = /^(?:#text|A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:ATA|EL|FN)|EM|FONT|HR|I(?:FRAME|MG|NPUT|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:AMP|MALL|PAN|TR(?:IKE|ONG)|U[BP])?|TIME|U|VAR|WBR)$/; + var leafNodeNames = /* @__PURE__ */ new Set(["BR", "HR", "IFRAME", "IMG", "INPUT"]); var UNKNOWN = 0; var INLINE = 1; var BLOCK = 2; var CONTAINER = 3; - var cache = new WeakMap(); - var isLeaf = (node) => isElement(node) && leafNodeNames.has(node.nodeName); + var cache = /* @__PURE__ */ new WeakMap(); + var resetNodeCategoryCache = () => { + cache = /* @__PURE__ */ new WeakMap(); + }; + var isLeaf = (node) => { + return leafNodeNames.has(node.nodeName); + }; var getNodeCategory = (node) => { switch (node.nodeType) { case TEXT_NODE: @@ -71,13 +71,26 @@ default: return UNKNOWN; } - let nodeCategory = Array.prototype.every.call(node.childNodes, isInline) ? (inlineNodeNames.test(node.nodeName) ? INLINE : BLOCK) : CONTAINER; + let nodeCategory; + if (!Array.from(node.childNodes).every(isInline)) { + nodeCategory = CONTAINER; + } else if (inlineNodeNames.test(node.nodeName)) { + nodeCategory = INLINE; + } else { + nodeCategory = BLOCK; + } cache.set(node, nodeCategory); return nodeCategory; }; - var isInline = (node) => getNodeCategory(node) === INLINE; - var isBlock = (node) => getNodeCategory(node) === BLOCK; - var isContainer = (node) => getNodeCategory(node) === CONTAINER; + var isInline = (node) => { + return getNodeCategory(node) === INLINE; + }; + var isBlock = (node) => { + return getNodeCategory(node) === BLOCK; + }; + var isContainer = (node) => { + return getNodeCategory(node) === CONTAINER; + }; // source/node/Node.ts var createElement = (tag, props, children) => { @@ -86,12 +99,42 @@ children = props; props = null; } - setAttributes(el, props); - children && el.append(...children); + if (props) { + for (const attr in props) { + const value = props[attr]; + if (value !== void 0) { + el.setAttribute(attr, value); + } + } + } + if (children) { + children.forEach((node) => el.append(node)); + } return el; }; - var areAlike = (node, node2) => !isLeaf(node) && (node.nodeType === node2.nodeType && node.nodeName === node2.nodeName && node.nodeName !== "A" && node.className === node2.className && node.style?.cssText === node2.style?.cssText); - var hasTagAttributes = (node, tag, attributes) => node.nodeName === tag && Object.entries(attributes || {}).every(([k,v]) => node.getAttribute(k) === v); + var areAlike = (node, node2) => { + if (isLeaf(node)) { + return false; + } + if (node.nodeType !== node2.nodeType || node.nodeName !== node2.nodeName) { + return false; + } + if (node instanceof HTMLElement && node2 instanceof HTMLElement) { + return node.nodeName !== "A" && node.className === node2.className && node.style.cssText === node2.style.cssText; + } + return true; + }; + var hasTagAttributes = (node, tag, attributes) => { + if (node.nodeName !== tag) { + return false; + } + for (const attr in attributes) { + if (!("getAttribute" in node) || node.getAttribute(attr) !== attributes[attr]) { + return false; + } + } + return true; + }; var getNearest = (node, root, tag, attributes) => { while (node && node !== root) { if (hasTagAttributes(node, tag, attributes)) { @@ -103,7 +146,7 @@ }; var getNodeBeforeOffset = (node, offset) => { let children = node.childNodes; - while (offset && isElement(node)) { + while (offset && node instanceof Element) { node = children[offset - 1]; children = node.childNodes; offset = children.length; @@ -111,49 +154,60 @@ return node; }; var getNodeAfterOffset = (node, offset) => { - if (isElement(node)) { - const children = node.childNodes; + let returnNode = node; + if (returnNode instanceof Element) { + const children = returnNode.childNodes; if (offset < children.length) { - node = children[offset]; + returnNode = children[offset]; } else { - while (node && !node.nextSibling) { - node = node.parentNode; + while (returnNode && !returnNode.nextSibling) { + returnNode = returnNode.parentNode; + } + if (returnNode) { + returnNode = returnNode.nextSibling; } - node && (node = node.nextSibling); } } + return returnNode; + }; + var getLength = (node) => { + return node instanceof Element || node instanceof DocumentFragment ? node.childNodes.length : node instanceof CharacterData ? node.length : 0; + }; + var empty = (node) => { + const frag = document.createDocumentFragment(); + let child = node.firstChild; + while (child) { + frag.append(child); + child = node.firstChild; + } + return frag; + }; + var detach = (node) => { + const parent = node.parentNode; + if (parent) { + parent.removeChild(node); + } return node; }; - var getLength = (node) => isElement(node) || node instanceof DocumentFragment ? node.childNodes.length : node.length || 0; - var empty = (node) => { - const frag = document.createDocumentFragment(), childNodes = node.childNodes; - childNodes && frag.append(...childNodes); - return frag; + var replaceWith = (node, node2) => { + const parent = node.parentNode; + if (parent) { + parent.replaceChild(node2, node); + } }; - var detach = (node) => node.parentNode?.removeChild(node); - var replaceWith = (node, node2) => node.parentNode?.replaceChild(node2, node); var getClosest = (node, root, selector) => { - node = (node && !node.closest) ? node.parentElement : node; - node = node?.closest(selector); - return (node && root.contains(node)) ? node : null; - }; - var isElement = (node) => node instanceof Element; - var isTextNode = (node) => node instanceof Text; - var isBrElement = (node) => "BR" === node?.nodeName; - var setAttributes = (node, props) => { - props && Object.entries(props).forEach(([k, v]) => { - if (null == v) { - node.removeAttribute(k); - } else if ("style" === k && typeof v === "object") { - Object.entries(v).forEach(([k2, v2]) => node.style[k2] = v2); - } else { - node.setAttribute(k, v); - } - }); + var _a; + node = (_a = node && !node.closest ? node.parentElement : node) == null ? void 0 : _a.closest(selector); + return node && root.contains(node) ? node : null; }; // source/node/Whitespace.ts - var notWSTextNode = (node) => isElement(node) ? isBrElement(node) : notWS.test(node.data); + var notWSTextNode = (node) => { + return node instanceof Element ? node.nodeName === "BR" : ( + // okay if data is 'undefined' here. + notWS.test(node.data) + ); + }; var isLineBreak = (br, isLBIfEmptyBlock) => { let block = br.parentNode; while (isInline(block)) { @@ -165,21 +219,27 @@ notWSTextNode ); walker.currentNode = br; - return !!walker.nextNode() || (isLBIfEmptyBlock && !walker.previousNode()); + return !!walker.nextNode() || isLBIfEmptyBlock && !walker.previousNode(); }; var removeZWS = (root, keepNode) => { const walker = createTreeWalker(root, SHOW_TEXT); let textNode; let index; while (textNode = walker.nextNode()) { - while ((index = textNode.data.indexOf(ZWS)) > -1 && (!keepNode || textNode.parentNode !== keepNode)) { + while ((index = textNode.data.indexOf(ZWS)) > -1 && // eslint-disable-next-line no-unmodified-loop-condition + (!keepNode || textNode.parentNode !== keepNode)) { if (textNode.length === 1) { - do { - let parent = textNode.parentNode; - textNode.remove(); - textNode = parent; + let node = textNode; + let parent = node.parentNode; + while (parent) { + parent.removeChild(node); walker.currentNode = parent; - } while (isInline(textNode) && !getLength(textNode)); + if (!isInline(parent) || getLength(parent)) { + break; + } + node = parent; + parent = node.parentNode; + } break; } else { textNode.deleteData(index, 1); @@ -193,30 +253,35 @@ var START_TO_END = 1; var END_TO_END = 2; var END_TO_START = 3; - var isNodeContainedInRange = (range, node, partial = true) => { + var isNodeContainedInRange = (range, node, partial) => { const nodeRange = document.createRange(); nodeRange.selectNode(node); - return partial - ? range.compareBoundaryPoints(END_TO_START, nodeRange) < 0 - && range.compareBoundaryPoints(START_TO_END, nodeRange) > 0 - : range.compareBoundaryPoints(START_TO_START, nodeRange) < 1 - && range.compareBoundaryPoints(END_TO_END, nodeRange) > -1; + if (partial) { + const nodeEndBeforeStart = range.compareBoundaryPoints(END_TO_START, nodeRange) > -1; + const nodeStartAfterEnd = range.compareBoundaryPoints(START_TO_END, nodeRange) < 1; + return !nodeEndBeforeStart && !nodeStartAfterEnd; + } else { + const nodeStartAfterStart = range.compareBoundaryPoints(START_TO_START, nodeRange) < 1; + const nodeEndBeforeEnd = range.compareBoundaryPoints(END_TO_END, nodeRange) > -1; + return nodeStartAfterStart && nodeEndBeforeEnd; + } }; - var moveRangeBoundariesDownTree = range => { + var moveRangeBoundariesDownTree = (range) => { let { startContainer, startOffset, endContainer, endOffset } = range; - while (!isTextNode(startContainer)) { + while (!(startContainer instanceof Text)) { let child = startContainer.childNodes[startOffset]; if (!child || isLeaf(child)) { if (startOffset) { child = startContainer.childNodes[startOffset - 1]; - if (isTextNode(child)) { + if (child instanceof Text) { + let textChild = child; let prev; - while (!child.length && (prev = child.previousSibling) && isTextNode(prev)) { - child.remove(); - child = prev; + while (!textChild.length && (prev = textChild.previousSibling) && prev instanceof Text) { + textChild.remove(); + textChild = prev; } - startContainer = child; - startOffset = child.data.length; + startContainer = textChild; + startOffset = textChild.data.length; } } break; @@ -225,11 +290,11 @@ startOffset = 0; } if (endOffset) { - while (!isTextNode(endContainer)) { + while (!(endContainer instanceof Text)) { const child = endContainer.childNodes[endOffset - 1]; if (!child || isLeaf(child)) { - if (isBrElement(child) && !isLineBreak(child)) { - --endOffset; + if (child && child.nodeName === "BR" && !isLineBreak(child, false)) { + endOffset -= 1; continue; } break; @@ -238,7 +303,7 @@ endOffset = getLength(endContainer); } } else { - while (!isTextNode(endContainer)) { + while (!(endContainer instanceof Text)) { const child = endContainer.firstChild; if (!child || isLeaf(child)) { break; @@ -263,18 +328,23 @@ } while (!startOffset && startContainer !== startMax && startContainer !== root) { parent = startContainer.parentNode; - startOffset = indexOf(parent.childNodes, startContainer); + startOffset = Array.from(parent.childNodes).indexOf( + startContainer + ); startContainer = parent; } - while (endContainer !== endMax && endContainer !== root) { - if (!isTextNode(endContainer) && isBrElement(endContainer.childNodes[endOffset]) && !isLineBreak(endContainer.childNodes[endOffset])) { - ++endOffset; + while (true) { + if (endContainer === endMax || endContainer === root) { + break; + } + if (endContainer.nodeType !== TEXT_NODE && endContainer.childNodes[endOffset] && endContainer.childNodes[endOffset].nodeName === "BR" && !isLineBreak(endContainer.childNodes[endOffset], false)) { + endOffset += 1; } if (endOffset !== getLength(endContainer)) { break; } parent = endContainer.parentNode; - endOffset = indexOf(parent.childNodes, endContainer) + 1; + endOffset = Array.from(parent.childNodes).indexOf(endContainer) + 1; endContainer = parent; } range.setStart(startContainer, startOffset); @@ -296,55 +366,73 @@ // source/node/MergeSplit.ts var fixCursor = (node) => { let fixer = null; - if (!isTextNode(node)) { - if (isInline(node)) { - let child = node.firstChild; - if (cantFocusEmptyTextNodes) { - while (isTextNode(child) && !child.data) { - node.removeChild(child); - child = node.firstChild; - } + if (node instanceof Text) { + return node; + } + if (isInline(node)) { + let child = node.firstChild; + if (cantFocusEmptyTextNodes) { + while (child && child instanceof Text && !child.data) { + node.removeChild(child); + child = node.firstChild; } - if (!child) { - fixer = document.createTextNode(cantFocusEmptyTextNodes ? ZWS : ""); - } - } else if (isElement(node) && !node.querySelector("BR")) { - fixer = createElement("BR"); } - if (fixer) { - try { - node.appendChild(fixer); - } catch (error) { - didError({ - name: 'Squire: fixCursor – ' + error, - message: 'Parent: ' + node.nodeName + '/' + node.innerHTML + - ' appendChild: ' + fixer.nodeName - }); + if (!child) { + if (cantFocusEmptyTextNodes) { + fixer = document.createTextNode(ZWS); + } else { + fixer = document.createTextNode(""); } } + } else if ((node instanceof Element || node instanceof DocumentFragment) && !node.querySelector("BR")) { + fixer = createElement("BR"); + let parent = node; + let child; + while ((child = parent.lastElementChild) && !isInline(child)) { + parent = child; + } + node = parent; + } + if (fixer) { + try { + node.appendChild(fixer); + } catch (error) { + } } return node; }; var fixContainer = (container, root) => { - let wrapper; - [...container.childNodes].forEach((child) => { - const isBR = isBrElement(child); - if (!isBR && child.parentNode == root && isInline(child)) { - wrapper = wrapper || createElement("DIV"); + let wrapper = null; + Array.from(container.childNodes).forEach((child) => { + const isBR = child.nodeName === "BR"; + if (!isBR && isInline(child)) { + if (!wrapper) { + wrapper = createElement("DIV"); + } wrapper.append(child); } else if (isBR || wrapper) { - wrapper = wrapper || createElement("DIV"); + if (!wrapper) { + wrapper = createElement("DIV"); + } fixCursor(wrapper); - child[isBR ? "replaceWith" : "before"](wrapper); + if (isBR) { + container.replaceChild(wrapper, child); + } else { + container.insertBefore(wrapper, child); + } wrapper = null; } - isContainer(child) && fixContainer(child, root); + if (isContainer(child)) { + fixContainer(child, root); + } }); - wrapper && container.append(fixCursor(wrapper)); + if (wrapper) { + container.append(fixCursor(wrapper)); + } return container; }; var split = (node, offset, stopNode, root) => { - if (isTextNode(node) && node !== stopNode) { + if (node instanceof Text && node !== stopNode) { if (typeof offset !== "number") { throw new Error("Offset must be a number to split text node!"); } @@ -355,7 +443,7 @@ } let nodeAfterSplit = typeof offset === "number" ? offset < node.childNodes.length ? node.childNodes[offset] : null : offset; const parent = node.parentNode; - if (!parent || node === stopNode || !isElement(node)) { + if (!parent || node === stopNode || !(node instanceof Element)) { return nodeAfterSplit; } const clone = node.cloneNode(false); @@ -369,17 +457,17 @@ } fixCursor(node); fixCursor(clone); - node.after(clone); + parent.insertBefore(clone, node.nextSibling); return split(parent, clone, stopNode, root); }; var _mergeInlines = (node, fakeRange) => { const children = node.childNodes; let l = children.length; - let frags = []; + const frags = []; while (l--) { const child = children[l]; - const prev = l && children[l - 1]; - if (prev && isInline(child) && areAlike(child, prev)/* && !leafNodeNames.has(child.nodeName)*/) { + const prev = l ? children[l - 1] : null; + if (prev && isInline(child) && areAlike(child, prev)) { if (fakeRange.startContainer === child) { fakeRange.startContainer = prev; fakeRange.startOffset += getLength(prev); @@ -390,7 +478,7 @@ } if (fakeRange.startContainer === node) { if (fakeRange.startOffset > l) { - --fakeRange.startOffset; + fakeRange.startOffset -= 1; } else if (fakeRange.startOffset === l) { fakeRange.startContainer = prev; fakeRange.startOffset = getLength(prev); @@ -398,35 +486,37 @@ } if (fakeRange.endContainer === node) { if (fakeRange.endOffset > l) { - --fakeRange.endOffset; + fakeRange.endOffset -= 1; } else if (fakeRange.endOffset === l) { fakeRange.endContainer = prev; fakeRange.endOffset = getLength(prev); } } detach(child); - if (isTextNode(child)) { + if (child instanceof Text) { prev.appendData(child.data); } else { - frags.unshift(empty(child)); + frags.push(empty(child)); + } + } else if (child instanceof Element) { + let frag; + while (frag = frags.pop()) { + child.append(frag); } - } else if (isElement(child)) { - child.append(...frags); - frags = []; _mergeInlines(child, fakeRange); } } }; var mergeInlines = (node, range) => { - node = isTextNode(node) ? node.parentNode : node; - if (isElement(node)) { + const element = node instanceof Text ? node.parentNode : node; + if (element instanceof Element) { const fakeRange = { startContainer: range.startContainer, startOffset: range.startOffset, endContainer: range.endContainer, endOffset: range.endOffset }; - _mergeInlines(node, fakeRange); + _mergeInlines(element, fakeRange); range.setStart(fakeRange.startContainer, fakeRange.startOffset); range.setEnd(fakeRange.endContainer, fakeRange.endOffset); } @@ -435,15 +525,15 @@ let container = next; let parent; let offset; - while ((parent = container.parentNode) && parent !== root && isElement(parent) && parent.childNodes.length === 1) { + while ((parent = container.parentNode) && parent !== root && parent instanceof Element && parent.childNodes.length === 1) { container = parent; } detach(container); offset = block.childNodes.length; const last = block.lastChild; - if (isBrElement(last)) { - last.remove(); - --offset; + if (last && last.nodeName === "BR") { + block.removeChild(last); + offset -= 1; } block.append(empty(next)); range.setStart(block, offset); @@ -454,23 +544,28 @@ const prev = node.previousSibling; const first = node.firstChild; const isListItem = node.nodeName === "LI"; - if (isListItem && (!first || !listNodeNames.has(first.nodeName))) { + if (isListItem && (!first || !/^[OU]L$/.test(first.nodeName))) { return; } if (prev && areAlike(prev, node)) { if (!isContainer(prev)) { - if (!isListItem) { + if (isListItem) { + const block = createElement("DIV"); + block.append(empty(prev)); + prev.append(block); + } else { return; } - const block = createElement("DIV"); - block.append(empty(prev)); - prev.append(block); } detach(node); const needsFix = !isContainer(node); prev.append(empty(node)); - needsFix && fixContainer(prev, root); - first && mergeContainers(first, root); + if (needsFix) { + fixContainer(prev, root); + } + if (first) { + mergeContainers(first, root); + } } else if (isListItem) { const block = createElement("DIV"); node.insertBefore(block, first); @@ -480,57 +575,65 @@ // source/Clean.ts var styleToSemantic = { - fontWeight: { + "font-weight": { regexp: /^bold|^700/i, - replace: () => createElement("B") + replace() { + return createElement("B"); + } }, - fontStyle: { + "font-style": { regexp: /^italic/i, - replace: () => createElement("I") + replace() { + return createElement("I"); + } }, - fontFamily: { + "font-family": { regexp: notWS, - replace: (family) => createElement("SPAN", { - style: "font-family:" + family - }) + replace(classNames, family) { + return createElement("SPAN", { + class: classNames.fontFamily, + style: "font-family:" + family + }); + } }, - fontSize: { + "font-size": { regexp: notWS, - replace: (size) => createElement("SPAN", { - style: "font-size:" + size - }) + replace(classNames, size) { + return createElement("SPAN", { + class: classNames.fontSize, + style: "font-size:" + size + }); + } }, - textDecoration: { + "text-decoration": { regexp: /^underline/i, - replace: () => createElement("U") + replace() { + return createElement("U"); + } } - /* - textDecoration: { - regexp: /^line-through/i, - replace: doc => createElement("S") - } - */ }; - var replaceStyles = (node) => { + var replaceStyles = (node, _, config) => { const style = node.style; let newTreeBottom; let newTreeTop; - Object.entries(styleToSemantic).forEach(([attr, converter]) => { - const css = style[attr]; + for (const attr in styleToSemantic) { + const converter = styleToSemantic[attr]; + const css = style.getPropertyValue(attr); if (css && converter.regexp.test(css)) { - const el = converter.replace(css); - if (el.nodeName !== node.nodeName || el.className !== node.className) { - if (!newTreeTop) { - newTreeTop = el; - } - if (newTreeBottom) { - newTreeBottom.append(el); - } - newTreeBottom = el; - node.style.removeProperty(attr); + const el = converter.replace(config.classNames, css); + if (el.nodeName === node.nodeName && el.className === node.className) { + continue; } + if (!newTreeTop) { + newTreeTop = el; + } + if (newTreeBottom) { + newTreeBottom.append(el); + } + newTreeBottom = el; + node.style.removeProperty(attr); } - }); + } if (newTreeTop && newTreeBottom) { newTreeBottom.append(empty(node)); if (node.style.cssText) { @@ -541,23 +644,27 @@ } return newTreeBottom || node; }; - var replaceWithTag = (tag) => (node) => { - const el = createElement(tag); - Array.prototype.forEach.call(node.attributes, (attr) => el.setAttribute(attr.name, attr.value)); - replaceWith(node, el); - el.append(empty(node)); - return el; + var replaceWithTag = (tag) => { + return (node, parent) => { + const el = createElement(tag); + const attributes = node.attributes; + for (let i = 0, l = attributes.length; i < l; i += 1) { + const attribute = attributes[i]; + el.setAttribute(attribute.name, attribute.value); + } + parent.replaceChild(el, node); + el.append(empty(node)); + return el; + }; }; var fontSizes = { - 1: "x-small", - 2: "small", - 3: "medium", - 4: "large", - 5: "x-large", - 6: "xx-large", - 7: "xxx-large", - "-1": "smaller", - "+1": "larger" + "1": "10", + "2": "13", + "3": "16", + "4": "18", + "5": "24", + "6": "32", + "7": "48" }; var stylesRewriters = { STRONG: replaceWithTag("B"), @@ -565,85 +672,118 @@ INS: replaceWithTag("U"), STRIKE: replaceWithTag("S"), SPAN: replaceStyles, - FONT: (node) => { - const face = node.face; - const size = node.size; - let color = node.color; - let newTag = createElement("SPAN"); - let css = newTag.style; - newTag.style.cssText = node.style.cssText; + FONT: (node, parent, config) => { + const font = node; + const face = font.face; + const size = font.size; + let color = font.color; + const classNames = config.classNames; + let fontSpan; + let sizeSpan; + let colorSpan; + let newTreeBottom; + let newTreeTop; if (face) { - css.fontFamily = face; + fontSpan = createElement("SPAN", { + class: classNames.fontFamily, + style: "font-family:" + face + }); + newTreeTop = fontSpan; + newTreeBottom = fontSpan; } if (size) { - css.fontSize = fontSizes[size]; + sizeSpan = createElement("SPAN", { + class: classNames.fontSize, + style: "font-size:" + fontSizes[size] + "px" + }); + if (!newTreeTop) { + newTreeTop = sizeSpan; + } + if (newTreeBottom) { + newTreeBottom.append(sizeSpan); + } + newTreeBottom = sizeSpan; } if (color && /^#?([\dA-F]{3}){1,2}$/i.test(color)) { if (color.charAt(0) !== "#") { color = "#" + color; } - css.color = color; + colorSpan = createElement("SPAN", { + class: classNames.color, + style: "color:" + color + }); + if (!newTreeTop) { + newTreeTop = colorSpan; + } + if (newTreeBottom) { + newTreeBottom.append(colorSpan); + } + newTreeBottom = colorSpan; } - replaceWith(node, newTag); - newTag.append(empty(node)); - return newTag; + if (!newTreeTop || !newTreeBottom) { + newTreeTop = newTreeBottom = createElement("SPAN"); + } + parent.replaceChild(newTreeTop, font); + newTreeBottom.append(empty(font)); + return newTreeBottom; }, - TT: (node) => { + TT: (node, parent, config) => { const el = createElement("SPAN", { + class: config.classNames.fontFamily, style: 'font-family:menlo,consolas,"courier new",monospace' }); - replaceWith(node, el); + parent.replaceChild(el, node); el.append(empty(node)); return el; } }; var allowedBlock = /^(?:A(?:DDRESS|RTICLE|SIDE|UDIO)|BLOCKQUOTE|CAPTION|D(?:[DLT]|IV)|F(?:IGURE|IGCAPTION|OOTER)|H[1-6]|HEADER|L(?:ABEL|EGEND|I)|O(?:L|UTPUT)|P(?:RE)?|SECTION|T(?:ABLE|BODY|D|FOOT|H|HEAD|R)|COL(?:GROUP)?|UL)$/; - var blacklist = new Set(["HEAD", "META", "STYLE"]); - var cleanTree = (node, preserveWS) => { + var blacklist = /^(?:HEAD|META|STYLE)/; + var cleanTree = (node, config, preserveWS) => { const children = node.childNodes; let nonInlineParent = node; while (isInline(nonInlineParent)) { nonInlineParent = nonInlineParent.parentNode; } -/* const walker = createTreeWalker( nonInlineParent, SHOW_ELEMENT_OR_TEXT ); -*/ - let i = children.length; - while (i--) { + for (let i = 0, l = children.length; i < l; i += 1) { let child = children[i]; const nodeName = child.nodeName; - if (isElement(child)) { + const rewriter = stylesRewriters[nodeName]; + if (child instanceof HTMLElement) { const childLength = child.childNodes.length; - if (stylesRewriters[nodeName]) { - child = stylesRewriters[nodeName](child); - } else if (blacklist.has(nodeName)) { - child.remove(); + if (rewriter) { + child = rewriter(child, node, config); + } else if (blacklist.test(nodeName)) { + node.removeChild(child); + i -= 1; + l -= 1; continue; } else if (!allowedBlock.test(nodeName) && !isInline(child)) { - i += childLength; - replaceWith(child, empty(child)); + i -= 1; + l += childLength - 1; + node.replaceChild(empty(child), child); continue; } if (childLength) { - cleanTree(child, preserveWS || (nodeName === "PRE")); + cleanTree(child, config, preserveWS || nodeName === "PRE"); } -/* } else { - if (isTextNode(child)) { + if (child instanceof Text) { let data = child.data; const startsWithWS = !notWS.test(data.charAt(0)); const endsWithWS = !notWS.test(data.charAt(data.length - 1)); - if (preserveWS || (!startsWithWS && !endsWithWS)) { + if (preserveWS || !startsWithWS && !endsWithWS) { continue; } if (startsWithWS) { walker.currentNode = child; let sibling; while (sibling = walker.previousPONode()) { - if (sibling.nodeName === "IMG" || isTextNode(sibling) && notWS.test(sibling.data)) { + if (sibling.nodeName === "IMG" || sibling instanceof Text && notWS.test(sibling.data)) { break; } if (!isInline(sibling)) { @@ -657,7 +797,7 @@ walker.currentNode = child; let sibling; while (sibling = walker.nextNode()) { - if (sibling.nodeName === "IMG" || (isTextNode(sibling) && notWS.test(sibling.data))) { + if (sibling.nodeName === "IMG" || sibling instanceof Text && notWS.test(sibling.data)) { break; } if (!isInline(sibling)) { @@ -673,7 +813,8 @@ } } node.removeChild(child); -*/ + i -= 1; + l -= 1; } } return node; @@ -683,32 +824,39 @@ let l = children.length; while (l--) { const child = children[l]; - if (isElement(child) && !isLeaf(child)) { + if (child instanceof Element && !isLeaf(child)) { removeEmptyInlines(child); - if (!child.firstChild && isInline(child)) { - child.remove(); + if (isInline(child) && !child.firstChild) { + node.removeChild(child); } - } else if (!child.data && isTextNode(child)) { + } else if (child instanceof Text && !child.data) { node.removeChild(child); } } }; var cleanupBRs = (node, root, keepForBlankLine) => { const brs = node.querySelectorAll("BR"); + const brBreaksLine = []; let l = brs.length; + for (let i = 0; i < l; i += 1) { + brBreaksLine[i] = isLineBreak(brs[i], keepForBlankLine); + } while (l--) { const br = brs[l]; const parent = br.parentNode; - if (parent) { - if (!isLineBreak(br, keepForBlankLine)) { - detach(br); - } else if (!isInline(parent)) { - fixContainer(parent, root); - } + if (!parent) { + continue; + } + if (!brBreaksLine[l]) { + detach(br); + } else if (!isInline(parent)) { + fixContainer(parent, root); } } }; - var escapeHTML = (text) => text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """); + var escapeHTML = (text) => { + return text.split("&").join("&").split("<").join("<").split(">").join(">").split('"').join("""); + }; // source/node/Block.ts var getBlockWalker = (node, root) => { @@ -717,14 +865,16 @@ return walker; }; var getPreviousBlock = (node, root) => { - node = getBlockWalker(node, root).previousNode(); - return node !== root ? node : null; + const block = getBlockWalker(node, root).previousNode(); + return block !== root ? block : null; }; var getNextBlock = (node, root) => { - node = getBlockWalker(node, root).nextNode(); - return node !== root ? node : null; + const block = getBlockWalker(node, root).nextNode(); + return block !== root ? block : null; + }; + var isEmptyBlock = (block) => { + return !block.textContent && !block.querySelector("IMG"); }; - var isEmptyBlock = (block) => !block.textContent && !block.querySelector("IMG"); // source/range/Block.ts var getStartBlockOfRange = (range, root) => { @@ -735,9 +885,10 @@ } else if (container !== root && container instanceof HTMLElement && isBlock(container)) { block = container; } else { - block = getNextBlock(getNodeBeforeOffset(container, range.startOffset), root); + const node = getNodeBeforeOffset(container, range.startOffset); + block = getNextBlock(node, root); } - return block && isNodeContainedInRange(range, block) ? block : null; + return block && isNodeContainedInRange(range, block, true) ? block : null; }; var getEndBlockOfRange = (range, root) => { const container = range.endContainer; @@ -757,49 +908,55 @@ } block = getPreviousBlock(node, root); } - return block && isNodeContainedInRange(range, block) ? block : null; + return block && isNodeContainedInRange(range, block, true) ? block : null; + }; + var isContent = (node) => { + return node instanceof Text ? notWS.test(node.data) : node.nodeName === "IMG"; }; - var createContentWalker = (root) => createTreeWalker( - root, - SHOW_ELEMENT_OR_TEXT, - (node) => isTextNode(node) ? notWS.test(node.data) : node.nodeName === "IMG" - ); var rangeDoesStartAtBlockBoundary = (range, root) => { const startContainer = range.startContainer; const startOffset = range.startOffset; let nodeAfterCursor; - if (isTextNode(startContainer)) { - let i = startOffset; - while (i--) { - if (startContainer.data.charAt(i) !== ZWS) { + if (startContainer instanceof Text) { + const text = startContainer.data; + for (let i = startOffset; i > 0; i -= 1) { + if (text.charAt(i - 1) !== ZWS) { return false; } } nodeAfterCursor = startContainer; } else { nodeAfterCursor = getNodeAfterOffset(startContainer, startOffset); - if (!nodeAfterCursor || !root.contains(nodeAfterCursor)) { + if (nodeAfterCursor && !root.contains(nodeAfterCursor)) { + nodeAfterCursor = null; + } + if (!nodeAfterCursor) { nodeAfterCursor = getNodeBeforeOffset(startContainer, startOffset); - if (isTextNode(nodeAfterCursor) && nodeAfterCursor.length) { + if (nodeAfterCursor instanceof Text && nodeAfterCursor.length) { return false; } } } const block = getStartBlockOfRange(range, root); - if (block) { - const contentWalker = createContentWalker(block); - contentWalker.currentNode = nodeAfterCursor; - return !contentWalker.previousNode(); + if (!block) { + return false; } + const contentWalker = createTreeWalker( + block, + SHOW_ELEMENT_OR_TEXT, + isContent + ); + contentWalker.currentNode = nodeAfterCursor; + return !contentWalker.previousNode(); }; var rangeDoesEndAtBlockBoundary = (range, root) => { const endContainer = range.endContainer; const endOffset = range.endOffset; let currentNode; - if (isTextNode(endContainer)) { + if (endContainer instanceof Text) { const text = endContainer.data; const length = text.length; - for (let i = endOffset; i < length; ++i) { + for (let i = endOffset; i < length; i += 1) { if (text.charAt(i) !== ZWS) { return false; } @@ -809,23 +966,31 @@ currentNode = getNodeBeforeOffset(endContainer, endOffset); } const block = getEndBlockOfRange(range, root); - if (block) { - const contentWalker = createContentWalker(block); - contentWalker.currentNode = currentNode; - return !contentWalker.nextNode(); + if (!block) { + return false; } + const contentWalker = createTreeWalker( + block, + SHOW_ELEMENT_OR_TEXT, + isContent + ); + contentWalker.currentNode = currentNode; + return !contentWalker.nextNode(); }; var expandRangeToBlockBoundaries = (range, root) => { const start = getStartBlockOfRange(range, root); const end = getEndBlockOfRange(range, root); + let parent; if (start && end) { - range.setStart(start, 0); - range.setEnd(end, end.childNodes.length); + parent = start.parentNode; + range.setStart(parent, Array.from(parent.childNodes).indexOf(start)); + parent = end.parentNode; + range.setEnd(parent, Array.from(parent.childNodes).indexOf(end) + 1); } }; // source/range/InsertDelete.ts - var createRange = (startContainer, startOffset, endContainer, endOffset) => { + function createRange(startContainer, startOffset, endContainer, endOffset) { const range = document.createRange(); range.setStart(startContainer, startOffset); if (endContainer && typeof endOffset === "number") { @@ -834,15 +999,15 @@ range.setEnd(startContainer, startOffset); } return range; - }; + } var insertNodeInRange = (range, node) => { let { startContainer, startOffset, endContainer, endOffset } = range; let children; - if (isTextNode(startContainer)) { + if (startContainer instanceof Text) { const parent = startContainer.parentNode; children = parent.childNodes; if (startOffset === startContainer.length) { - startOffset = indexOf(children, startContainer) + 1; + startOffset = Array.from(children).indexOf(startContainer) + 1; if (range.collapsed) { endContainer = parent; endOffset = startOffset; @@ -854,11 +1019,13 @@ endOffset -= startOffset; endContainer = afterSplit; } else if (endContainer === parent) { - ++endOffset; + endOffset += 1; } startContainer = afterSplit; } - startOffset = indexOf(children, startContainer); + startOffset = Array.from(children).indexOf( + startContainer + ); } startContainer = parent; } else { @@ -877,30 +1044,39 @@ range.setEnd(endContainer, endOffset); }; var extractContentsOfRange = (range, common, root) => { - common = common || range.commonAncestorContainer; - if (isTextNode(common)) { + const frag = document.createDocumentFragment(); + if (range.collapsed) { + return frag; + } + if (!common) { + common = range.commonAncestorContainer; + } + if (common instanceof Text) { common = common.parentNode; } - let endNode = split(range.endContainer, range.endOffset, common, root), - frag = range.extractContents(), - startContainer = common, - startOffset = endNode ? indexOf(common.childNodes, endNode) : common.childNodes.length, - after = common.childNodes[startOffset], - before = after?.previousSibling, - beforeText, afterText; - if (isTextNode(before) && isTextNode(after)) { - startContainer = before; - startOffset = before.length; - beforeText = before.data; - afterText = after.data; - if (beforeText.charAt(beforeText.length - 1) === ' ' && afterText.charAt(0) === ' ') { - afterText = NBSP + afterText.slice(1); - } - before.appendData(afterText); - detach(after); + const startContainer = range.startContainer; + const startOffset = range.startOffset; + let endContainer = split(range.endContainer, range.endOffset, common, root); + let endOffset = 0; + let node = split(startContainer, startOffset, common, root); + while (node && node !== endContainer) { + const next = node.nextSibling; + frag.append(node); + node = next; + } + node = endContainer && endContainer.previousSibling; + if (node && node instanceof Text && endContainer instanceof Text) { + endOffset = node.length; + node.appendData(endContainer.data); + detach(endContainer); + endContainer = node; } range.setStart(startContainer, startOffset); - range.collapse(true); + if (endContainer) { + range.setEnd(endContainer, endOffset); + } else { + range.setEnd(common, common.childNodes.length); + } fixCursor(common); return frag; }; @@ -908,7 +1084,7 @@ iterator.currentNode = node; let nextNode; while (nextNode = iterator[method]()) { - if (isTextNode(nextNode) || isLeaf(nextNode)) { + if (nextNode instanceof Text || isLeaf(nextNode)) { return nextNode; } if (!isInline(nextNode)) { @@ -937,9 +1113,11 @@ fixCursor(startBlock); } const child = root.firstChild; - if (!child || isBrElement(child)) { + if (!child || child.nodeName === "BR") { fixCursor(root); - root.firstChild && range.selectNodeContents(root.firstChild); + if (root.firstChild) { + range.selectNodeContents(root.firstChild); + } } range.collapse(true); const startContainer = range.startContainer; @@ -947,41 +1125,43 @@ const iterator = createTreeWalker(root, SHOW_ELEMENT_OR_TEXT); let afterNode = startContainer; let afterOffset = startOffset; - if (!isTextNode(afterNode) || afterOffset === afterNode.data.length) { + if (!(afterNode instanceof Text) || afterOffset === afterNode.data.length) { afterNode = getAdjacentInlineNode(iterator, "nextNode", afterNode); afterOffset = 0; } let beforeNode = startContainer; let beforeOffset = startOffset - 1; - if (!isTextNode(beforeNode) || beforeOffset === -1) { + if (!(beforeNode instanceof Text) || beforeOffset === -1) { beforeNode = getAdjacentInlineNode( iterator, "previousPONode", - afterNode || (isTextNode(startContainer) ? startContainer : startContainer.childNodes[startOffset] || startContainer) + afterNode || (startContainer instanceof Text ? startContainer : startContainer.childNodes[startOffset] || startContainer) ); - if (isTextNode(beforeNode)) { + if (beforeNode instanceof Text) { beforeOffset = beforeNode.data.length; } } let node = null; let offset = 0; - if (isTextNode(afterNode) && afterNode.data.charAt(afterOffset) === " " && rangeDoesStartAtBlockBoundary(range, root)) { + if (afterNode instanceof Text && afterNode.data.charAt(afterOffset) === " " && rangeDoesStartAtBlockBoundary(range, root)) { node = afterNode; offset = afterOffset; - } else if (isTextNode(beforeNode) && beforeNode.data.charAt(beforeOffset) === " ") { - if (isTextNode(afterNode) && afterNode.data.charAt(afterOffset) === " " || rangeDoesEndAtBlockBoundary(range, root)) { + } else if (beforeNode instanceof Text && beforeNode.data.charAt(beforeOffset) === " ") { + if (afterNode instanceof Text && afterNode.data.charAt(afterOffset) === " " || rangeDoesEndAtBlockBoundary(range, root)) { node = beforeNode; offset = beforeOffset; } } - node && node.replaceData(offset, 1, "\xA0"); + if (node) { + node.replaceData(offset, 1, "\xA0"); + } range.setStart(startContainer, startOffset); range.collapse(true); return frag; }; var insertTreeFragmentIntoRange = (range, frag, root) => { const firstInFragIsInline = frag.firstChild && isInline(frag.firstChild); - let node, blockContentsAfterSplit; + let node; fixContainer(frag, root); node = frag; while (node = getNextBlock(node, root)) { @@ -991,13 +1171,14 @@ deleteContentsOfRange(range, root); } moveRangeBoundariesDownTree(range); - range.collapse(); + range.collapse(false); const stopPoint = getClosest(range.endContainer, root, "BLOCKQUOTE") || root; let block = getStartBlockOfRange(range, root); + let blockContentsAfterSplit = null; const firstBlockInFrag = getNextBlock(frag, frag); const replaceBlock = !firstInFragIsInline && !!block && isEmptyBlock(block); - if (block && firstBlockInFrag && !replaceBlock && - !getClosest(firstBlockInFrag, frag, "PRE,TABLE")) { + if (block && firstBlockInFrag && !replaceBlock && // Don't merge table cells or PRE elements into block + !getClosest(firstBlockInFrag, frag, "PRE") && !getClosest(firstBlockInFrag, frag, "TABLE")) { moveRangeBoundariesUpTree(range, block, block, root); range.collapse(true); let container = range.endContainer; @@ -1011,10 +1192,12 @@ root ); container = nodeAfterSplit.parentNode; - offset = indexOf(container.childNodes, nodeAfterSplit); + offset = Array.from(container.childNodes).indexOf( + nodeAfterSplit + ); } if ( - /*isBlock(container) && */ + /*isBlock( container ) && */ offset !== getLength(container) ) { blockContentsAfterSplit = document.createDocumentFragment(); @@ -1023,14 +1206,16 @@ } } mergeWithBlock(container, firstBlockInFrag, range, root); - offset = indexOf(container.parentNode.childNodes, container) + 1; + offset = Array.from(container.parentNode.childNodes).indexOf( + container + ) + 1; container = container.parentNode; range.setEnd(container, offset); } if (getLength(frag)) { if (replaceBlock && block) { range.setEndBefore(block); - range.collapse(); + range.collapse(false); detach(block); } moveRangeBoundariesUpTree(range, stopPoint, stopPoint, root); @@ -1054,7 +1239,7 @@ if (nodeAfterSplit && isContainer(nodeAfterSplit)) { mergeContainers(nodeAfterSplit, root); } - nodeAfterSplit = nodeBeforeSplit?.nextSibling; + nodeAfterSplit = nodeBeforeSplit && nodeBeforeSplit.nextSibling; if (nodeAfterSplit && isContainer(nodeAfterSplit)) { mergeContainers(nodeAfterSplit, root); } @@ -1062,6 +1247,7 @@ } if (blockContentsAfterSplit && block) { const tempRange = range.cloneRange(); + fixCursor(blockContentsAfterSplit); mergeWithBlock(block, blockContentsAfterSplit, tempRange, root); range.setEnd(tempRange.endContainer, tempRange.endOffset); } @@ -1069,29 +1255,29 @@ }; // source/range/Contents.ts -/* var getTextContentsOfRange = (range) => { if (range.collapsed) { return ""; } const startContainer = range.startContainer; const endContainer = range.endContainer; - const filter = (node2) => isNodeContainedInRange(range, node2, true); const walker = createTreeWalker( range.commonAncestorContainer, SHOW_ELEMENT_OR_TEXT, - filter + (node2) => { + return isNodeContainedInRange(range, node2, true); + } ); walker.currentNode = startContainer; let node = startContainer; let textContent = ""; let addedTextInBlock = false; let value; - if (!isElement(node) && !isTextNode(node) || !filter(node)) { + if (!(node instanceof Element) && !(node instanceof Text) || !walker.filter(node)) { node = walker.nextNode(); } while (node) { - if (isTextNode(node)) { + if (node instanceof Text) { value = node.data; if (value && /\S/.test(value)) { if (node === endContainer) { @@ -1103,61 +1289,74 @@ textContent += value; addedTextInBlock = true; } - } else if (isBrElement(node) || addedTextInBlock && !isInline(node)) { + } else if (node.nodeName === "BR" || addedTextInBlock && !isInline(node)) { textContent += "\n"; addedTextInBlock = false; } node = walker.nextNode(); } - textContent = textContent.replace(/\xA0/g, " "); + textContent = textContent.replace(/ /g, " "); return textContent; }; -*/ // source/Clipboard.ts - var extractRangeToClipboard = (event, range, root, cut) => { - if (event.clipboardData) { - let startBlock = getStartBlockOfRange(range, root), - endBlock = getEndBlockOfRange(range, root), - copyRoot = ((startBlock === endBlock) && startBlock) || root, - contents, parent, newContents; - if (cut) { - contents = deleteContentsOfRange(range, root); - } else { - range = range.cloneRange(); - moveRangeBoundariesDownTree(range); - moveRangeBoundariesUpTree(range, copyRoot, copyRoot, root); - contents = range.cloneContents(); - } - parent = range.commonAncestorContainer; - if (isTextNode(parent)) { - parent = parent.parentNode; - } - while (parent && parent !== copyRoot) { - newContents = parent.cloneNode(false); - newContents.append(contents); - contents = newContents; - parent = parent.parentNode; - } - let clipboardData = event.clipboardData; - let body = document.body; - let node = createElement("div"); - let html, text; + var indexOf = Array.prototype.indexOf; + var extractRangeToClipboard = (event, range, root, removeRangeFromDocument, toCleanHTML, toPlainText, plainTextOnly) => { + const clipboardData = event.clipboardData; + if (!clipboardData) { + return false; + } + let text = toPlainText ? "" : getTextContentsOfRange(range); + const startBlock = getStartBlockOfRange(range, root); + const endBlock = getEndBlockOfRange(range, root); + let copyRoot = root; + if (startBlock === endBlock && (startBlock == null ? void 0 : startBlock.contains(range.commonAncestorContainer))) { + copyRoot = startBlock; + } + let contents; + if (removeRangeFromDocument) { + contents = deleteContentsOfRange(range, root); + } else { + range = range.cloneRange(); + moveRangeBoundariesDownTree(range); + moveRangeBoundariesUpTree(range, copyRoot, copyRoot, root); + contents = range.cloneContents(); + } + let parent = range.commonAncestorContainer; + if (parent instanceof Text) { + parent = parent.parentNode; + } + while (parent && parent !== copyRoot) { + const newContents = parent.cloneNode(false); + newContents.append(contents); + contents = newContents; + parent = parent.parentNode; + } + let html; + if (contents.childNodes.length === 1 && contents.childNodes[0] instanceof Text) { + text = contents.childNodes[0].data.replace(/ /g, " "); + plainTextOnly = true; + } else { + const node = createElement("DIV"); node.append(contents); html = node.innerHTML; - cleanupBRs(node, root, true); - node.setAttribute("style", - 'position:fixed;overflow:hidden;bottom:100%;right:100%;'); - body.append(node); - text = (node.innerText || node.textContent).replace(NBSP, ' '); // Replace nbsp with regular space - node.remove(); - if (text !== html) { - clipboardData.setData("text/html", html); + if (toCleanHTML) { + html = toCleanHTML(html); } - clipboardData.setData("text/plain", text); - event.preventDefault(); - return true; } + if (toPlainText && html !== void 0) { + text = toPlainText(html); + } + if (isWin) { + text = text.replace(/\r?\n/g, "\r\n"); + } + if (!plainTextOnly && html && text !== html) { + html = "" + html; + clipboardData.setData("text/html", html); + } + clipboardData.setData("text/plain", text); + event.preventDefault(); + return true; }; var _onCut = function(event) { const range = this.getSelection(); @@ -1167,12 +1366,21 @@ return; } this.saveUndoState(range); - if (!extractRangeToClipboard(event, range, root, true)) { + const handled = extractRangeToClipboard( + event, + range, + root, + true, + this._config.willCutCopy, + this._config.toPlainText, + false + ); + if (!handled) { setTimeout(() => { try { this._ensureBottomLine(); } catch (error) { - didError(error); + this._config.didError(error); } }, 0); } @@ -1182,70 +1390,99 @@ extractRangeToClipboard( event, this.getSelection(), - this._root + this._root, + false, + this._config.willCutCopy, + this._config.toPlainText, + false ); }; + var _monitorShiftKey = function(event) { + this._isShiftDown = event.shiftKey; + }; var _onPaste = function(event) { const clipboardData = event.clipboardData; - const items = clipboardData?.items; - let imageItem = null; + const items = clipboardData.items; + const choosePlain = this._isShiftDown; + let hasRTF = false; + let hasImage = false; let plainItem = null; let htmlItem = null; - let self = this; - let type; - if (items) { - [...items].forEach(item => { - type = item.type; - if (type === "text/html") { - htmlItem = item; - } else if (type === "text/plain" || type === "text/uri-list") { - plainItem = item; - } else if (item.kind === "file" && /^image\/(png|jpeg|webp)/.test(type)) { - imageItem = item; + let l = items.length; + while (l--) { + const item = items[l]; + const type = item.type; + if (type === "text/html") { + htmlItem = item; + } else if (type === "text/plain" || type === "text/uri-list") { + plainItem = item; + } else if (type === "text/rtf") { + hasRTF = true; + } else if (/^image\/.*/.test(type)) { + hasImage = true; + } + } + if (hasImage && !(hasRTF && htmlItem)) { + event.preventDefault(); + this.fireEvent("pasteImage", { + clipboardData + }); + return; + } + event.preventDefault(); + if (htmlItem && (!choosePlain || !plainItem)) { + htmlItem.getAsString((html) => { + this.insertHTML(html, true); + }); + } else if (plainItem) { + plainItem.getAsString((text) => { + let isLink = false; + const range = this.getSelection(); + if (!range.collapsed && notWS.test(range.toString())) { + const match = this.linkRegExp.exec(text); + isLink = !!match && match[0].length === text.length; + } + if (isLink) { + this.makeLink(text); + } else { + this.insertPlainText(text, true); } }); - if (htmlItem || plainItem || imageItem) { - event.preventDefault(); - if (imageItem) { - let reader = new FileReader(); - reader.onload = (event) => { - let img = createElement("img", {src: event.target.result}), - canvas = createElement("canvas"), - ctx = canvas.getContext('2d'); - img.onload = ()=>{ - ctx.drawImage(img, 0, 0); - let width = img.width, height = img.height; - if (width > height) { - if (width > 1024) { - height = height * 1024 / width; - width = 1024; - } - } else if (height > 1024) { - width = width * 1024 / height; - height = 1024; - } - canvas.width = width; - canvas.height = height; - ctx.drawImage(img, 0, 0, width, height); - self.insertHTML('', true); - }; - } - reader.readAsDataURL(imageItem.getAsFile()); - } else if (htmlItem && (!self.isShiftDown || !plainItem)) { - htmlItem.getAsString(html => self.insertHTML(html, true)); - } else if (plainItem) { - plainItem.getAsString(text => self.insertPlainText(text, true)); - } + } + }; + var _onDrop = function(event) { + if (!event.dataTransfer) { + return; + } + const types = event.dataTransfer.types; + let l = types.length; + let hasPlain = false; + let hasHTML = false; + while (l--) { + switch (types[l]) { + case "text/plain": + hasPlain = true; + break; + case "text/html": + hasHTML = true; + break; + default: + return; } } + if (hasHTML || hasPlain && this.saveUndoState) { + this.saveUndoState(); + } }; // source/keyboard/KeyHelpers.ts var afterDelete = (self, range) => { try { - range = range || self.getSelection(); + if (!range) { + range = self.getSelection(); + } let node = range.startContainer; - if (isTextNode(node)) { + if (node instanceof Text) { node = node.parentNode; } let parent = node; @@ -1256,23 +1493,77 @@ if (node !== parent) { range.setStart( parent, - indexOf(parent.childNodes, node) + Array.from(parent.childNodes).indexOf(node) ); range.collapse(true); - node.remove(); + parent.removeChild(node); if (!isBlock(parent)) { - parent = getPreviousBlock(parent, self._root) || parent; + parent = getPreviousBlock(parent, self._root) || self._root; } fixCursor(parent); moveRangeBoundariesDownTree(range); } - if (node === self._root && (node = node.firstChild) && isBrElement(node)) { + if (node === self._root && (node = node.firstChild) && node.nodeName === "BR") { detach(node); } self._ensureBottomLine(); - self.setRange(range); + self.setSelection(range); + self._updatePath(range, true); } catch (error) { - didError(error); + self._config.didError(error); + } + }; + var detachUneditableNode = (node, root) => { + let parent; + while (parent = node.parentNode) { + if (parent === root || parent.isContentEditable) { + break; + } + node = parent; + } + detach(node); + }; + var linkifyText = (self, textNode, offset) => { + if (getClosest(textNode, self._root, "A")) { + return; + } + const data = textNode.data || ""; + const searchFrom = Math.max( + data.lastIndexOf(" ", offset - 1), + data.lastIndexOf("\xA0", offset - 1) + ) + 1; + const searchText = data.slice(searchFrom, offset); + const match = self.linkRegExp.exec(searchText); + if (match) { + const selection = self.getSelection(); + self._docWasChanged(); + self._recordUndoState(selection); + self._getRangeAndRemoveBookmark(selection); + const index = searchFrom + match.index; + const endIndex = index + match[0].length; + const needsSelectionUpdate = selection.startContainer === textNode; + const newSelectionOffset = selection.startOffset - endIndex; + if (index) { + textNode = textNode.splitText(index); + } + const defaultAttributes = self._config.tagAttributes.a; + const link = createElement( + "A", + Object.assign( + { + href: match[1] ? /^(?:ht|f)tps?:/i.test(match[1]) ? match[1] : "http://" + match[1] : "mailto:" + match[0] + }, + defaultAttributes + ) + ); + link.textContent = data.slice(index, endIndex); + textNode.parentNode.insertBefore(link, textNode); + textNode.data = data.slice(endIndex); + if (needsSelectionUpdate) { + selection.setStart(textNode, newSelectionOffset); + selection.setEnd(textNode, newSelectionOffset); + } + self.setSelection(selection); } }; @@ -1287,13 +1578,13 @@ afterDelete(self, range); } else if (rangeDoesStartAtBlockBoundary(range, root)) { event.preventDefault(); - let current = getStartBlockOfRange(range, root); - let previous; - if (!current) { + const startBlock = getStartBlockOfRange(range, root); + if (!startBlock) { return; } + let current = startBlock; fixContainer(current.parentNode, root); - previous = getPreviousBlock(current, root); + const previous = getPreviousBlock(current, root); if (previous) { if (!previous.isContentEditable) { detachUneditableNode(previous, root); @@ -1309,14 +1600,32 @@ } self.setSelection(range); } else if (current) { - if (decreaseLevel(self, range, current)) { + if (getClosest(current, root, "UL") || getClosest(current, root, "OL")) { + self.decreaseListLevel(range); + return; + } else if (getClosest(current, root, "BLOCKQUOTE")) { + self.removeQuote(range); return; } - self.setRange(range); + self.setSelection(range); + self._updatePath(range, true); } } else { - self.setSelection(range); - setTimeout(() => afterDelete(self), 0); + moveRangeBoundariesDownTree(range); + const text = range.startContainer; + const offset = range.startOffset; + const a = text.parentNode; + if (text instanceof Text && a instanceof HTMLAnchorElement && offset && a.href.includes(text.data)) { + text.deleteData(offset - 1, 1); + self.setSelection(range); + self.removeLink(); + event.preventDefault(); + } else { + self.setSelection(range); + setTimeout(() => { + afterDelete(self); + }, 0); + } } }; @@ -1337,32 +1646,36 @@ afterDelete(self, range); } else if (rangeDoesEndAtBlockBoundary(range, root)) { event.preventDefault(); - if (current = getStartBlockOfRange(range, root)) { - fixContainer(current.parentNode, root); - if (next = getNextBlock(current, root)) { - if (!next.isContentEditable) { - detachUneditableNode(next, root); - return; - } - mergeWithBlock(current, next, range, root); - next = current.parentNode; - while (next !== root && !next.nextSibling) { - next = next.parentNode; - } - if (next !== root && (next = next.nextSibling)) { - mergeContainers(next, root); - } - self.setRange(range); + current = getStartBlockOfRange(range, root); + if (!current) { + return; + } + fixContainer(current.parentNode, root); + next = getNextBlock(current, root); + if (next) { + if (!next.isContentEditable) { + detachUneditableNode(next, root); + return; } + mergeWithBlock(current, next, range, root); + next = current.parentNode; + while (next !== root && !next.nextSibling) { + next = next.parentNode; + } + if (next !== root && (next = next.nextSibling)) { + mergeContainers(next, root); + } + self.setSelection(range); + self._updatePath(range, true); } } else { originalRange = range.cloneRange(); moveRangeBoundariesUpTree(range, root, root, root); cursorContainer = range.endContainer; cursorOffset = range.endOffset; - if (isElement(cursorContainer)) { + if (cursorContainer instanceof Element) { nodeAfterCursor = cursorContainer.childNodes[cursorOffset]; - if (nodeAfterCursor?.nodeName === "IMG") { + if (nodeAfterCursor && nodeAfterCursor.nodeName === "IMG") { event.preventDefault(); detach(nodeAfterCursor); moveRangeBoundariesDownTree(range); @@ -1371,30 +1684,30 @@ } } self.setSelection(originalRange); - setTimeout(() => afterDelete(self), 0); + setTimeout(() => { + afterDelete(self); + }, 0); } }; // source/keyboard/Tab.ts var Tab = (self, event, range) => { + const root = self._root; self._removeZWS(); - range.collapsed - && rangeDoesStartAtBlockBoundary(range, self._root) - && getClosest(range.startContainer, self._root, "UL,OL,BLOCKQUOTE") - && self.changeIndentationLevel("increase") - && event.preventDefault(); + if (range.collapsed && rangeDoesStartAtBlockBoundary(range, root)) { + getClosest(range.startContainer, root, "UL,OL,BLOCKQUOTE") && self.changeIndentationLevel("increase") && event.preventDefault(); + } }; var ShiftTab = (self, event, range) => { + const root = self._root; self._removeZWS(); - range.collapsed - && rangeDoesStartAtBlockBoundary(range, self._root) - && decreaseLevel(self, range, range.startContainer) - && event.preventDefault(); + if (range.collapsed && rangeDoesStartAtBlockBoundary(range, root)) { + decreaseLevel(self, range, range.startContainer) && event.preventDefault(); + } }; // source/keyboard/Space.ts var Space = (self, event, range) => { -/* var _a; let node; const root = self._root; @@ -1408,13 +1721,16 @@ } else if (rangeDoesEndAtBlockBoundary(range, root)) { const block = getStartBlockOfRange(range, root); if (block && block.nodeName !== "PRE") { - const text = block.textContent?.trimEnd().replace(ZWS, ""); + const text = (_a = block.textContent) == null ? void 0 : _a.trimEnd().replace(ZWS, ""); if (text === "*" || text === "1.") { event.preventDefault(); + self.insertPlainText(" ", false); + self._docWasChanged(); + self.saveUndoState(range); const walker = createTreeWalker(block, SHOW_TEXT); let textNode; while (textNode = walker.nextNode()) { - textNode.data = cantFocusEmptyTextNodes ? ZWS : ""; + detach(textNode); } if (text === "*") { self.makeUnorderedList(); @@ -1444,485 +1760,421 @@ }, 0); } self.setSelection(range); -*/ - const root = self._root; - self._recordUndoState(range); - self._config.addLinks && addLinks(range.startContainer, root); - self._getRangeAndRemoveBookmark(range); -/* - let node = range.endContainer; - if (range.collapsed && range.endOffset === getLength(node)) { - do { - if (node.nodeName === "A") { - range.setStartAfter(node); - break; - } - } while (!node.nextSibling && (node = node.parentNode) && node !== root); - } -*/ - if (!range.collapsed) { - deleteContentsOfRange(range, root); - self._ensureBottomLine(); - } - self.setRange(range); }; // source/keyboard/KeyHandlers.ts var _onKey = function(event) { - if (event.defaultPrevented) { + if (event.defaultPrevented || event.isComposing) { return; } - let key = event.key, - range = this.getSelection(), - root = this._root; - if (key !== "Backspace" && key !== "Delete") { - if (event.shiftKey) { - key = "Shift-" + key; - } - if (event[osKey]) { key = ctrlKey + key; } + let key = event.key; + let modifiers = ""; + const code = event.code; + if (/^Digit\d$/.test(code)) { + key = code.slice(-1); } + if (key !== "Backspace" && key !== "Delete") { + if (event.altKey) { + modifiers += "Alt-"; + } + if (event.ctrlKey) { + modifiers += "Ctrl-"; + } + if (event.metaKey) { + modifiers += "Meta-"; + } + if (event.shiftKey) { + modifiers += "Shift-"; + } + } + if (isWin && event.shiftKey && key === "Delete") { + modifiers += "Shift-"; + } + key = modifiers + key; + const range = this.getSelection(); if (this._keyHandlers[key]) { this._keyHandlers[key](this, event, range); - } else if (!range.collapsed && !event.isComposing && !event[osKey] && key.length === 1) { + } else if (!range.collapsed && !event.ctrlKey && !event.metaKey && key.length === 1) { this.saveUndoState(range); - deleteContentsOfRange(range, root); + deleteContentsOfRange(range, this._root); this._ensureBottomLine(); - this.setRange(range); - } else if (range.collapsed && range.startContainer === root && root.children.length > 0) { - const nextElement = root.children[range.startOffset]; - if (nextElement && !isBlock(nextElement)) { - range = createRange(root.insertBefore( - this.createDefaultBlock(), nextElement - ), 0); - if (isBrElement(nextElement)) { - root.removeChild(nextElement); - } - const restore = this._willRestoreSelection; - this.setSelection(range); - this._willRestoreSelection = restore; - } + this.setSelection(range); + this._updatePath(range, true); } }; - var mapKeyToFormat = (tag, remove) => { - return (self, event) => { - event.preventDefault(); - self.toggleTag(tag, remove); - }; - }; - var mapKeyTo = (method) => (self, event) => { - event.preventDefault(); - self[method](); - }; - var toggleList = (type, methodIfNotInList) => (self, event) => { - event.preventDefault(); - let parent = self.getSelectionClosest("UL,OL"); - if (type == parent?.nodeName) { - self.removeList(); - } else { - self[methodIfNotInList](); - } - }; - var changeIndentationLevel = (direction) => (self, event) => { - event.preventDefault(); - self.changeIndentationLevel(direction); - }; var keyHandlers = { - Tab, + "Backspace": Backspace, + "Delete": Delete, + "Tab": Tab, "Shift-Tab": ShiftTab, - Space, - ArrowLeft(self) { + " ": Space, + "ArrowLeft"(self) { self._removeZWS(); }, - ArrowRight(self) { - self._removeZWS() - }, - [ctrlKey + "b"]: mapKeyToFormat("B"), - [ctrlKey + "i"]: mapKeyToFormat("I"), - [ctrlKey + "u"]: mapKeyToFormat("U"), - [ctrlKey + "Shift-7"]: mapKeyToFormat("S"), - [ctrlKey + "Shift-5"]: mapKeyToFormat("SUB", "SUP"), - [ctrlKey + "Shift-6"]: mapKeyToFormat("SUP", "SUB"), - [ctrlKey + "Shift-8"]: toggleList("UL", "makeUnorderedList"), - [ctrlKey + "Shift-9"]: toggleList("OL", "makeOrderedList"), - [ctrlKey + "["]: changeIndentationLevel("decrease"), - [ctrlKey + "]"]: changeIndentationLevel("increase"), - [ctrlKey + "d"]: mapKeyTo("toggleCode"), -// [ctrlKey + "z"]: mapKeyTo("undo"), // historyUndo - [ctrlKey + "y"]: mapKeyTo("redo"), // historyRedo - [ctrlKey + "Shift-Z"]: mapKeyTo("redo"), - ["Redo"]: mapKeyTo("redo") - }; - var blockTag = "DIV"; - var DOCUMENT_POSITION_PRECEDING = 2; - - var NBSP = '\u00A0'; - var win = document.defaultView; - var osKey = isMac ? "metaKey" : "ctrlKey"; -/* - typeToBitArray = { - 1: 1, - 2: 2, - 3: 4, - 8: 128, - 9: 256, - 11: 1024 - }, -*/ - - var didError = error => console.error(error); - var detachUneditableNode = (node, root) => { - let parent; - while (parent = node.parentNode) { - if (parent === root || parent.isContentEditable) { - break; - } - node = parent; - } - detach(node); - }; - - var mergeObjects = (base, extras, mayOverride) => { - base = base || {}; - extras && Object.entries(extras).forEach(([prop,value])=>{ - if (mayOverride || !(prop in base)) { - base[prop] = (value?.constructor === Object) ? - mergeObjects(base[prop], value, mayOverride) : - value; - } - }); - return base; - }; - - var createBookmarkNodes = () => [ - createElement("INPUT", { - id: startSelectionId, - type: "hidden" - }), - createElement("INPUT", { - id: endSelectionId, - type: "hidden" - }) - ]; - - var getListSelection = (range, root) => { - let list = range.commonAncestorContainer; - let startLi = range.startContainer; - let endLi = range.endContainer; - while (list && list !== root && !listNodeNames.has(list.nodeName)) { - list = list.parentNode; - } - if (!list || list === root) { - return null; - } - if (startLi === list) { - startLi = startLi.childNodes[range.startOffset]; - } - if (endLi === list) { - endLi = endLi.childNodes[range.endOffset]; - } - while (startLi && startLi.parentNode !== list) { - startLi = startLi.parentNode; - } - while (endLi && endLi.parentNode !== list) { - endLi = endLi.parentNode; - } - return [list, startLi, endLi]; - }; - var setDirection = (self, frag, dir) => { - let walker = getBlockWalker(frag, self._root), - node; - while (node = walker.nextNode()) { - if (node.nodeName === "LI") { - node.parentNode.setAttribute("dir", dir); - break; - } - node.setAttribute("dir", dir); - } - return frag; - }; - var decreaseLevel = (self, range, node) => - getClosest(node, self._root, "UL,OL,BLOCKQUOTE") && self.changeIndentationLevel("decrease"); - var addLinks = (frag, root) => { - let walker = createTreeWalker(frag, SHOW_TEXT, node => !getClosest(node, root, "A")); - let node, data, parent, match, index, endIndex, child; - while (node = walker.nextNode()) { - data = node.data; - parent = node.parentNode; - while (match = linkRegExp.exec(data)) { - index = match.index; - endIndex = index + match[0].length; - if (index) { - child = document.createTextNode(data.slice(0, index)); - parent.insertBefore(child, node); - } - child = createElement("A", { - href: match[1] - ? (match[2] ? match[1] : "https://" + match[1]) - : "mailto:" + match[0] - }, [data.slice(index, endIndex)]); - parent.insertBefore(child, node); - node.data = data = data.slice(endIndex); + "ArrowRight"(self, event, range) { + self._removeZWS(); + const root = self.getRoot(); + if (rangeDoesEndAtBlockBoundary(range, root)) { + moveRangeBoundariesDownTree(range); + let node = range.endContainer; + do { + if (node.nodeName === "CODE") { + let next = node.nextSibling; + if (!(next instanceof Text)) { + const textNode = document.createTextNode("\xA0"); + node.parentNode.insertBefore(textNode, next); + next = textNode; + } + range.setStart(next, 1); + self.setSelection(range); + event.preventDefault(); + break; + } + } while (!node.nextSibling && (node = node.parentNode) && node !== root); } } }; - -keyHandlers[ctrlKey + "b"] = mapKeyToFormat("B"); -keyHandlers[ctrlKey + "i"] = mapKeyToFormat("I"); -keyHandlers[ctrlKey + "u"] = mapKeyToFormat("U"); -keyHandlers[ctrlKey + "Shift-7"] = mapKeyToFormat("S"); -keyHandlers[ctrlKey + "Shift-5"] = mapKeyToFormat("SUB", "SUP"); -keyHandlers[ctrlKey + "Shift-6"] = mapKeyToFormat("SUP", "SUB"); -keyHandlers[ctrlKey + "Shift-8"] = toggleList("UL", "makeUnorderedList"); -keyHandlers[ctrlKey + "Shift-9"] = toggleList("OL", "makeOrderedList"); -keyHandlers[ctrlKey + "["] = changeIndentationLevel("decrease"); -keyHandlers[ctrlKey + "]"] = changeIndentationLevel("increase"); -keyHandlers[ctrlKey + "d"] = mapKeyTo("toggleCode"); -keyHandlers[ctrlKey + "y"] = mapKeyTo("redo"); -keyHandlers[ctrlKey + "Shift-Z"] = mapKeyTo("redo"); -keyHandlers["Redo"] = mapKeyTo("redo"); - -class EditStack extends Array -{ - constructor(squire) { - super(); - this.squire = squire; - this.index = -1; - this.inUndoState = false; - this.limit = 0; // -1 means no limit - } - - clear() { - this.index = -1; - this.length = 0; - } - - stateChanged(/*canUndo, canRedo*/) { - this.squire.fireEvent("undoStateChange", { - canUndo: this.index > 0, - canRedo: this.index + 1 < this.length - }); - this.squire.fireEvent("input"); - } - - docWasChanged() { - if (this.inUndoState) { - this.inUndoState = false; - this.stateChanged(/*true, false*/); - } else - this.squire.fireEvent("input"); - } - - /** - * Leaves bookmark. - */ - recordUndoState(range, replace) { - if (!this.inUndoState || replace) { - let undoIndex = this.index; - let undoLimit = this.limit; - let squire = this.squire; - replace || ++undoIndex; - undoIndex = Math.max(0, undoIndex); - this.length = Math.min(undoIndex + 1, this.length); - range && squire._saveRangeToBookmark(range); - const html = squire._getRawHTML(); - if (undoLimit > 0 && undoIndex > undoLimit) { - this.splice(0, undoIndex - undoLimit); - undoIndex = undoLimit; - } - this[undoIndex] = html; - this.index = undoIndex; - this.inUndoState = true; + if (!isMac && !isIOS) { + keyHandlers.PageUp = (self) => { + self.moveCursorToStart(); + }; + keyHandlers.PageDown = (self) => { + self.moveCursorToEnd(); + }; + } + var mapKeyToFormat = (tag, remove) => { + remove = remove || null; + return (self, event) => { + event.preventDefault(); + const range = self.getSelection(); + if (self.hasFormat(tag, null, range)) { + self.changeFormat(null, { tag }, range); + } else { + self.changeFormat({ tag }, remove, range); } + }; + }; + keyHandlers[ctrlKey + "b"] = mapKeyToFormat("B"); + keyHandlers[ctrlKey + "i"] = mapKeyToFormat("I"); + keyHandlers[ctrlKey + "u"] = mapKeyToFormat("U"); + keyHandlers[ctrlKey + "Shift-7"] = mapKeyToFormat("S"); + keyHandlers[ctrlKey + "Shift-5"] = mapKeyToFormat("SUB", { tag: "SUP" }); + keyHandlers[ctrlKey + "Shift-6"] = mapKeyToFormat("SUP", { tag: "SUB" }); + keyHandlers[ctrlKey + "Shift-8"] = (self, event) => { + event.preventDefault(); + const path = self.getPath(); + if (!/(?:^|>)UL/.test(path)) { + self.makeUnorderedList(); + } else { + self.removeList(); } - - saveUndoState(range) { - let squire = this.squire; - range = range || squire.getSelection(); - this.recordUndoState(range, true); - squire._getRangeAndRemoveBookmark(range); + }; + keyHandlers[ctrlKey + "Shift-9"] = (self, event) => { + event.preventDefault(); + const path = self.getPath(); + if (!/(?:^|>)OL/.test(path)) { + self.makeOrderedList(); + } else { + self.removeList(); } - - undo() { - let squire = this.squire; - if (this.index > 0 || !this.inUndoState) { - this.recordUndoState(squire.getSelection()); - const undoIndex = this.index - 1; - this.index = undoIndex; - squire._setRawHTML(this[undoIndex]); - let range = squire._getRangeAndRemoveBookmark(); - if (range) { - squire.setSelection(range); - } - this.stateChanged(/*undoIndex > 0, true*/); - } + }; + keyHandlers[ctrlKey + "["] = (self, event) => { + event.preventDefault(); + const path = self.getPath(); + if (/(?:^|>)BLOCKQUOTE/.test(path) || !/(?:^|>)[OU]L/.test(path)) { + self.decreaseQuoteLevel(); + } else { + self.decreaseListLevel(); } - - redo() { - let squire = this.squire, - undoIndex = this.index + 1; - if (undoIndex < this.length && this.inUndoState) { - this.index = undoIndex; - squire._setRawHTML(this[undoIndex]); - let range = squire._getRangeAndRemoveBookmark(); - if (range) { - squire.setSelection(range); - } - this.stateChanged(/*true, undoIndex + 1 < this.length*/); - } + }; + keyHandlers[ctrlKey + "]"] = (self, event) => { + event.preventDefault(); + const path = self.getPath(); + if (/(?:^|>)BLOCKQUOTE/.test(path) || !/(?:^|>)[OU]L/.test(path)) { + self.increaseQuoteLevel(); + } else { + self.increaseListLevel(); } -} + }; + keyHandlers[ctrlKey + "d"] = (self, event) => { + event.preventDefault(); + self.toggleCode(); + }; + keyHandlers[ctrlKey + "z"] = (self, event) => { + event.preventDefault(); + self.undo(); + }; + keyHandlers[ctrlKey + "y"] = // Depending on platform, the Shift may cause the key to come through as + // upper case, but sometimes not. Just add both as shortcuts — the browser + // will only ever fire one or the other. + keyHandlers[ctrlKey + "Shift-z"] = keyHandlers[ctrlKey + "Shift-Z"] = (self, event) => { + event.preventDefault(); + self.redo(); + }; // source/Editor.ts - var customEvents = new Set([ - "pathChange", - "select", - "input", - "pasteImage", - "undoStateChange" - ]); - var startSelectionId = "squire-selection-start"; - var endSelectionId = "squire-selection-end"; - var tagAfterSplit = { - DT: "DD", - DD: "DT", - LI: "LI", - PRE: "PRE" - }; - var linkRegExp = /\b(?:((https?:\/\/)?(?:www\d{0,3}\.|[a-z0-9][a-z0-9.-]*\.[a-z]{2,}\/)(?:[^\s()<>]+|\([^\s()<>]+\))+(?:[^\s?&`!()[\]{};:'".,<>«»“”‘’]|\([^\s()<>]+\)))|([\w\-.%+]+@(?:[\w-]+\.)+[a-z]{2,}\b(?:\?[^&?\s]+=[^\s?&`!()[\]{};:'".,<>«»“”‘’]+(?:&[^&?\s]+=[^\s?&`!()[\]{};:'".,<>«»“”‘’]+)*)?))/i; var Squire = class { constructor(root, config) { + /** + * Subscribing to these events won't automatically add a listener to the + * document node, since these events are fired in a custom manner by the + * editor code. + */ + this.customEvents = /* @__PURE__ */ new Set([ + "pathChange", + "select", + "input", + "pasteImage", + "undoStateChange" + ]); + // --- + this.startSelectionId = "squire-selection-start"; + this.endSelectionId = "squire-selection-end"; + /* + linkRegExp = new RegExp( + // Only look on boundaries + '\\b(?:' + + // Capture group 1: URLs + '(' + + // Add links to URLS + // Starts with: + '(?:' + + // http(s):// or ftp:// + '(?:ht|f)tps?:\\/\\/' + + // or + '|' + + // www. + 'www\\d{0,3}[.]' + + // or + '|' + + // foo90.com/ + '[a-z0-9][a-z0-9.\\-]*[.][a-z]{2,}\\/' + + ')' + + // Then we get one or more: + '(?:' + + // Run of non-spaces, non ()<> + '[^\\s()<>]+' + + // or + '|' + + // balanced parentheses (one level deep only) + '\\([^\\s()<>]+\\)' + + ')+' + + // And we finish with + '(?:' + + // Not a space or punctuation character + '[^\\s?&`!()\\[\\]{};:\'".,<>«»“”‘’]' + + // or + '|' + + // Balanced parentheses. + '\\([^\\s()<>]+\\)' + + ')' + + // Capture group 2: Emails + ')|(' + + // Add links to emails + '[\\w\\-.%+]+@(?:[\\w\\-]+\\.)+[a-z]{2,}\\b' + + // Allow query parameters in the mailto: style + '(?:' + + '[?][^&?\\s]+=[^\\s?&`!()\\[\\]{};:\'".,<>«»“”‘’]+' + + '(?:&[^&?\\s]+=[^\\s?&`!()\\[\\]{};:\'".,<>«»“”‘’]+)*' + + ')?' + + '))', + 'i' + ); + */ + this.linkRegExp = /\b(?:((?:(?:ht|f)tps?:\/\/|www\d{0,3}[.]|[a-z0-9][a-z0-9.\-]*[.][a-z]{2,}\/)(?:[^\s()<>]+|\([^\s()<>]+\))+(?:[^\s?&`!()\[\]{};:'".,<>«»“”‘’]|\([^\s()<>]+\)))|([\w\-.%+]+@(?:[\w\-]+\.)+[a-z]{2,}\b(?:[?][^&?\s]+=[^\s?&`!()\[\]{};:'".,<>«»“”‘’]+(?:&[^&?\s]+=[^\s?&`!()\[\]{};:'".,<>«»“”‘’]+)*)?))/i; + this.tagAfterSplit = { + DT: "DD", + DD: "DT", + LI: "LI", + PRE: "PRE" + }; this._root = root; - this.setConfig(config); + this._config = this._makeConfig(config); this._isFocused = false; - this._lastSelection = null; + this._lastSelection = createRange(root, 0); this._willRestoreSelection = false; this._mayHaveZWS = false; + this._lastAnchorNode = null; + this._lastFocusNode = null; this._path = ""; - this._pathRange = null; - this._events = new Map(); - this.editStack = new EditStack(this); + this._events = /* @__PURE__ */ new Map(); + this._undoIndex = -1; + this._undoStack = []; + this._undoStackLength = 0; + this._isInUndoState = false; this._ignoreChange = false; - this.addEventListener("selectionchange", () => this._isFocused && this._updatePath(this.getSelection())) - .addEventListener("blur", () => this._willRestoreSelection = true) - .addEventListener("pointerdown mousedown touchstart", () => this._willRestoreSelection = false) - .addEventListener("focus", () => this._willRestoreSelection && this.setSelection(this._lastSelection)) - .addEventListener("cut", _onCut) - .addEventListener("copy", _onCopy) - .addEventListener("paste", _onPaste) - .addEventListener("drop", (event) => { - let types = event.dataTransfer.types; - if (types.includes("text/plain") || types.includes("text/html")) { - this.saveUndoState(); - } - }) - .addEventListener("keydown keyup", (event) => this.isShiftDown = event.shiftKey) - .addEventListener("keydown", _onKey) - .addEventListener("pointerup keyup mouseup touchend", () => this.getSelection()) - .addEventListener("beforeinput", this._beforeInput); + this._ignoreAllChanges = false; + this.addEventListener("selectionchange", this._updatePathOnEvent); + this.addEventListener("blur", this._enableRestoreSelection); + this.addEventListener("mousedown", this._disableRestoreSelection); + this.addEventListener("touchstart", this._disableRestoreSelection); + this.addEventListener("focus", this._restoreSelection); + this.addEventListener("blur", this._removeZWS); + this._isShiftDown = false; + this.addEventListener("cut", _onCut); + this.addEventListener("copy", _onCopy); + this.addEventListener("paste", _onPaste); + this.addEventListener("drop", _onDrop); + this.addEventListener( + "keydown", + _monitorShiftKey + ); + this.addEventListener("keyup", _monitorShiftKey); + this.addEventListener("keydown", _onKey); this._keyHandlers = Object.create(keyHandlers); - this._mutation = new MutationObserver(() => this._docWasChanged()); - this._mutation.observe(root, { + const mutation = new MutationObserver(() => this._docWasChanged()); + mutation.observe(root, { childList: true, attributes: true, characterData: true, subtree: true }); + this._mutation = mutation; root.setAttribute("contenteditable", "true"); + this.addEventListener( + "beforeinput", + this._beforeInput + ); this.setHTML(""); - this._beforeInputTypes = { - insertText: (event) => { - if (isAndroid && event.data && event.data.includes("\n")) { - event.preventDefault(); - } + } + destroy() { + this._events.forEach((_, type) => { + this.removeEventListener(type); + }); + this._mutation.disconnect(); + this._undoIndex = -1; + this._undoStack = []; + this._undoStackLength = 0; + } + _makeConfig(userConfig) { + const config = { + blockTag: "DIV", + blockAttributes: null, + tagAttributes: {}, + classNames: { + color: "color", + fontFamily: "font", + fontSize: "size", + highlight: "highlight" }, - insertLineBreak: (event) => { + undo: { + documentSizeThreshold: -1, + // -1 means no threshold + undoLimit: -1 + // -1 means no limit + }, + addLinks: true, + willCutCopy: null, + toPlainText: null, + sanitizeToDOMFragment: (html) => { + const frag = DOMPurify.sanitize(html, { + ALLOW_UNKNOWN_PROTOCOLS: true, + WHOLE_DOCUMENT: false, + RETURN_DOM: true, + RETURN_DOM_FRAGMENT: true, + FORCE_BODY: false + }); + return frag ? document.importNode(frag, true) : document.createDocumentFragment(); + }, + didError: (error) => console.log(error) + }; + if (userConfig) { + Object.assign(config, userConfig); + config.blockTag = config.blockTag.toUpperCase(); + } + return config; + } + setKeyHandler(key, fn) { + this._keyHandlers[key] = fn; + return this; + } + _beforeInput(event) { + switch (event.inputType) { + case "insertLineBreak": event.preventDefault(); this.splitBlock(true); - }, - insertParagraph: (event) => { + break; + case "insertParagraph": event.preventDefault(); this.splitBlock(false); - }, - insertOrderedList: (event) => { + break; + case "insertOrderedList": event.preventDefault(); this.makeOrderedList(); - }, - insertUnoderedList: (event) => { + break; + case "insertUnoderedList": event.preventDefault(); this.makeUnorderedList(); - }, - historyUndo: (event) => { + break; + case "historyUndo": event.preventDefault(); this.undo(); - }, - historyRedo: (event) => { + break; + case "historyRedo": event.preventDefault(); - }, - formatRemove: (event) => { - event.preventDefault(); - this.setStyle(); - }, - formatSetBlockTextDirection: (event) => { - event.preventDefault(); - let dir = event.data; - this.setTextDirection(dir === "null" ? null : dir); - }, - formatBackColor: (event) => { - event.preventDefault(); - this.setStyle({ backgroundColor: event.data }); - }, - formatFontColor: (event) => { - event.preventDefault(); - this.setStyle({ color: event.data }); - }, - formatFontName: (event) => { - event.preventDefault(); - this.setStyle({ fontFamily: event.data }); - }, -/* - formatIndent: event => { - event.preventDefault(); - this.changeIndentationLevel("increase"); - }, - formatOutdent: event => { - event.preventDefault(); - this.changeIndentationLevel("decrease"); - }, - this.saveUndoState(); - }, -*/ - deleteContentBackward: (event) => { - Backspace(this, event, this.getSelection()); - }, - deleteContentForward: (event) => { - Delete(this, event, this.getSelection()); - } - }; - } - - _beforeInput(event) { - let type = event.isComposing ? "" : event.inputType; - switch (type) { + this.redo(); + break; case "formatBold": - case "formatItalic": + event.preventDefault(); + this.bold(); + break; + case "formaItalic": + event.preventDefault(); + this.italic(); + break; case "formatUnderline": + event.preventDefault(); + this.underline(); + break; case "formatStrikeThrough": + event.preventDefault(); + this.strikethrough(); + break; case "formatSuperscript": + event.preventDefault(); + this.superscript(); + break; case "formatSubscript": event.preventDefault(); - this[type.slice(6).toLowerCase()](); + this.subscript(); break; case "formatJustifyFull": case "formatJustifyCenter": case "formatJustifyRight": case "formatJustifyLeft": { event.preventDefault(); - let alignment = type.slice(13).toLowerCase(); - this.setStyle({textAlign:alignment === "full" ? "justify" : alignment}); + let alignment = event.inputType.slice(13).toLowerCase(); + if (alignment === "full") { + alignment = "justify"; + } + this.setTextAlignment(alignment); break; } - default: - this._beforeInputTypes[type]?.(event); + case "formatRemove": + event.preventDefault(); + this.setStyle(); + break; + case "formatSetBlockTextDirection": { + event.preventDefault(); + let dir = event.data; + if (dir === "null") { + dir = null; + } + this.setTextDirection(dir); + break; + } + case "formatBackColor": + event.preventDefault(); + this.setStyle({ backgroundColor: event.data }); + break; + case "formatFontColor": + event.preventDefault(); + this.setStyle({ color: event.data }); + break; + case "formatFontName": + event.preventDefault(); + this.setStyle({ fontFamily: event.data }); + break; } } // --- Events @@ -1952,37 +2204,37 @@ class EditStack extends Array handlers = handlers.slice(); for (const handler of handlers) { try { - handler.handleEvent ? handler.handleEvent(event) : handler.call(this, event); + if ("handleEvent" in handler) { + handler.handleEvent(event); + } else { + handler.call(this, event); + } } catch (error) { - error.details = 'Squire: fireEvent error. Event type: ' + type; - didError(error); + this._config.didError(error); } } } return this; } - addEventListener(types, fn) { - if (!fn) { - didError({ - name: 'Squire: addEventListener with null or undefined fn', - message: 'Event type: ' + types - }); - return this; - } - types.split(/\s+/).forEach((type) => { - let handlers = this._events.get(type); - let target = this._root; - if (!handlers) { - handlers = []; - this._events.set(type, handlers); - customEvents.has(type) || (type === "selectionchange" ? document : target).addEventListener(type, this, {capture:true,passive:"touchstart"===type}); + addEventListener(type, fn) { + let handlers = this._events.get(type); + let target = this._root; + if (!handlers) { + handlers = []; + this._events.set(type, handlers); + if (!this.customEvents.has(type)) { + if (type === "selectionchange") { + target = document; + } + target.addEventListener(type, this, true); } - handlers.push(fn); - }); + } + handlers.push(fn); return this; } removeEventListener(type, fn) { const handlers = this._events.get(type); + let target = this._root; if (handlers) { if (fn) { let l = handlers.length; @@ -1996,7 +2248,12 @@ class EditStack extends Array } if (!handlers.length) { this._events.delete(type); - customEvents.has(type) || (type === "selectionchange" ? document : this._root).removeEventListener(type, this, true); + if (!this.customEvents.has(type)) { + if (type === "selectionchange") { + target = document; + } + target.removeEventListener(type, this, true); + } } } return this; @@ -2010,23 +2267,42 @@ class EditStack extends Array this._root.blur(); return this; } - // --- - _removeZWS() { - if (this._mayHaveZWS) { - removeZWS(this._root); - this._mayHaveZWS = false; + // --- Selection and bookmarking + _enableRestoreSelection() { + this._willRestoreSelection = true; + } + _disableRestoreSelection() { + this._willRestoreSelection = false; + } + _restoreSelection() { + if (this._willRestoreSelection) { + this.setSelection(this._lastSelection); } } // --- + _removeZWS() { + if (!this._mayHaveZWS) { + return; + } + removeZWS(this._root); + this._mayHaveZWS = false; + } _saveRangeToBookmark(range) { - let [startNode, endNode] = createBookmarkNodes(), - temp; + let startNode = createElement("INPUT", { + id: this.startSelectionId, + type: "hidden" + }); + let endNode = createElement("INPUT", { + id: this.endSelectionId, + type: "hidden" + }); + let temp; insertNodeInRange(range, startNode); - range.collapse(); + range.collapse(false); insertNodeInRange(range, endNode); - if (startNode.compareDocumentPosition(endNode) & DOCUMENT_POSITION_PRECEDING) { - startNode.id = endSelectionId; - endNode.id = startSelectionId; + if (startNode.compareDocumentPosition(endNode) & Node.DOCUMENT_POSITION_PRECEDING) { + startNode.id = this.endSelectionId; + endNode.id = this.startSelectionId; temp = startNode; startNode = endNode; endNode = temp; @@ -2036,19 +2312,23 @@ class EditStack extends Array } _getRangeAndRemoveBookmark(range) { const root = this._root; - const start = root.querySelector("#" + startSelectionId); - const end = root.querySelector("#" + endSelectionId); + const start = root.querySelector("#" + this.startSelectionId); + const end = root.querySelector("#" + this.endSelectionId); if (start && end) { let startContainer = start.parentNode; let endContainer = end.parentNode; - const startOffset = indexOf(startContainer.childNodes, start); - let endOffset = indexOf(endContainer.childNodes, end); + const startOffset = Array.from(startContainer.childNodes).indexOf( + start + ); + let endOffset = Array.from(endContainer.childNodes).indexOf(end); if (startContainer === endContainer) { - --endOffset; + endOffset -= 1; + } + start.remove(); + end.remove(); + if (!range) { + range = document.createRange(); } - detach(start); - detach(end); - range = range || document.createRange(); range.setStart(startContainer, startOffset); range.setEnd(endContainer, endOffset); mergeInlines(startContainer, range); @@ -2057,12 +2337,12 @@ class EditStack extends Array } if (range.collapsed) { startContainer = range.startContainer; - if (isTextNode(startContainer)) { + if (startContainer instanceof Text) { endContainer = startContainer.childNodes[range.startOffset]; - if (!endContainer || !isTextNode(endContainer)) { + if (!endContainer || !(endContainer instanceof Text)) { endContainer = startContainer.childNodes[range.startOffset - 1]; } - if (isTextNode(endContainer)) { + if (endContainer && endContainer instanceof Text) { range.setStart(endContainer, 0); range.collapse(true); } @@ -2072,11 +2352,11 @@ class EditStack extends Array return range || null; } getSelection() { - const sel = win.getSelection(); + const selection = window.getSelection(); const root = this._root; - let range; - if (this._isFocused && sel?.rangeCount) { - range = sel.getRangeAt(0).cloneRange(); + let range = null; + if (this._isFocused && selection && selection.rangeCount) { + range = selection.getRangeAt(0).cloneRange(); const startContainer = range.startContainer; const endContainer = range.endContainer; if (startContainer && isLeaf(startContainer)) { @@ -2094,42 +2374,84 @@ class EditStack extends Array range = null; } } - return range || createRange(root.firstChild, 0); + if (!range) { + range = createRange(root.firstElementChild || root, 0); + } + return range; } setSelection(range) { this._lastSelection = range; - if (this._isFocused) { - const selection = win.getSelection(); - if (selection) { - selection.setBaseAndExtent( - range.startContainer, - range.startOffset, - range.endContainer, - range.endOffset - ); - } + if (!this._isFocused) { + this._enableRestoreSelection(); } else { - this._willRestoreSelection = true; + const selection = window.getSelection(); + if (selection) { + if ("setBaseAndExtent" in Selection.prototype) { + selection.setBaseAndExtent( + range.startContainer, + range.startOffset, + range.endContainer, + range.endOffset + ); + } else { + selection.removeAllRanges(); + selection.addRange(range); + } + } } return this; } // --- + _moveCursorTo(toStart) { + const root = this._root; + const range = createRange(root, toStart ? 0 : root.childNodes.length); + moveRangeBoundariesDownTree(range); + this.setSelection(range); + return this; + } + moveCursorToStart() { + return this._moveCursorTo(true); + } + moveCursorToEnd() { + return this._moveCursorTo(false); + } + // --- + getCursorPosition() { + const range = this.getSelection(); + let rect = range.getBoundingClientRect(); + if (rect && !rect.top) { + this._ignoreChange = true; + const node = createElement("SPAN"); + node.textContent = ZWS; + insertNodeInRange(range, node); + rect = node.getBoundingClientRect(); + const parent = node.parentNode; + parent.removeChild(node); + mergeInlines(parent, range); + } + return rect; + } // --- Path getPath() { return this._path; } + _updatePathOnEvent() { + if (this._isFocused) { + this._updatePath(this.getSelection()); + } + } _updatePath(range, force) { - const anchor = range.startContainer, - focus = range.endContainer; - if (force || anchor !== this._pathRange.startContainer || focus !== this._pathRange.endContainer) { - this._pathRange = range.cloneRange(); - let node = anchor === focus ? focus : null, - newPath = (anchor && focus) ? (node ? this._getPath(focus) : "(selection)") : ""; - if (this._path !== newPath) { + const anchor = range.startContainer; + const focus = range.endContainer; + let newPath; + if (force || anchor !== this._lastAnchorNode || focus !== this._lastFocusNode) { + this._lastAnchorNode = anchor; + this._lastFocusNode = focus; + newPath = anchor && focus ? anchor === focus ? this._getPath(focus) : "(selection)" : ""; + if (this._path !== newPath || anchor !== focus) { this._path = newPath; this.fireEvent("pathChange", { - path: newPath, - element: (!node || isElement(node)) ? node : node.parentElement + path: newPath }); } } @@ -2139,48 +2461,167 @@ class EditStack extends Array } _getPath(node) { const root = this._root; - let path = "", style; + const config = this._config; + let path = ""; if (node && node !== root) { - path = this._getPath(node.parentNode, root); - if (isElement(node)) { + const parent = node.parentNode; + path = parent ? this._getPath(parent) : ""; + if (node instanceof HTMLElement) { + const id = node.id; + const classList = node.classList; + const classNames = Array.from(classList).sort(); + const dir = node.dir; + const styleNames = config.classNames; path += (path ? ">" : "") + node.nodeName; - if (node.id) { - path += "#" + node.id; + if (id) { + path += "#" + id; } - if (node.dir) { - path += "[dir=" + node.dir + "]"; + if (classNames.length) { + path += "."; + path += classNames.join("."); } - if (style = node.style.cssText) { - path += "[style=" + style + "]"; + if (dir) { + path += "[dir=" + dir + "]"; + } + if (classList.contains(styleNames.highlight)) { + path += "[backgroundColor=" + node.style.backgroundColor.replace(/ /g, "") + "]"; + } + if (classList.contains(styleNames.color)) { + path += "[color=" + node.style.color.replace(/ /g, "") + "]"; + } + if (classList.contains(styleNames.fontFamily)) { + path += "[fontFamily=" + node.style.fontFamily.replace(/ /g, "") + "]"; + } + if (classList.contains(styleNames.fontSize)) { + path += "[fontSize=" + node.style.fontSize + "]"; } } } return path; } // --- History + modifyDocument(modificationFn) { + const mutation = this._mutation; + if (mutation) { + if (mutation.takeRecords().length) { + this._docWasChanged(); + } + mutation.disconnect(); + } + this._ignoreAllChanges = true; + modificationFn(); + this._ignoreAllChanges = false; + if (mutation) { + mutation.observe(this._root, { + childList: true, + attributes: true, + characterData: true, + subtree: true + }); + this._ignoreChange = false; + } + return this; + } _docWasChanged() { - cache = new WeakMap(); - this._mayHaveZWS = cantFocusEmptyTextNodes; + resetNodeCategoryCache(); + this._mayHaveZWS = true; + if (this._ignoreAllChanges) { + return; + } if (this._ignoreChange) { this._ignoreChange = false; - } else { - this.editStack.docWasChanged(); + return; } + if (this._isInUndoState) { + this._isInUndoState = false; + this.fireEvent("undoStateChange", { + canUndo: true, + canRedo: false + }); + } + this.fireEvent("input"); } /** * Leaves bookmark. */ _recordUndoState(range, replace) { - this.editStack.recordUndoState(range, replace); + const isInUndoState = this._isInUndoState; + if (!isInUndoState || replace) { + let undoIndex = this._undoIndex + 1; + const undoStack = this._undoStack; + const undoConfig = this._config.undo; + const undoThreshold = undoConfig.documentSizeThreshold; + const undoLimit = undoConfig.undoLimit; + if (undoIndex < this._undoStackLength) { + undoStack.length = this._undoStackLength = undoIndex; + } + if (range) { + this._saveRangeToBookmark(range); + } + if (isInUndoState) { + return this; + } + const html = this._getRawHTML(); + if (replace) { + undoIndex -= 1; + } + if (undoThreshold > -1 && html.length * 2 > undoThreshold) { + if (undoLimit > -1 && undoIndex > undoLimit) { + undoStack.splice(0, undoIndex - undoLimit); + undoIndex = undoLimit; + this._undoStackLength = undoLimit; + } + } + undoStack[undoIndex] = html; + this._undoIndex = undoIndex; + this._undoStackLength += 1; + this._isInUndoState = true; + } + return this; } saveUndoState(range) { - this.editStack.saveUndoState(range); + if (!range) { + range = this.getSelection(); + } + this._recordUndoState(range, this._isInUndoState); + this._getRangeAndRemoveBookmark(range); + return this; } undo() { - this.editStack.undo(); + if (this._undoIndex !== 0 || !this._isInUndoState) { + this._recordUndoState(this.getSelection(), false); + this._undoIndex -= 1; + this._setRawHTML(this._undoStack[this._undoIndex]); + const range = this._getRangeAndRemoveBookmark(); + if (range) { + this.setSelection(range); + } + this._isInUndoState = true; + this.fireEvent("undoStateChange", { + canUndo: this._undoIndex !== 0, + canRedo: true + }); + this.fireEvent("input"); + } + return this.focus(); } redo() { - this.editStack.redo(); + const undoIndex = this._undoIndex; + const undoStackLength = this._undoStackLength; + if (undoIndex + 1 < undoStackLength && this._isInUndoState) { + this._undoIndex += 1; + this._setRawHTML(this._undoStack[this._undoIndex]); + const range = this._getRangeAndRemoveBookmark(); + if (range) { + this.setSelection(range); + } + this.fireEvent("undoStateChange", { + canUndo: true, + canRedo: undoIndex + 2 < undoStackLength + }); + this.fireEvent("input"); + } + return this.focus(); } // --- Get and set data getRoot() { @@ -2190,49 +2631,70 @@ class EditStack extends Array return this._root.innerHTML; } _setRawHTML(html) { - if (html !== undefined) { - const root = this._root; - let node = root; - root.innerHTML = html; - do { + const root = this._root; + root.innerHTML = html; + let node = root; + const child = node.firstChild; + if (!child || child.nodeName === "BR") { + const block = this.createDefaultBlock(); + if (child) { + node.replaceChild(block, child); + } else { + node.append(block); + } + } else { + while (node = getNextBlock(node, root)) { fixCursor(node); - } while (node = getNextBlock(node, root)); - this._ignoreChange = true; + } } + this._ignoreChange = true; + return this; } - getHTML(withBookMark) { - let html, range; - if (withBookMark) { + getHTML(withBookmark) { + let range; + if (withBookmark) { range = this.getSelection(); this._saveRangeToBookmark(range); } - html = this._getRawHTML().replace(/\u200B/g, ""); - withBookMark && this._getRangeAndRemoveBookmark(range); + const html = this._getRawHTML().replace(/\u200B/g, ""); + if (withBookmark) { + this._getRangeAndRemoveBookmark(range); + } return html; } setHTML(html) { - const root = this._root, - frag = this._config.sanitizeToDOMFragment(html, false); - cleanTree(frag); + const frag = this._config.sanitizeToDOMFragment(html, this); + const root = this._root; + cleanTree(frag, this._config); cleanupBRs(frag, root, false); fixContainer(frag, root); - let node, walker = getBlockWalker(frag, root); - while ((node = walker.nextNode()) && node !== root) { - fixCursor(node); + let node = frag; + let child = node.firstChild; + if (!child || child.nodeName === "BR") { + const block = this.createDefaultBlock(); + if (child) { + node.replaceChild(block, child); + } else { + node.append(block); + } + } else { + while (node = getNextBlock(node, root)) { + fixCursor(node); + } } this._ignoreChange = true; - if (root.replaceChildren) { - root.replaceChildren(frag); - } else { - while (root.lastChild) - detach(root.lastChild); - root.append(frag); + while (child = root.lastChild) { + root.removeChild(child); } - fixCursor(root); - this.editStack.clear(); + root.append(frag); + this._undoIndex = -1; + this._undoStack.length = 0; + this._undoStackLength = 0; + this._isInUndoState = false; const range = this._getRangeAndRemoveBookmark() || createRange(root.firstElementChild || root, 0); this.saveUndoState(range); - this.setRange(range); + this.setSelection(range); + this._updatePath(range, true); return this; } /** @@ -2241,63 +2703,88 @@ class EditStack extends Array * replaced by the html being inserted. */ insertHTML(html, isPaste) { - let range = this.getSelection(); - if (isPaste) { - let startFragmentIndex = html.indexOf(""), - endFragmentIndex = html.lastIndexOf(""); - if (startFragmentIndex > -1 && endFragmentIndex > -1) { - html = html.slice(startFragmentIndex + 20, endFragmentIndex); - } - } - let frag = this._config.sanitizeToDOMFragment(html, isPaste); + const config = this._config; + let frag = config.sanitizeToDOMFragment(html, this); + const range = this.getSelection(); this.saveUndoState(range); try { - let root = this._root, node = frag; - addLinks(frag, frag); - cleanTree(frag); + const root = this._root; + if (config.addLinks) { + this.addDetectedLinks(frag, frag); + } + cleanTree(frag, this._config); cleanupBRs(frag, root, false); removeEmptyInlines(frag); frag.normalize(); + let node = frag; while (node = getNextBlock(node, frag)) { fixCursor(node); } - insertTreeFragmentIntoRange(range, frag, root); - range.collapse(); - moveRangeBoundaryOutOf(range, "A", root); - this._ensureBottomLine(); - this.setRange(range); - isPaste && this.focus(); + let doInsert = true; + if (isPaste) { + const event = new CustomEvent("willPaste", { + cancelable: true, + detail: { + html, + fragment: frag + } + }); + this.fireEvent("willPaste", event); + frag = event.detail.fragment; + doInsert = !event.defaultPrevented; + } + if (doInsert) { + insertTreeFragmentIntoRange(range, frag, root); + range.collapse(false); + moveRangeBoundaryOutOf(range, "A", root); + this._ensureBottomLine(); + } + this.setSelection(range); + this._updatePath(range, true); + if (isPaste) { + this.focus(); + } } catch (error) { - didError(error); + this._config.didError(error); } return this; } insertElement(el, range) { - range = range || this.getSelection(); + if (!range) { + range = this.getSelection(); + } range.collapse(true); if (isInline(el)) { insertNodeInRange(range, el); range.setStartAfter(el); } else { const root = this._root; - let splitNode = getStartBlockOfRange(range, root) || root; - let nodeAfterSplit; + const startNode = getStartBlockOfRange( + range, + root + ); + let splitNode = startNode || root; + let nodeAfterSplit = null; while (splitNode !== root && !splitNode.nextSibling) { splitNode = splitNode.parentNode; } if (splitNode !== root) { const parent = splitNode.parentNode; - nodeAfterSplit = split(parent, splitNode.nextSibling, root, root); + nodeAfterSplit = split( + parent, + splitNode.nextSibling, + root, + root + ); } - if (nodeAfterSplit) { - nodeAfterSplit.before(el); - } else { - root.append(el); - nodeAfterSplit = this.createDefaultBlock(); - root.append(nodeAfterSplit); + if (startNode && isEmptyBlock(startNode)) { + detach(startNode); } - range.setStart(nodeAfterSplit, 0); - range.setEnd(nodeAfterSplit, 0); + root.insertBefore(el, nodeAfterSplit); + const blankLine = this.createDefaultBlock(); + root.insertBefore(blankLine, nodeAfterSplit); + range.setStart(blankLine, 0); + range.setEnd(blankLine, 0); moveRangeBoundariesDownTree(range); } this.focus(); @@ -2306,39 +2793,78 @@ class EditStack extends Array return this; } insertImage(src, attributes) { - const img = createElement("IMG", mergeObjects({ - src: src - }, attributes, true)); + const img = createElement( + "IMG", + Object.assign( + { + src + }, + attributes + ) + ); this.insertElement(img); return img; } insertPlainText(plainText, isPaste) { const range = this.getSelection(); if (range.collapsed && getClosest(range.startContainer, this._root, "PRE")) { - let node = range.startContainer; + const startContainer = range.startContainer; let offset = range.startOffset; - let text; - if (!isTextNode(node)) { - text = document.createTextNode(""); - node?.childNodes[offset].before(text); - node = text; + let textNode; + if (!startContainer || !(startContainer instanceof Text)) { + const text = document.createTextNode(""); + startContainer.insertBefore( + text, + startContainer.childNodes[offset] + ); + textNode = text; offset = 0; + } else { + textNode = startContainer; + } + let doInsert = true; + if (isPaste) { + const event = new CustomEvent("willPaste", { + cancelable: true, + detail: { + text: plainText + } + }); + this.fireEvent("willPaste", event); + plainText = event.detail.text; + doInsert = !event.defaultPrevented; + } + if (doInsert) { + textNode.insertData(offset, plainText); + range.setStart(textNode, offset + plainText.length); + range.collapse(true); } - node.insertData(offset, plainText); - range.setStart(node, offset + plainText.length); - range.collapse(true); this.setSelection(range); return this; } - const lines = plainText.split(/\r?\n/), - closeBlock = "", - openBlock = "<" + blockTag + ">"; - lines.forEach((line, i) => { - line = escapeHTML(line).replace(/ (?=(?: |$))/g, NBSP); - lines[i] = i ? openBlock + (line || "
") + closeBlock : line; - }); + const lines = plainText.split("\n"); + const config = this._config; + const tag = config.blockTag; + const attributes = config.blockAttributes; + const closeBlock = ""; + let openBlock = "<" + tag; + for (const attr in attributes) { + openBlock += " " + attr + '="' + escapeHTML(attributes[attr]) + '"'; + } + openBlock += ">"; + for (let i = 0, l = lines.length; i < l; i += 1) { + let line = lines[i]; + line = escapeHTML(line).replace(/ (?=(?: |$))/g, " "); + if (i) { + line = openBlock + (line || "
") + closeBlock; + } + lines[i] = line; + } return this.insertHTML(lines.join(""), isPaste); } + getSelectedText(range) { + return getTextContentsOfRange(range || this.getSelection()); + } // --- Inline formatting /** * Extracts the font-family and font-size (if any) of the element @@ -2348,33 +2874,41 @@ class EditStack extends Array const fontInfo = { color: void 0, backgroundColor: void 0, - family: void 0, - size: void 0 + fontFamily: void 0, + fontSize: void 0 }; - range = range || this.getSelection(); + if (!range) { + range = this.getSelection(); + } + moveRangeBoundariesDownTree(range); let seenAttributes = 0; - let element = range.commonAncestorContainer, style, attr; - if (range.collapsed || isTextNode(element)) { - if (isTextNode(element)) { + let element = range.commonAncestorContainer; + if (range.collapsed || element instanceof Text) { + if (element instanceof Text) { element = element.parentNode; } while (seenAttributes < 4 && element) { - if (style = element.style) { - if (!fontInfo.color && (attr = style.color)) { - fontInfo.color = attr; - ++seenAttributes; + const style = element.style; + if (style) { + const color = style.color; + if (!fontInfo.color && color) { + fontInfo.color = color; + seenAttributes += 1; } - if (!fontInfo.backgroundColor && (attr = style.backgroundColor)) { - fontInfo.backgroundColor = attr; - ++seenAttributes; + const backgroundColor = style.backgroundColor; + if (!fontInfo.backgroundColor && backgroundColor) { + fontInfo.backgroundColor = backgroundColor; + seenAttributes += 1; } - if (!fontInfo.family && (attr = style.fontFamily)) { - fontInfo.family = attr; - ++seenAttributes; + const fontFamily = style.fontFamily; + if (!fontInfo.fontFamily && fontFamily) { + fontInfo.fontFamily = fontFamily; + seenAttributes += 1; } - if (!fontInfo.size && (attr = style.fontSize)) { - fontInfo.size = attr; - ++seenAttributes; + const fontSize = style.fontSize; + if (!fontInfo.fontSize && fontSize) { + fontInfo.fontSize = fontSize; + seenAttributes += 1; } } element = element.parentNode; @@ -2388,11 +2922,16 @@ class EditStack extends Array */ hasFormat(tag, attributes, range) { tag = tag.toUpperCase(); - range = range || this.getSelection(); - if (!range.collapsed && isTextNode(range.startContainer) && range.startOffset === range.startContainer.length && range.startContainer.nextSibling) { + if (!attributes) { + attributes = {}; + } + if (!range) { + range = this.getSelection(); + } + if (!range.collapsed && range.startContainer instanceof Text && range.startOffset === range.startContainer.length && range.startContainer.nextSibling) { range.setStartBefore(range.startContainer.nextSibling); } - if (!range.collapsed && isTextNode(range.endContainer) && range.endOffset === 0 && range.endContainer.previousSibling) { + if (!range.collapsed && range.endContainer instanceof Text && range.endOffset === 0 && range.endContainer.previousSibling) { range.setEndAfter(range.endContainer.previousSibling); } const root = this._root; @@ -2400,10 +2939,12 @@ class EditStack extends Array if (getNearest(common, root, tag, attributes)) { return true; } - if (isTextNode(common)) { + if (common instanceof Text) { return false; } - const walker = createTreeWalker(common, SHOW_TEXT, (node2) => isNodeContainedInRange(range, node2)); + const walker = createTreeWalker(common, SHOW_TEXT, (node2) => { + return isNodeContainedInRange(range, node2, true); + }); let seenNode = false; let node; while (node = walker.nextNode()) { @@ -2415,7 +2956,9 @@ class EditStack extends Array return seenNode; } changeFormat(add, remove, range, partial) { - range = range || this.getSelection(); + if (!range) { + range = this.getSelection(); + } this.saveUndoState(range); if (remove) { range = this._removeFormat( @@ -2432,16 +2975,18 @@ class EditStack extends Array range ); } - this.setRange(range); + this.setSelection(range); + this._updatePath(range, true); return this.focus(); } _addFormat(tag, attributes, range) { const root = this._root; - let node; if (range.collapsed) { const el = fixCursor(createElement(tag, attributes)); insertNodeInRange(range, el); - range.setStart(el.firstChild, el.firstChild.length); + const focusNode = el.firstChild || el; + const focusOffset = focusNode instanceof Text ? focusNode.length : 0; + range.setStart(focusNode, focusOffset); range.collapse(true); let block = el; while (isInline(block)) { @@ -2449,55 +2994,52 @@ class EditStack extends Array } removeZWS(block, el); } else { - const filter = (node) => (isTextNode(node) || isBrElement(node) || node.nodeName === "IMG") && isNodeContainedInRange(range, node); const walker = createTreeWalker( range.commonAncestorContainer, SHOW_ELEMENT_OR_TEXT, - filter + (node) => { + return (node instanceof Text || node.nodeName === "BR" || node.nodeName === "IMG") && isNodeContainedInRange(range, node, true); + } ); let { startContainer, startOffset, endContainer, endOffset } = range; walker.currentNode = startContainer; - if (!isElement(startContainer) && !isTextNode(startContainer) || !filter(startContainer)) { - startContainer = walker.nextNode(); + if (!(startContainer instanceof Element) && !(startContainer instanceof Text) || !walker.filter(startContainer)) { + const next = walker.nextNode(); + if (!next) { + return range; + } + startContainer = next; startOffset = 0; } - if (startContainer) { - do { - node = walker.currentNode; - if (!getNearest(node, root, tag, attributes)) { - if (node === endContainer && node.length > endOffset) { - node.splitText(endOffset); - } - if (node === startContainer && startOffset) { - node = node.splitText(startOffset); - if (endContainer === startContainer) { - endContainer = node; - endOffset -= startOffset; - } - startContainer = node; - startOffset = 0; - } - const el = createElement(tag, attributes); - replaceWith(node, el); - el.append(node); + do { + let node = walker.currentNode; + const needsFormat = !getNearest(node, root, tag, attributes); + if (needsFormat) { + if (node === endContainer && node.length > endOffset) { + node.splitText(endOffset); } - } while (walker.nextNode()); - if (!isTextNode(endContainer)) { - if (isTextNode(node)) { - endContainer = node; - endOffset = node.length; - } else { - endContainer = node.parentNode; - endOffset = 1; + if (node === startContainer && startOffset) { + node = node.splitText(startOffset); + if (endContainer === startContainer) { + endContainer = node; + endOffset -= startOffset; + } else if (endContainer === startContainer.parentNode) { + endOffset += 1; + } + startContainer = node; + startOffset = 0; } + const el = createElement(tag, attributes); + replaceWith(node, el); + el.append(node); } - range = createRange( - startContainer, - startOffset, - endContainer, - endOffset - ); - } + } while (walker.nextNode()); + range = createRange( + startContainer, + startOffset, + endContainer, + endOffset + ); } return range; } @@ -2505,30 +3047,35 @@ class EditStack extends Array this._saveRangeToBookmark(range); let fixer; if (range.collapsed) { - this._mayHaveZWS = cantFocusEmptyTextNodes; - fixer = document.createTextNode(cantFocusEmptyTextNodes ? ZWS : ""); + if (cantFocusEmptyTextNodes) { + fixer = document.createTextNode(ZWS); + } else { + fixer = document.createTextNode(""); + } insertNodeInRange(range, fixer); } let root = range.commonAncestorContainer; while (isInline(root)) { root = root.parentNode; } - const { startContainer, startOffset, endContainer, endOffset } = range; + const startContainer = range.startContainer; + const startOffset = range.startOffset; + const endContainer = range.endContainer; + const endOffset = range.endOffset; const toWrap = []; const examineNode = (node, exemplar) => { if (isNodeContainedInRange(range, node, false)) { return; } - let isText = isTextNode(node); let child; let next; - if (!isNodeContainedInRange(range, node)) { - if (node.nodeName !== "INPUT" && (!isText || node.data)) { + if (!isNodeContainedInRange(range, node, true)) { + if (!(node instanceof HTMLInputElement) && (!(node instanceof Text) || node.data)) { toWrap.push([exemplar, node]); } return; } - if (isText) { + if (node instanceof Text) { if (node === endContainer && endOffset !== node.length) { toWrap.push([exemplar, node.splitText(endOffset)]); } @@ -2543,17 +3090,24 @@ class EditStack extends Array } } }; - const formatTags = Array.prototype.filter.call( - root.getElementsByTagName(tag), - (el) => isNodeContainedInRange(range, el, true) && hasTagAttributes(el, tag, attributes) - ); - partial || formatTags.forEach((node) => examineNode(node, node)); + const formatTags = Array.from( + root.getElementsByTagName(tag) + ).filter((el) => { + return isNodeContainedInRange(range, el, true) && hasTagAttributes(el, tag, attributes); + }); + if (!partial) { + formatTags.forEach((node) => { + examineNode(node, node); + }); + } toWrap.forEach(([el, node]) => { el = el.cloneNode(false); replaceWith(node, el); el.append(node); }); - formatTags.forEach((el) => replaceWith(el, empty(el))); + formatTags.forEach((el) => { + replaceWith(el, empty(el)); + }); if (cantFocusEmptyTextNodes && fixer) { fixer = fixer.parentNode; let block = fixer; @@ -2565,7 +3119,9 @@ class EditStack extends Array } } this._getRangeAndRemoveBookmark(range); - fixer && range.collapse(); + if (fixer) { + range.collapse(false); + } mergeInlines(root, range); return range; } @@ -2592,17 +3148,23 @@ class EditStack extends Array makeLink(url, attributes) { const range = this.getSelection(); if (range.collapsed) { + let protocolEnd = url.indexOf(":") + 1; + if (protocolEnd) { + while (url[protocolEnd] === "/") { + protocolEnd += 1; + } + } insertNodeInRange( range, - document.createTextNode(url.replace(/^[^:]*:\/*/, "")) + document.createTextNode(url.slice(protocolEnd)) ); } - attributes = mergeObjects( - mergeObjects({ + attributes = Object.assign( + { href: url - }, attributes, true), - null, - false + }, + this._config.tagAttributes.a, + attributes ); return this.changeFormat( { @@ -2625,27 +3187,67 @@ class EditStack extends Array true ); } + addDetectedLinks(searchInNode, root) { + const walker = createTreeWalker( + searchInNode, + SHOW_TEXT, + (node2) => !getClosest(node2, root || this._root, "A") + ); + const linkRegExp = this.linkRegExp; + const defaultAttributes = this._config.tagAttributes.a; + let node; + while (node = walker.nextNode()) { + const parent = node.parentNode; + let data = node.data; + let match; + while (match = linkRegExp.exec(data)) { + const index = match.index; + const endIndex = index + match[0].length; + if (index) { + parent.insertBefore( + document.createTextNode(data.slice(0, index)), + node + ); + } + const child = createElement( + "A", + Object.assign( + { + href: match[1] ? /^(?:ht|f)tps?:/i.test(match[1]) ? match[1] : "http://" + match[1] : "mailto:" + match[0] + }, + defaultAttributes + ) + ); + child.textContent = data.slice(index, endIndex); + parent.insertBefore(child, node); + node.data = data = data.slice(endIndex); + } + } + return this; + } // --- Block formatting _ensureBottomLine() { const root = this._root; const last = root.lastElementChild; - if (!last || last.nodeName !== blockTag || !isBlock(last)) { + if (!last || last.nodeName !== this._config.blockTag || !isBlock(last)) { root.append(this.createDefaultBlock()); } } createDefaultBlock(children) { + const config = this._config; return fixCursor( - createElement(blockTag, null, children) + createElement(config.blockTag, config.blockAttributes, children) ); } splitBlock(lineBreakOnly, range) { - range = range || this.getSelection(); + if (!range) { + range = this.getSelection(); + } const root = this._root; let block; let parent; let node; let nodeAfterSplit; - this.editStack.inUndoState && this._docWasChanged(); this._recordUndoState(range); this._removeZWS(); this._getRangeAndRemoveBookmark(range); @@ -2654,8 +3256,10 @@ class EditStack extends Array } if (this._config.addLinks) { moveRangeBoundariesDownTree(range); + const textNode = range.startContainer; + const offset2 = range.startOffset; setTimeout(() => { - addLinks(range.startContainer, root); + linkifyText(this, textNode, offset2); }, 0); } block = getStartBlockOfRange(range, root); @@ -2663,11 +3267,11 @@ class EditStack extends Array moveRangeBoundariesDownTree(range); node = range.startContainer; const offset2 = range.startOffset; - if (!isTextNode(node)) { + if (!(node instanceof Text)) { node = document.createTextNode(""); parent.insertBefore(node, parent.firstChild); } - if (!lineBreakOnly && isTextNode(node) && (node.data.charAt(offset2 - 1) === "\n" || rangeDoesStartAtBlockBoundary(range, root)) && (node.data.charAt(offset2) === "\n" || rangeDoesEndAtBlockBoundary(range, root))) { + if (!lineBreakOnly && node instanceof Text && (node.data.charAt(offset2 - 1) === "\n" || rangeDoesStartAtBlockBoundary(range, root)) && (node.data.charAt(offset2) === "\n" || rangeDoesEndAtBlockBoundary(range, root))) { node.deleteData(offset2 && offset2 - 1, offset2 ? 2 : 1); nodeAfterSplit = split( node, @@ -2680,7 +3284,7 @@ class EditStack extends Array detach(node); } node = this.createDefaultBlock(); - nodeAfterSplit.before(node); + nodeAfterSplit.parentNode.insertBefore(node, nodeAfterSplit); if (!nodeAfterSplit.textContent) { detach(nodeAfterSplit); } @@ -2695,34 +3299,48 @@ class EditStack extends Array } } range.collapse(true); - this.setRange(range); + this.setSelection(range); + this._updatePath(range, true); this._docWasChanged(); - return; + return this; } if (!block || lineBreakOnly || /^T[HD]$/.test(block.nodeName)) { moveRangeBoundaryOutOf(range, "A", root); insertNodeInRange(range, createElement("BR")); - range.collapse(); - this.setRange(range); - return; + range.collapse(false); + this.setSelection(range); + this._updatePath(range, true); + return this; } - block = getClosest(block, root, "LI") || block; - if (isEmptyBlock(block) && (parent = getClosest(block, root, "UL,OL,BLOCKQUOTE"))) { - return "BLOCKQUOTE" === parent.nodeName - ? this.modifyBlocks((/* frag */) => this.createDefaultBlock(createBookmarkNodes()), range) - : this.decreaseListLevel(range); + if (parent = getClosest(block, root, "LI")) { + block = parent; + } + if (isEmptyBlock(block)) { + if (getClosest(block, root, "UL") || getClosest(block, root, "OL")) { + this.decreaseListLevel(range); + return this; + } else if (getClosest(block, root, "BLOCKQUOTE")) { + this.replaceWithBlankLine(range); + return this; + } } node = range.startContainer; const offset = range.startOffset; - let splitTag = tagAfterSplit[block.nodeName] || blockTag; + let splitTag = this.tagAfterSplit[block.nodeName]; nodeAfterSplit = split( node, offset, block.parentNode, - root + this._root ); - if (!hasTagAttributes(nodeAfterSplit, splitTag)) { - block = createElement(splitTag); + const config = this._config; + let splitProperties = null; + if (!splitTag) { + splitTag = config.blockTag; + splitProperties = config.blockAttributes; + } + if (!hasTagAttributes(nodeAfterSplit, splitTag, splitProperties)) { + block = createElement(splitTag, splitProperties); if (nodeAfterSplit.dir) { block.dir = nodeAfterSplit.dir; } @@ -2733,7 +3351,7 @@ class EditStack extends Array removeZWS(block); removeEmptyInlines(block); fixCursor(block); - while (isElement(nodeAfterSplit)) { + while (nodeAfterSplit instanceof Element) { let child = nodeAfterSplit.firstChild; let next; if (nodeAfterSplit.nodeName === "A" && (!nodeAfterSplit.textContent || nodeAfterSplit.textContent === ZWS)) { @@ -2742,29 +3360,68 @@ class EditStack extends Array nodeAfterSplit = child; break; } - while (isTextNode(child) && !child.data) { + while (child && child instanceof Text && !child.data) { next = child.nextSibling; - if (!next || isBrElement(next)) { + if (!next || next.nodeName === "BR") { break; } detach(child); child = next; } - if (!child || isBrElement(child) || isTextNode(child)) { + if (!child || child.nodeName === "BR" || child instanceof Text) { break; } nodeAfterSplit = child; } range = createRange(nodeAfterSplit, 0); - this.setRange(range); + this.setSelection(range); + this._updatePath(range, true); + return this; + } + forEachBlock(fn, mutates, range) { + if (!range) { + range = this.getSelection(); + } + if (mutates) { + this.saveUndoState(range); + } + const root = this._root; + let start = getStartBlockOfRange(range, root); + const end = getEndBlockOfRange(range, root); + if (start && end) { + do { + if (fn(start) || start === end) { + break; + } + } while (start = getNextBlock(start, root)); + } + if (mutates) { + this.setSelection(range); + this._updatePath(range, true); + } + return this; } modifyBlocks(modify, range) { - range = range || this.getSelection(); - this._recordUndoState(range, true); + if (!range) { + range = this.getSelection(); + } + this._recordUndoState(range, this._isInUndoState); const root = this._root; expandRangeToBlockBoundaries(range, root); moveRangeBoundariesUpTree(range, root, root, root); const frag = extractContentsOfRange(range, root, root); + if (!range.collapsed) { + let node = range.endContainer; + if (node === root) { + range.collapse(false); + } else { + while (node.parentNode !== root) { + node = node.parentNode; + } + range.setStartBefore(node); + range.collapse(true); + } + } insertNodeInRange(range, modify.call(this, frag)); if (range.endOffset < range.endContainer.childNodes.length) { mergeContainers( @@ -2777,108 +3434,181 @@ class EditStack extends Array root ); this._getRangeAndRemoveBookmark(range); - this.setRange(range); + this.setSelection(range); + this._updatePath(range, true); return this; } // --- + setTextAlignment(alignment) { + this.forEachBlock((block) => { + const className = block.className.split(/\s+/).filter((klass) => { + return !!klass && !/^align/.test(klass); + }).join(" "); + if (alignment) { + block.className = className + " align-" + alignment; + block.style.textAlign = alignment; + } else { + block.className = className; + block.style.textAlign = ""; + } + }, true); + return this.focus(); + } setTextDirection(direction) { - return this.modifyBlocks(frag => setDirection(this, frag, direction)).focus(); + this.forEachBlock((block) => { + if (direction) { + block.dir = direction; + } else { + block.removeAttribute("dir"); + } + }, true); + return this.focus(); + } + // --- + _getListSelection(range, root) { + let list = range.commonAncestorContainer; + let startLi = range.startContainer; + let endLi = range.endContainer; + while (list && list !== root && !/^[OU]L$/.test(list.nodeName)) { + list = list.parentNode; + } + if (!list || list === root) { + return null; + } + if (startLi === list) { + startLi = startLi.childNodes[range.startOffset]; + } + if (endLi === list) { + endLi = endLi.childNodes[range.endOffset]; + } + while (startLi && startLi.parentNode !== list) { + startLi = startLi.parentNode; + } + while (endLi && endLi.parentNode !== list) { + endLi = endLi.parentNode; + } + return [list, startLi, endLi]; } increaseListLevel(range) { - range = range || this.getSelection(); - const root = this._root; - const listSelection = getListSelection(range, root); - if (listSelection) { - let [list, startLi, endLi] = listSelection; - if (startLi && startLi !== list.firstChild) { - this._recordUndoState(range, true); - const type = list.nodeName; - let newParent = startLi.previousSibling; - let next; - if (newParent.nodeName !== type) { - newParent = createElement(type); - startLi.before(newParent); - } - do { - next = startLi === endLi ? null : startLi.nextSibling; - newParent.append(startLi); - } while (startLi = next); - next = newParent.nextSibling; - next && mergeContainers(next, root); - this._getRangeAndRemoveBookmark(range); - this.setRange(range); - } + if (!range) { + range = this.getSelection(); } + const root = this._root; + const listSelection = this._getListSelection(range, root); + if (!listSelection) { + return this.focus(); + } + let [list, startLi, endLi] = listSelection; + if (!startLi || startLi === list.firstChild) { + return this.focus(); + } + this._recordUndoState(range, this._isInUndoState); + const type = list.nodeName; + let newParent = startLi.previousSibling; + let listAttrs; + let next; + if (newParent.nodeName !== type) { + listAttrs = this._config.tagAttributes[type.toLowerCase()]; + newParent = createElement(type, listAttrs); + list.insertBefore(newParent, startLi); + } + do { + next = startLi === endLi ? null : startLi.nextSibling; + newParent.append(startLi); + } while (startLi = next); + next = newParent.nextSibling; + if (next) { + mergeContainers(next, root); + } + this._getRangeAndRemoveBookmark(range); + this.setSelection(range); + this._updatePath(range, true); return this.focus(); } decreaseListLevel(range) { - range = range || this.getSelection(); - const root = this._root; - const listSelection = getListSelection(range, root); - if (listSelection) { - let list = listSelection[0]; - let startLi = listSelection[1] || list.firstChild; - let endLi = listSelection[2] || list.lastChild; - let next, insertBefore; - this._recordUndoState(range, true); - if (startLi) { - let newParent = list.parentNode; - insertBefore = !endLi.nextSibling ? - list.nextSibling : - split(list, endLi.nextSibling, newParent, root); - if (newParent !== root && newParent.nodeName === "LI") { - newParent = newParent.parentNode; - while (insertBefore) { - next = insertBefore.nextSibling; - endLi.append(insertBefore); - insertBefore = next; - } - insertBefore = list.parentNode.nextSibling; - } - const makeNotList = !/^[OU]L$/.test(newParent.nodeName); - do { - next = startLi === endLi ? null : startLi.nextSibling; - startLi.remove(); - if (makeNotList && startLi.nodeName === "LI") { - startLi = this.createDefaultBlock([empty(startLi)]); - } - newParent.insertBefore(startLi, insertBefore); - } while (startLi = next); - } - list.firstChild || detach(list); - insertBefore && mergeContainers(insertBefore, root); - this._getRangeAndRemoveBookmark(range); - this.setRange(range); + if (!range) { + range = this.getSelection(); } + const root = this._root; + const listSelection = this._getListSelection(range, root); + if (!listSelection) { + return this.focus(); + } + let [list, startLi, endLi] = listSelection; + if (!startLi) { + startLi = list.firstChild; + } + if (!endLi) { + endLi = list.lastChild; + } + this._recordUndoState(range, this._isInUndoState); + let next; + let insertBefore = null; + if (startLi) { + let newParent = list.parentNode; + insertBefore = !endLi.nextSibling ? list.nextSibling : split(list, endLi.nextSibling, newParent, root); + if (newParent !== root && newParent.nodeName === "LI") { + newParent = newParent.parentNode; + while (insertBefore) { + next = insertBefore.nextSibling; + endLi.append(insertBefore); + insertBefore = next; + } + insertBefore = list.parentNode.nextSibling; + } + const makeNotList = !/^[OU]L$/.test(newParent.nodeName); + do { + next = startLi === endLi ? null : startLi.nextSibling; + list.removeChild(startLi); + if (makeNotList && startLi.nodeName === "LI") { + startLi = this.createDefaultBlock([empty(startLi)]); + } + newParent.insertBefore(startLi, insertBefore); + } while (startLi = next); + } + if (!list.firstChild) { + detach(list); + } + if (insertBefore) { + mergeContainers(insertBefore, root); + } + this._getRangeAndRemoveBookmark(range); + this.setSelection(range); + this._updatePath(range, true); return this.focus(); } _makeList(frag, type) { - let walker = getBlockWalker(frag, this._root), - node, tag, prev, newLi; + const walker = getBlockWalker(frag, this._root); + const tagAttributes = this._config.tagAttributes; + const listAttrs = tagAttributes[type.toLowerCase()]; + const listItemAttrs = tagAttributes.li; + let node; while (node = walker.nextNode()) { - if (node.parentNode.nodeName === "LI") { + if (node.parentNode instanceof HTMLLIElement) { node = node.parentNode; walker.currentNode = node.lastChild; } - if (node.nodeName !== "LI") { - newLi = createElement("LI"); + if (!(node instanceof HTMLLIElement)) { + const newLi = createElement("LI", listItemAttrs); if (node.dir) { newLi.dir = node.dir; } - if ((prev = node.previousSibling) && prev.nodeName === type) { + const prev = node.previousSibling; + if (prev && prev.nodeName === type) { prev.append(newLi); detach(node); } else { - replaceWith(node, createElement(type, null, [newLi])); + replaceWith(node, createElement(type, listAttrs, [newLi])); } newLi.append(empty(node)); walker.currentNode = newLi; } else { node = node.parentNode; - tag = node.nodeName; - if (tag !== type && listNodeNames.has(tag)) { - replaceWith(node, - createElement(type, null, [empty(node)]) + const tag = node.nodeName; + if (tag !== type && /^[OU]L$/.test(tag)) { + replaceWith( + node, + createElement(type, listAttrs, [empty(node)]) ); } } @@ -2886,106 +3616,163 @@ class EditStack extends Array return frag; } makeUnorderedList() { - return this.modifyBlocks((frag) => this._makeList(frag, "UL")).focus(); + this.modifyBlocks((frag) => this._makeList(frag, "UL")); + return this.focus(); } makeOrderedList() { - return this.modifyBlocks((frag) => this._makeList(frag, "OL")).focus(); + this.modifyBlocks((frag) => this._makeList(frag, "OL")); + return this.focus(); } removeList() { - return this.modifyBlocks((frag) => { + this.modifyBlocks((frag) => { + const lists = frag.querySelectorAll("UL, OL"); + const items = frag.querySelectorAll("LI"); const root = this._root; - frag.querySelectorAll("LI").forEach((item) => { + for (let i = 0, l = lists.length; i < l; i += 1) { + const list = lists[i]; + const listFrag = empty(list); + fixContainer(listFrag, root); + replaceWith(list, listFrag); + } + for (let i = 0, l = items.length; i < l; i += 1) { + const item = items[i]; if (isBlock(item)) { replaceWith(item, this.createDefaultBlock([empty(item)])); } else { fixContainer(item, root); replaceWith(item, empty(item)); } - }); - frag.querySelectorAll("UL, OL").forEach((list) => { - const listFrag = empty(list); - fixContainer(listFrag, root); - replaceWith(list, listFrag); - }); + } return frag; - }).focus(); + }); + return this.focus(); } // --- increaseQuoteLevel(range) { - return this.modifyBlocks( + this.modifyBlocks( (frag) => createElement( "BLOCKQUOTE", - null, + this._config.tagAttributes.blockquote, [frag] ), range - ).focus(); + ); + return this.focus(); } decreaseQuoteLevel(range) { - return this.modifyBlocks((frag) => { - Array.prototype.filter.call( - frag.querySelectorAll("blockquote"), - (el) => !getClosest(el.parentNode, frag, "BLOCKQUOTE") - ).forEach( - (el) => replaceWith(el, empty(el)) + this.modifyBlocks((frag) => { + Array.from(frag.querySelectorAll("blockquote")).filter((el) => { + return !getClosest(el.parentNode, frag, "BLOCKQUOTE"); + }).forEach((el) => { + replaceWith(el, empty(el)); + }); + return frag; + }, range); + return this.focus(); + } + removeQuote(range) { + this.modifyBlocks((frag) => { + Array.from(frag.querySelectorAll("blockquote")).forEach( + (el) => { + replaceWith(el, empty(el)); + } ); return frag; - }, range).focus(); + }, range); + return this.focus(); + } + replaceWithBlankLine(range) { + this.modifyBlocks( + () => this.createDefaultBlock([ + createElement("INPUT", { + id: this.startSelectionId, + type: "hidden" + }), + createElement("INPUT", { + id: this.endSelectionId, + type: "hidden" + }) + ]), + range + ); + return this.focus(); } // --- code() { const range = this.getSelection(); if (range.collapsed || isContainer(range.commonAncestorContainer)) { - return this.modifyBlocks((frag) => { + this.modifyBlocks((frag) => { const root = this._root; const output = document.createDocumentFragment(); - let walker = getBlockWalker(frag, root); + const blockWalker = getBlockWalker(frag, root); let node; - while (node = walker.nextNode()) { - node.querySelectorAll("BR").forEach(br => { - if (!isLineBreak(br, false)) { + while (node = blockWalker.nextNode()) { + let nodes = node.querySelectorAll("BR"); + const brBreaksLine = []; + let l = nodes.length; + for (let i = 0; i < l; i += 1) { + brBreaksLine[i] = isLineBreak(nodes[i], false); + } + while (l--) { + const br = nodes[l]; + if (!brBreaksLine[l]) { detach(br); } else { replaceWith(br, document.createTextNode("\n")); } - }); - node.querySelectorAll("CODE").forEach(el => detach(el)); + } + nodes = node.querySelectorAll("CODE"); + l = nodes.length; + while (l--) { + replaceWith(nodes[l], empty(nodes[l])); + } if (output.childNodes.length) { output.append(document.createTextNode("\n")); } output.append(empty(node)); } - walker = createTreeWalker(output, SHOW_TEXT); - while (node = walker.nextNode()) { - node.data = node.data.replace(NBSP, " "); // nbsp -> sp + const textWalker = createTreeWalker(output, SHOW_TEXT); + while (node = textWalker.nextNode()) { + node.data = node.data.replace(/ /g, " "); } output.normalize(); return fixCursor( - createElement("PRE", null, [ + createElement("PRE", this._config.tagAttributes.pre, [ output ]) ); - }, range).focus(); + }, range); + this.focus(); + } else { + this.changeFormat( + { + tag: "CODE", + attributes: this._config.tagAttributes.code + }, + null, + range + ); } - return this.changeFormat({ tag: "CODE" }, null, range); + return this; } removeCode() { const range = this.getSelection(); const ancestor = range.commonAncestorContainer; const inPre = getClosest(ancestor, this._root, "PRE"); if (inPre) { - return this.modifyBlocks((frag) => { + this.modifyBlocks((frag) => { const root = this._root; const pres = frag.querySelectorAll("PRE"); let l = pres.length; - let pre, walker, node, value, contents, index; while (l--) { - pre = pres[l]; - walker = createTreeWalker(pre, SHOW_TEXT); + const pre = pres[l]; + const walker = createTreeWalker(pre, SHOW_TEXT); + let node; while (node = walker.nextNode()) { - value = node.data; - value = value.replace(/ (?=)/g, NBSP); // sp -> nbsp - contents = document.createDocumentFragment(); + let value = node.data; + value = value.replace(/ (?= )/g, "\xA0"); + const contents = document.createDocumentFragment(); + let index; while ((index = value.indexOf("\n")) > -1) { contents.append( document.createTextNode(value.slice(0, index)) @@ -2993,25 +3780,35 @@ class EditStack extends Array contents.append(createElement("BR")); value = value.slice(index + 1); } - node.before(contents); + node.parentNode.insertBefore(contents, node); node.data = value; } fixContainer(pre, root); replaceWith(pre, empty(pre)); } return frag; - }, range).focus(); + }, range); + this.focus(); + } else { + this.changeFormat(null, { tag: "CODE" }, range); } - return this.changeFormat(null, { tag: "CODE" }, range); + return this; } toggleCode() { - return (this.hasFormat("PRE") || this.hasFormat("CODE")) ? this.removeCode() : this.code(); + if (this.hasFormat("PRE") || this.hasFormat("CODE")) { + this.removeCode(); + } else { + this.code(); + } + return this; } - // SnappyMail + /** + * SnappyMail + */ changeIndentationLevel(direction) { let parent = this.getSelectionClosest("UL,OL,BLOCKQUOTE"); if (parent || "increase" === direction) { - direction += (!parent || "BLOCKQUOTE" === parent.nodeName) ? "Quote" : "List"; + direction += !parent || "BLOCKQUOTE" === parent.nodeName ? "Quote" : "List"; return this[direction + "Level"](); } } @@ -3020,9 +3817,9 @@ class EditStack extends Array } setAttribute(name, value) { let range = this.getSelection(); - let start = range?.startContainer || {}; - let end = range?.endContainer || {}; - if ("dir" == name || (isTextNode(start) && 0 === range.startOffset && start === end && end.length === range.endOffset)) { + let start = (range == null ? void 0 : range.startContainer) || {}; + let end = (range == null ? void 0 : range.endContainer) || {}; + if ("dir" == name || isTextNode(start) && 0 === range.startOffset && start === end && end.length === range.endOffset) { this._recordUndoState(range); setAttributes(start.parentNode, { [name]: value }); this._docWasChanged(); @@ -3054,15 +3851,14 @@ class EditStack extends Array this.setSelection(range); this._updatePath(range, true); } - setConfig(config) { this._config = mergeObjects({ addLinks: true }, config, true); return this; } - } + }; // source/Legacy.ts - win.Squire = Squire; + window.Squire = Squire; })(); From 48acb9ca3808541df72d484ffd0afd1210151c8b Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Sun, 15 Sep 2024 14:41:37 +0200 Subject: [PATCH 06/57] Squire bugfixes --- vendors/squire/build/squire-raw.js | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/vendors/squire/build/squire-raw.js b/vendors/squire/build/squire-raw.js index 0f824dae1..beb3d0b0a 100644 --- a/vendors/squire/build/squire-raw.js +++ b/vendors/squire/build/squire-raw.js @@ -24,9 +24,9 @@ var createTreeWalker = (root, whatToShow, filter) => document.createTreeWalker( root, whatToShow, - filter ? { - acceptNode: (node) => filter(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP - } : null + { + acceptNode: (node) => !filter || filter(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP + } ); // source/Constants.ts @@ -100,12 +100,7 @@ props = null; } if (props) { - for (const attr in props) { - const value = props[attr]; - if (value !== void 0) { - el.setAttribute(attr, value); - } - } + setAttributes(el, props); } if (children) { children.forEach((node) => el.append(node)); @@ -200,6 +195,17 @@ node = (_a = node && !node.closest ? node.parentElement : node) == null ? void 0 : _a.closest(selector); return node && root.contains(node) ? node : null; }; + var setAttributes = (node, props) => { + props && Object.entries(props).forEach(([k, v]) => { + if (null == v) { + node.removeAttribute(k); + } else if ("style" === k && typeof v === "object") { + Object.entries(v).forEach(([k2, v2]) => node.style[k2] = v2); + } else { + node.setAttribute(k, v); + } + }); + }; // source/node/Whitespace.ts var notWSTextNode = (node) => { @@ -1273,7 +1279,7 @@ let textContent = ""; let addedTextInBlock = false; let value; - if (!(node instanceof Element) && !(node instanceof Text) || !walker.filter(node)) { + if (!(node instanceof Element) && !(node instanceof Text) || NodeFilter.FILTER_ACCEPT !== walker.filter.acceptNode(node)) { node = walker.nextNode(); } while (node) { @@ -3003,7 +3009,7 @@ ); let { startContainer, startOffset, endContainer, endOffset } = range; walker.currentNode = startContainer; - if (!(startContainer instanceof Element) && !(startContainer instanceof Text) || !walker.filter(startContainer)) { + if (!(startContainer instanceof Element) && !(startContainer instanceof Text) || NodeFilter.FILTER_ACCEPT !== walker.filter.acceptNode(startContainer)) { const next = walker.nextNode(); if (!next) { return range; @@ -3819,7 +3825,7 @@ let range = this.getSelection(); let start = (range == null ? void 0 : range.startContainer) || {}; let end = (range == null ? void 0 : range.endContainer) || {}; - if ("dir" == name || isTextNode(start) && 0 === range.startOffset && start === end && end.length === range.endOffset) { + if ("dir" == name || start instanceof Text && 0 === range.startOffset && start === end && end.length === range.endOffset) { this._recordUndoState(range); setAttributes(start.parentNode, { [name]: value }); this._docWasChanged(); From 12b6ed3dbbef7e49bde7269db1ff334fc531618d Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Sun, 15 Sep 2024 17:40:00 +0200 Subject: [PATCH 07/57] Improved SquireUI handling of pathChange and bugfix `bold` match --- dev/External/SquireUI.js | 19 +++++++++++----- vendors/squire/build/squire-raw.js | 35 ++++++++++-------------------- 2 files changed, 24 insertions(+), 30 deletions(-) diff --git a/dev/External/SquireUI.js b/dev/External/SquireUI.js index 563f8562f..e770433bd 100644 --- a/dev/External/SquireUI.js +++ b/dev/External/SquireUI.js @@ -155,7 +155,7 @@ class SquireUI html: 'B', cmd: () => this.doAction('bold'), key: 'B', - matches: 'B,STRONT' + matches: 'B,STRONG' }, italic: { html: 'I', @@ -477,15 +477,22 @@ class SquireUI // ----- - squire.addEventListener('pathChange', e => { + squire.addEventListener('pathChange', () => { const squireRoot = squire.getRoot(); - let elm = e.detail.element; - + let range = squire.getSelection(), + collapsed = range.collapsed, + elm = collapsed ? range.endContainer : range?.commonAncestorContainer; + if (elm && !(elm instanceof Element)) { + elm = elm.parentElement; + } forEachObjectValue(actions, entries => { forEachObjectValue(entries, cfg => { -// cfg.matches && cfg.input.classList.toggle('active', elm && elm.matches(cfg.matches)); - cfg.matches && cfg.input.classList.toggle('active', elm && elm.closestWithin(cfg.matches, squireRoot)); + // Check if selection has a matching parent or contains a matching element + cfg.matches && cfg.input.classList.toggle('active', !!(elm && ( + (!collapsed && [...elm.querySelectorAll(cfg.matches)].some(node => range.intersectsNode(node))) + || elm.closestWithin(cfg.matches, squireRoot) + ))); }); }); diff --git a/vendors/squire/build/squire-raw.js b/vendors/squire/build/squire-raw.js index beb3d0b0a..69a8e4707 100644 --- a/vendors/squire/build/squire-raw.js +++ b/vendors/squire/build/squire-raw.js @@ -99,12 +99,8 @@ children = props; props = null; } - if (props) { - setAttributes(el, props); - } - if (children) { - children.forEach((node) => el.append(node)); - } + setAttributes(el, props); + children && el.append(...children); return el; }; var areAlike = (node, node2) => { @@ -191,8 +187,7 @@ } }; var getClosest = (node, root, selector) => { - var _a; - node = (_a = node && !node.closest ? node.parentElement : node) == null ? void 0 : _a.closest(selector); + node = (node && !node.closest ? node.parentElement : node)?.closest(selector); return node && root.contains(node) ? node : null; }; var setAttributes = (node, props) => { @@ -256,17 +251,13 @@ // source/range/Boundaries.ts var START_TO_START = 0; - var START_TO_END = 1; var END_TO_END = 2; - var END_TO_START = 3; var isNodeContainedInRange = (range, node, partial) => { - const nodeRange = document.createRange(); - nodeRange.selectNode(node); if (partial) { - const nodeEndBeforeStart = range.compareBoundaryPoints(END_TO_START, nodeRange) > -1; - const nodeStartAfterEnd = range.compareBoundaryPoints(START_TO_END, nodeRange) < 1; - return !nodeEndBeforeStart && !nodeStartAfterEnd; + return range.intersectsNode(node); } else { + const nodeRange = document.createRange(); + nodeRange.selectNode(node); const nodeStartAfterStart = range.compareBoundaryPoints(START_TO_START, nodeRange) < 1; const nodeEndBeforeEnd = range.compareBoundaryPoints(END_TO_END, nodeRange) > -1; return nodeStartAfterStart && nodeEndBeforeEnd; @@ -1316,7 +1307,7 @@ const startBlock = getStartBlockOfRange(range, root); const endBlock = getEndBlockOfRange(range, root); let copyRoot = root; - if (startBlock === endBlock && (startBlock == null ? void 0 : startBlock.contains(range.commonAncestorContainer))) { + if (startBlock === endBlock && startBlock?.contains(range.commonAncestorContainer)) { copyRoot = startBlock; } let contents; @@ -1714,7 +1705,6 @@ // source/keyboard/Space.ts var Space = (self, event, range) => { - var _a; let node; const root = self._root; self._recordUndoState(range); @@ -1727,7 +1717,7 @@ } else if (rangeDoesEndAtBlockBoundary(range, root)) { const block = getStartBlockOfRange(range, root); if (block && block.nodeName !== "PRE") { - const text = (_a = block.textContent) == null ? void 0 : _a.trimEnd().replace(ZWS, ""); + const text = block.textContent?.trimEnd().replace(ZWS, ""); if (text === "*" || text === "1.") { event.preventDefault(); self.insertPlainText(" ", false); @@ -2380,10 +2370,7 @@ range = null; } } - if (!range) { - range = createRange(root.firstElementChild || root, 0); - } - return range; + return range || createRange(root.firstElementChild || root, 0); } setSelection(range) { this._lastSelection = range; @@ -3823,8 +3810,8 @@ } setAttribute(name, value) { let range = this.getSelection(); - let start = (range == null ? void 0 : range.startContainer) || {}; - let end = (range == null ? void 0 : range.endContainer) || {}; + let start = range?.startContainer || {}; + let end = range?.endContainer || {}; if ("dir" == name || start instanceof Text && 0 === range.startOffset && start === end && end.length === range.endOffset) { this._recordUndoState(range); setAttributes(start.parentNode, { [name]: value }); From 9a7ff9fe278b85b8c925fe171119167918c43a0e Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Sun, 15 Sep 2024 17:42:17 +0200 Subject: [PATCH 08/57] Resolve #1715 --- plugins/cache-memcache/Memcache.php | 5 ++++- plugins/cache-memcache/index.php | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/cache-memcache/Memcache.php b/plugins/cache-memcache/Memcache.php index f967ad687..30a2812c2 100644 --- a/plugins/cache-memcache/Memcache.php +++ b/plugins/cache-memcache/Memcache.php @@ -47,7 +47,10 @@ class Memcache implements \MailSo\Cache\DriverInterface public function Set(string $sKey, string $sValue) : bool { - return $this->oMem ? $this->oMem->set($this->generateCachedKey($sKey), $sValue, 0, $this->iExpire) : false; + if ($this->oMem instanceof \Memcache) { + return $this->oMem->set($this->generateCachedKey($sKey), $sValue, 0, $this->iExpire); + } + return $this->oMem ? $this->oMem->set($this->generateCachedKey($sKey), $sValue, $this->iExpire) : false; } public function Exists(string $sKey) : bool diff --git a/plugins/cache-memcache/index.php b/plugins/cache-memcache/index.php index 6a7354e13..7760ab398 100644 --- a/plugins/cache-memcache/index.php +++ b/plugins/cache-memcache/index.php @@ -4,8 +4,8 @@ class CacheMemcachePlugin extends \RainLoop\Plugins\AbstractPlugin { const NAME = 'Cache Memcache', - VERSION = '2.36', - RELEASE = '2024-03-22', + VERSION = '2.37', + RELEASE = '2024-09-15', REQUIRED = '2.36.0', CATEGORY = 'Cache', DESCRIPTION = 'Cache handler using PHP Memcache or PHP Memcached'; From 80f333118741d065881c0ec542a07577ef27e303 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Sun, 15 Sep 2024 18:31:30 +0200 Subject: [PATCH 09/57] Update Squire with SnappyMail changes --- vendors/squire/build/squire-raw.js | 65 +++++------------------------- 1 file changed, 9 insertions(+), 56 deletions(-) diff --git a/vendors/squire/build/squire-raw.js b/vendors/squire/build/squire-raw.js index 69a8e4707..6ba867e85 100644 --- a/vendors/squire/build/squire-raw.js +++ b/vendors/squire/build/squire-raw.js @@ -735,7 +735,7 @@ } }; var allowedBlock = /^(?:A(?:DDRESS|RTICLE|SIDE|UDIO)|BLOCKQUOTE|CAPTION|D(?:[DLT]|IV)|F(?:IGURE|IGCAPTION|OOTER)|H[1-6]|HEADER|L(?:ABEL|EGEND|I)|O(?:L|UTPUT)|P(?:RE)?|SECTION|T(?:ABLE|BODY|D|FOOT|H|HEAD|R)|COL(?:GROUP)?|UL)$/; - var blacklist = /^(?:HEAD|META|STYLE)/; + var blacklist = /* @__PURE__ */ new Set(["HEAD", "META", "STYLE"]); var cleanTree = (node, config, preserveWS) => { const children = node.childNodes; let nonInlineParent = node; @@ -746,72 +746,25 @@ nonInlineParent, SHOW_ELEMENT_OR_TEXT ); - for (let i = 0, l = children.length; i < l; i += 1) { + let i = children.length; + while (i--) { let child = children[i]; const nodeName = child.nodeName; - const rewriter = stylesRewriters[nodeName]; if (child instanceof HTMLElement) { const childLength = child.childNodes.length; - if (rewriter) { - child = rewriter(child, node, config); - } else if (blacklist.test(nodeName)) { - node.removeChild(child); - i -= 1; - l -= 1; + if (stylesRewriters[nodeName]) { + child = stylesRewriters[nodeName](child, node, config); + } else if (blacklist.has(nodeName)) { + child.remove(); continue; } else if (!allowedBlock.test(nodeName) && !isInline(child)) { - i -= 1; - l += childLength - 1; - node.replaceChild(empty(child), child); + i += childLength; + replaceWith(child, empty(child)); continue; } if (childLength) { cleanTree(child, config, preserveWS || nodeName === "PRE"); } - } else { - if (child instanceof Text) { - let data = child.data; - const startsWithWS = !notWS.test(data.charAt(0)); - const endsWithWS = !notWS.test(data.charAt(data.length - 1)); - if (preserveWS || !startsWithWS && !endsWithWS) { - continue; - } - if (startsWithWS) { - walker.currentNode = child; - let sibling; - while (sibling = walker.previousPONode()) { - if (sibling.nodeName === "IMG" || sibling instanceof Text && notWS.test(sibling.data)) { - break; - } - if (!isInline(sibling)) { - sibling = null; - break; - } - } - data = data.replace(/^[ \t\r\n]+/g, sibling ? " " : ""); - } - if (endsWithWS) { - walker.currentNode = child; - let sibling; - while (sibling = walker.nextNode()) { - if (sibling.nodeName === "IMG" || sibling instanceof Text && notWS.test(sibling.data)) { - break; - } - if (!isInline(sibling)) { - sibling = null; - break; - } - } - data = data.replace(/[ \t\r\n]+$/g, sibling ? " " : ""); - } - if (data) { - child.data = data; - continue; - } - } - node.removeChild(child); - i -= 1; - l -= 1; } } return node; From ed41d8e45f6c214f9c42b286e6ed30445ebf182d Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Sun, 15 Sep 2024 18:56:30 +0200 Subject: [PATCH 10/57] Implemen #1737 including an i18n fix --- plugins/nextcloud/index.php | 1 + plugins/nextcloud/js/message.js | 467 +++++++++++++++++- plugins/nextcloud/js/webdav.js | 52 +- plugins/nextcloud/langs/en.json | 14 +- .../templates/PopupsNextcloudInvites.html | 8 + 5 files changed, 530 insertions(+), 12 deletions(-) create mode 100644 plugins/nextcloud/templates/PopupsNextcloudInvites.html diff --git a/plugins/nextcloud/index.php b/plugins/nextcloud/index.php index 469ecf1b8..3bf4d65db 100644 --- a/plugins/nextcloud/index.php +++ b/plugins/nextcloud/index.php @@ -35,6 +35,7 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin $this->addTemplate('templates/PopupsNextcloudFiles.html'); $this->addTemplate('templates/PopupsNextcloudCalendars.html'); + $this->addTemplate('templates/PopupsNextcloudInvites.html'); // $this->addHook('login.credentials.step-2', 'loginCredentials2'); // $this->addHook('login.credentials', 'loginCredentials'); diff --git a/plugins/nextcloud/js/message.js b/plugins/nextcloud/js/message.js index f5154b745..bab6793a4 100644 --- a/plugins/nextcloud/js/message.js +++ b/plugins/nextcloud/js/message.js @@ -21,9 +21,18 @@ // https://github.com/nextcloud/calendar/issues/4684 if (cfg.CalDAV) { - attachmentsControls.append(Element.fromHTML(` + attachmentsControls.append(Element.fromHTML(` `)); + attachmentsControls.append(Element.fromHTML(` + + `)) + attachmentsControls.append(Element.fromHTML(` + + `)) + attachmentsControls.append(Element.fromHTML(` + + `)) } } /* @@ -107,18 +116,169 @@ }; view.nextcloudICS = ko.observable(null); + view.nextcloudICSOldInvitation = ko.observable(null); + view.nextcloudICSNewInvitation = ko.observable(null); + view.nextcloudICSLastInvitation = ko.observable(null); - view.nextcloudSaveICS = () => { + view.nextcloudICSShow = ko.observable(null); + + view.nextcloudICSCalendar = ko.observable(null); + view.filteredEventsUrls = ko.observable([]); + + view.nextcloudSaveICS = async () => { let VEVENT = view.nextcloudICS(); - VEVENT && rl.nextcloud.selectCalendar() - .then(href => href && rl.nextcloud.calendarPut(href, VEVENT)); + VEVENT = await view.handleUpdatedRecurrentEvents(VEVENT) + VEVENT && rl.nextcloud.selectCalendar(VEVENT) } + + view.handleUpdatedRecurrentEvents = async (VEVENT) => { + + const uid = VEVENT.UID + + let makePUTRequest = false + + const filteredEventUrls = view.filteredEventUrls + + if (filteredEventUrls.length > 1) { + // console.warn('filteredEventUrls.length > 1') + } + else if (filteredEventUrls.length == 1) { + const eventUrl = filteredEventUrls[0] + const eventText = await fetchEvent(eventUrl) + + // don't do anything for equal cards + if (VEVENT.rawText == eventText) { + return VEVENT + } + + const newVeventsText = extractVEVENTs(VEVENT.rawText) + const oldVeventsText = extractVEVENTs(eventText) + + // if there's only one event in the old card, it can't be an edit of the exception card + if (oldVeventsText.length == 1) { + const newRecurrenceId = extractProperties('RECURRENCE-ID', newVeventsText[0]) + + // if there's a property RECURRENCE-ID in the new card, it's an recurrence exception event + if (newRecurrenceId.length > 0) { + const updatedVEVENT = { + 'SUMMARY' : VEVENT.SUMMARY, + 'UID' : VEVENT.UID, + 'rawText' : mergeEventTexts(eventText, newVeventsText[0]) + } + VEVENT = updatedVEVENT + makePUTRequest = true + } + else { + return VEVENT + } + } + // if there's more than one event in the old card, it's possible to be an inclusion of a new exception card + // or update of old exception card + else { + let recurrenceIdMatch = false + + let isUpdate = false + const updateData = { + 'oldEventIndex': null, + 'newEventIndex': null, + } + + // check if it's an update + for (let i = 0; i < oldVeventsText.length; i++) { + let oldVeventText = oldVeventsText[i] + + for (let j = 0; j < newVeventsText.length; j++) { + let newVeventText = newVeventsText[j] + + let oldRecurrenceId = extractProperties('RECURRENCE-ID', oldVeventText) + let newRecurrenceId = extractProperties('RECURRENCE-ID', newVeventText) + let oldSequence = extractProperties('SEQUENCE', oldVeventText) + let newSequence = extractProperties('SEQUENCE', newVeventText) + + if (oldRecurrenceId.length == 0 || newRecurrenceId.length == 0 || oldSequence.length == 0 || newSequence.length == 0) { + continue + } + + if (oldRecurrenceId[0] == newRecurrenceId[0]) { + if (newSequence[0] > oldSequence[0]) { + isUpdate = true + + updateData.oldEventIndex = i + updateData.newEventIndex = j + + i = oldVeventsText.length + j = newVeventsText.length + } + } + } + } + + // if it's an update... + if (isUpdate) { + // substitute old event text for new event text + const oldEventStart = eventText.indexOf(oldVeventsText[updateData.oldEventIndex]) + const oldEventEnd = oldEventStart + oldVeventsText[updateData.oldEventIndex].length + + const newEvent = eventText.substring(0, oldEventStart) + newVeventsText[updateData.newEventIndex] + eventText.substring(oldEventEnd) + + const updatedVEVENT = { + 'SUMMARY' : VEVENT.SUMMARY, + 'UID': VEVENT.UID, + 'rawText' : newEvent + } + VEVENT = updatedVEVENT + makePUTRequest = true + } + // if it's not an update, it's an inclusion, as there's no match of RECURRENCE-ID + else { + const updatedVEVENT = { + 'SUMMARY' : VEVENT.SUMMARY, + 'UID' : VEVENT.UID, + 'rawText' : mergeEventTexts(eventText, newVeventsText[0]) + } + VEVENT = updatedVEVENT + makePUTRequest = true + } + } + } + + if (makePUTRequest) { + let href = "/" + (filteredEventUrls[0].split('/').slice(5, -1)[0]) + + rl.nextcloud.calendarPut(href, VEVENT, (response) => { + if (response.status != 201 && response.status != 204) { + InvitesPopupView.showModal([ + rl.i18n('NEXTCLOUD/EVENT_UPDATE_FAILURE_TITLE'), + rl.i18n('NEXTCLOUD/EVENT_UPDATE_FAILURE_BODY', {eventName: VEVENT.SUMMARY}) + ]) + return + } + InvitesPopupView.showModal([ + rl.i18n('NEXTCLOUD/EVENT_UPDATED_TITLE'), + rl.i18n('NEXTCLOUD/EVENT_UPDATED_BODY', {eventName: VEVENT.SUMMARY}) + ]) + }) + + return null + } + + return VEVENT + } + + + /** * TODO */ view.message.subscribe(msg => { view.nextcloudICS(null); + view.nextcloudICSOldInvitation(null); + view.nextcloudICSNewInvitation(null); + view.nextcloudICSLastInvitation(null); + view.nextcloudICSShow(view.nextcloudICS()) + + if (msg && cfg.CalDAV) { // let ics = msg.attachments.find(attachment => 'application/ics' == attachment.mimeType); let ics = msg.attachments.find(attachment => 'text/calendar' == attachment.mimeType); @@ -126,7 +286,7 @@ // fetch it and parse the VEVENT rl.fetch(ics.linkDownload()) .then(response => (response.status < 400) ? response.text() : Promise.reject(new Error({ response }))) - .then(text => { + .then(async (text) => { let VEVENT, VALARM, multiple = ['ATTACH','ATTENDEE','CATEGORIES','COMMENT','CONTACT','EXDATE', @@ -177,6 +337,158 @@ shouldReply: VEVENT.shouldReply() }); view.nextcloudICS(VEVENT); + view.nextcloudICSShow(true); + + + // try to get calendars, save + const calendarUrls = await fetchCalendarUrls() + + const filteredEventUrls = [] + for (let i = 0; i < calendarUrls.length; i++) { + let calendarUrl = calendarUrls[i] + + const skipCalendars = ['/inbox/', '/outbox/', '/trashbin/'] + let skip = false + for (let j = 0; j < skipCalendars.length; j++) { + if (calendarUrl.includes(skipCalendars[j])) { + skip = true + break + } + } + if (skip) { + continue + } + + // try to get event + const eventUrls = await fetchEventUrl(calendarUrl, VEVENT.UID) + + if (eventUrls.length == 0) { + continue + } + + eventUrls.forEach((url) => { + filteredEventUrls.push(url) + }) + } + view.filteredEventUrls = filteredEventUrls + + // if there's none, save in view.nextcloudICS + if (filteredEventUrls.length == 0) { + view.nextcloudICS(VEVENT); + view.nextcloudICSShow(true) + } + // if there's some... + else { + const savedEvent = await fetchEvent(filteredEventUrls[0]) + + const newVeventsText = extractVEVENTs(VEVENT.rawText) + const oldVeventsText = extractVEVENTs(savedEvent) + + // if there's more than one event in the old card, it's possible to be inclusion of new exception card + // or updated of old exception card + if (oldVeventsText.length > 1) { + let recurrenceIdMatch = false + + let oldNewestCreated = null + let newNewestCreated = null + + // check if it's an update + for (let i = 0; i < oldVeventsText.length; i++) { + let oldVeventText = oldVeventsText[i] + + for (let j = 0; j < newVeventsText.length; j++) { + let newVeventText = newVeventsText[j] + + let oldRecurrenceId = extractProperties('RECURRENCE-ID', oldVeventText) + let newRecurrenceId = extractProperties('RECURRENCE-ID', newVeventText) + let oldSequence = extractProperties('SEQUENCE', oldVeventText) + let newSequence = extractProperties('SEQUENCE', newVeventText) + + if (newRecurrenceId.length == 0 && oldRecurrenceId.length == 1) { + view.nextcloudICSShow(false) + view.nextcloudICSOldInvitation(true) + } + + if (oldRecurrenceId.length > 0 && newRecurrenceId.length > 0 && oldSequence.length > 0 && newSequence.length > 0) { + if (oldRecurrenceId[0] == newRecurrenceId[0]) { + if (newSequence[0] < oldSequence[0]) { + view.nextcloudICSOldInvitation(true) + view.nextcloudICSShow(false) + } + else if (newSequence[0] == oldSequence[0]) { + view.nextcloudICSLastInvitation(true) + view.nextcloudICSShow(false) + } + else if (newSequence[0] > oldSequence[0]) { + view.nextcloudICSNewInvitation(true) + view.nextcloudICSShow(false) + } + + // exit for loops + j = newVeventsText.length + i = oldVeventsText.length + } + } + + let oldCreated = extractProperties('CREATED', oldVeventText) + let newCreated = extractProperties('CREATED', newVeventText) + + if (oldCreated.length == 0 || newCreated.length == 0) { + continue + } + + const formattedOldDate = oldCreated[0].replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/, '$1-$2-$3T$4:$5:$6Z'); + const formattedNewDate = newCreated[0].replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/, '$1-$2-$3T$4:$5:$6Z'); + + const oldDate = new Date(formattedOldDate) + const newDate = new Date(formattedNewDate) + + if (oldNewestCreated == null || oldDate > oldNewestCreated) { + oldNewestCreated = oldDate + } + if (newNewestCreated == null || newDate > newNewestCreated) { + newNewestCreated = newDate + } + } + } + + if (newNewestCreated != null && oldNewestCreated != null) { + if (newNewestCreated < oldNewestCreated) { + view.nextcloudICSOldInvitation(true) + view.nextcloudICSShow(false) + } + else if (newNewestCreated == oldNewestCreated) { + view.nextcloudICSLastInvitation(true) + view.nextcloudICSShow(false) + } + } + } + else { + const oldLastModified = extractProperties('LAST-MODIFIED', oldVeventsText[0]) + const newLastModified = extractProperties('LAST-MODIFIED', newVeventsText[0]) + + if (oldLastModified.length == 1 && newLastModified.length == 1) { + const formattedOldDate = oldLastModified[0].replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/, '$1-$2-$3T$4:$5:$6Z'); + const formattedNewDate = newLastModified[0].replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z/, '$1-$2-$3T$4:$5:$6Z'); + + const oldDate = new Date(formattedOldDate) + const newDate = new Date(formattedNewDate) + + if (newDate > oldDate) { + view.nextcloudICSNewInvitation(true) + view.nextcloudICSShow(false) + } + else if (newDate < oldDate) { + view.nextcloudICSOldInvitation(true) + view.nextcloudICSShow(false) + } + else { + view.nextcloudICSLastInvitation(true) + view.nextcloudICSShow(false) + } + } + } + } } }); } @@ -186,3 +498,148 @@ }); })(window.rl); + + +async function fetchCalendarUrls () { + const username = OC.currentUser + const requestToken = OC.requestToken + + const url = '/remote.php/dav/calendars/' + username + const response = await fetch(url, { + 'method': 'PROPFIND', + 'headers': { + 'Depth': '1', + 'Content-Type': 'application/xml', + 'requesttoken' : requestToken + } + }) + + if (!response.ok) { + throw new Error('Error fetching calendars', response) + } + + const responseText = await response.text() + + const parser = new DOMParser() + const xmlDoc = parser.parseFromString(responseText, 'application/xml') + + const calendarUrls = [] + + const hrefElements = xmlDoc.getElementsByTagName('d:href') + for (let i = 1; i < hrefElements.length; i++) { + let calendarUrl = hrefElements[i].textContent.trim() + calendarUrls.push(calendarUrl) + } + + return calendarUrls +} + + +async function fetchEventUrl (calendarUrl, uid) { + const requestToken = OC.requestToken + + const xmlRequestBody = ` + + + + + + + + + ${uid} + + + + +` + + const response = await fetch(calendarUrl, { + 'method': 'REPORT', + 'headers': { + 'Content-Type': 'application/xml', + 'Depth': 1, + 'requesttoken': requestToken + }, + 'body': xmlRequestBody + }) + + const responseText = await response.text() + + const parser = new DOMParser() + const xmlDoc = parser.parseFromString(responseText, 'application/xml') + + const hrefElements = xmlDoc.getElementsByTagName('d:href') + const hrefValues = Array.from(hrefElements).map(element => element.textContent) + + return hrefValues +} + + +async function fetchEvent (eventUrl) { + const requestToken = OC.requestToken + const response = await fetch(eventUrl, { + 'method': 'GET', + 'headers': { + 'requesttoken': requestToken + } + }) + const responseText = await response.text() + + return responseText +} + + +function extractVEVENTs(text) { + let lastEndIndex = 0 + const vEvents = [] + + let textToSearch = "" + let beginIndex = 0 + let endIndex = 0 + let foundVevent = false + + while (true) { + textToSearch = text.substring(lastEndIndex) + + beginIndex = textToSearch.indexOf('BEGIN:VEVENT') + if (beginIndex == -1) { + break + } + endIndex = textToSearch.substring(beginIndex).indexOf('END:VEVENT') + if (endIndex == -1) { + break + } + endIndex += beginIndex + + lastEndIndex = lastEndIndex + endIndex + 'END:VEVENT'.length + + foundVevent = textToSearch.substring(beginIndex + 'BEGIN:VEVENT'.length, endIndex) + + vEvents.push(foundVevent) + } + + return vEvents +} + + +function extractProperties (property, text) { + const matches = text.match(`${property}.*`) + + if (matches == null) { + return [] + } + + const separatedMatches = matches.map((match) => { + return match.substring(property.length + 1) + }) + + return separatedMatches +} + + +function mergeEventTexts (oldEventText, newEventText) { + const appendIndex = oldEventText.indexOf('END:VEVENT') + 'END:VEVENT'.length + const updatedEventText = oldEventText.substring(0, appendIndex) + "\nBEGIN:VEVENT" + newEventText + "END:VEVENT" + oldEventText.substring(appendIndex) + return updatedEventText +} diff --git a/plugins/nextcloud/js/webdav.js b/plugins/nextcloud/js/webdav.js index 24aa1f226..4f2e2cafd 100644 --- a/plugins/nextcloud/js/webdav.js +++ b/plugins/nextcloud/js/webdav.js @@ -320,12 +320,28 @@ class NextcloudCalendarsPopupView extends rl.pluginPopupView { } onBuild(dom) { + let modalObj = this this.tree = dom.querySelector('#sm-nc-calendars'); this.tree.addEventListener('click', event => { let el = event.target; if (el.matches('button')) { this.select = el.href; - this.close(); + let VEVENT = this.VEVENT + + rl.nextcloud.calendarPut(this.select, this.VEVENT, (response) => { + if (response.status != 201 && response.status != 204) { + InvitesPopupView.showModal([ + rl.i18n('MESSAGE/EVENT_ADDITION_FAILURE_TITLE'), + rl.i18n('MESSAGE/EVENT_ADDITION_FAILURE_BODY', {eventName: VEVENT.SUMMARY}), + () => {modalObj.close()} + ]) + } + InvitesPopupView.showModal([ + rl.i18n('MESSAGE/EVENT_ADDED_TITLE'), + rl.i18n('MESSAGE/EVENT_ADDED_BODY', {eventName: VEVENT.SUMMARY}), + () => {modalObj.close()} + ]) + }) } }); } @@ -375,9 +391,10 @@ class NextcloudCalendarsPopupView extends rl.pluginPopupView { treeElement.appendChild(li); } // Happens after showModal() - beforeShow(fResolve) { + beforeShow(fResolve, VEVENT) { this.select = ''; this.fResolve = fResolve; + this.VEVENT = VEVENT this.tree.innerHTML = ''; davFetch('calendars', '/', { method: 'PROPFIND', @@ -432,14 +449,15 @@ close() {} } rl.nextcloud = { - selectCalendar: () => + selectCalendar: (VEVENT) => new Promise(resolve => { NextcloudCalendarsPopupView.showModal([ href => resolve(href), + VEVENT ]); }), - calendarPut: (path, event) => { + calendarPut: (path, event, callback) => { davFetch('calendars', path + '/' + event.UID + '.ics', { method: 'PUT', headers: { @@ -463,6 +481,8 @@ rl.nextcloud = { // response.text().then(text => console.error({status:response.status, body:text})); Promise.reject(new Error({ response })); } + + callback && callback(response) }); }, @@ -500,3 +520,27 @@ function getElementsInNamespaces(xmlDocument, tagName) { } return results; } + + +class InvitesPopupView extends rl.pluginPopupView { + constructor() { + super('NextcloudInvites') + } + + onBuild(dom) { + this.title = dom.querySelector('#sm-invites-popup-title') + this.body = dom.querySelector('#sm-invites-popup-body') + } + + beforeShow(title, body, onHide_) { + this.title.innerHTML = title + this.body.innerHTML = body + this.onHide_ = onHide_ + } + + onHide() { + if (this.onHide_) { + this.onHide_() + } + } +} diff --git a/plugins/nextcloud/langs/en.json b/plugins/nextcloud/langs/en.json index 2c19d11c2..d7843c3b9 100644 --- a/plugins/nextcloud/langs/en.json +++ b/plugins/nextcloud/langs/en.json @@ -2,13 +2,21 @@ "NEXTCLOUD": { "SAVE_ATTACHMENTS": "Save in Nextcloud", "SAVE_EML": "Save as .eml in Nextcloud", - "SAVE_ICS": "Add to calendar", "SELECT_FOLDER": "Select folder", "SELECT_FILES": "Select file(s)", "ATTACH_FILES": "Attach Nextcloud files", - "SELECT_CALENDAR": "Select calendar", "FILE_ATTACH": "attach", "FILE_INTERNAL": "internal", - "FILE_PUBLIC": "public" + "FILE_PUBLIC": "public", + + "SELECT_CALENDAR": "Select calendar", + "SAVE_ICS": "Add to calendar", + "OLD_INVITATION": "Old invitation", + "UPDATE_ON_MY_CALENDAR": "Update invitation", + "LAST_INVITATION": "Last invitation", + "EVENT_UPDATE_FAILURE_TITLE": "Update failed", + "EVENT_UPDATE_FAILURE_BODY": "Why, i have no clue", + "EVENT_UPDATED_TITLE": "Updated", + "EVENT_UPDATED_BODY": "Why, i have no clue" } } diff --git a/plugins/nextcloud/templates/PopupsNextcloudInvites.html b/plugins/nextcloud/templates/PopupsNextcloudInvites.html new file mode 100644 index 000000000..6056fb2ed --- /dev/null +++ b/plugins/nextcloud/templates/PopupsNextcloudInvites.html @@ -0,0 +1,8 @@ +
+ × +

+
+ +
+
From 61e7c00b62c6f00162a3e370bef4d8c5c2e601f8 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 16 Sep 2024 10:15:30 +0200 Subject: [PATCH 11/57] Resolve #1746 --- .../nextcloud/snappymail/lib/Util/SnappyMailHelper.php | 9 +++++---- plugins/nextcloud/index.php | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/integrations/nextcloud/snappymail/lib/Util/SnappyMailHelper.php b/integrations/nextcloud/snappymail/lib/Util/SnappyMailHelper.php index 5cf37e6e1..2f23916cd 100644 --- a/integrations/nextcloud/snappymail/lib/Util/SnappyMailHelper.php +++ b/integrations/nextcloud/snappymail/lib/Util/SnappyMailHelper.php @@ -92,7 +92,8 @@ class SnappyMailHelper if ($doLogin && $aCredentials[1] && $aCredentials[2]) { try { $ocSession = \OC::$server->getSession(); - if ($ocSession->get('is_oidc')) { + if (true === $aCredentials[2]) { + // OIDC $pwd = new \SnappyMail\SensitiveString($aCredentials[1]); $oAccount = $oActions->LoginProcess($aCredentials[1], $pwd); if ($oAccount) { @@ -102,7 +103,7 @@ class SnappyMailHelper $oAccount = $oActions->LoginProcess($aCredentials[1], $aCredentials[2]); if ($oAccount && $oConfig->Get('login', 'sign_me_auto', \RainLoop\Enumerations\SignMeType::DefaultOff) === \RainLoop\Enumerations\SignMeType::DefaultOn) { $oActions->SetSignMeToken($oAccount); - } + } } } catch (\Throwable $e) { // Login failure, reset password to prevent more attempts @@ -155,9 +156,9 @@ class SnappyMailHelper if ($config->getAppValue('snappymail', 'snappymail-autologin-oidc', false)) { if ($ocSession->get('is_oidc')) { // IToken->getPassword() ??? - if ($sAccessToken = $ocSession->get('oidc_access_token')) { + if ($ocSession->get('oidc_access_token')) { $sEmail = $config->getUserValue($sUID, 'settings', 'email'); - return [$sUID, $sEmail, $sAccessToken]; + return [$sUID, $sEmail, true]; } \SnappyMail\Log::debug('Nextcloud', 'OIDC access_token missing'); } else { diff --git a/plugins/nextcloud/index.php b/plugins/nextcloud/index.php index 3bf4d65db..68cc1fe65 100644 --- a/plugins/nextcloud/index.php +++ b/plugins/nextcloud/index.php @@ -109,6 +109,7 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin && \OC::$server->getSession()->get('is_oidc') && $sNcEmail === $oSettings->username && !$bAccountDefinedExplicitly + && $oAccount instanceof \RainLoop\Model\MainAccount // && $oClient->supportsAuthType('OAUTHBEARER') // v2.28 ) { $sAccessToken = \OC::$server->getSession()->get('oidc_access_token'); From f8520c27a288b335ca56f502c2b11015cb6eb90e Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 16 Sep 2024 10:17:26 +0200 Subject: [PATCH 12/57] Bump version number --- plugins/nextcloud/index.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/nextcloud/index.php b/plugins/nextcloud/index.php index 68cc1fe65..971dd29e7 100644 --- a/plugins/nextcloud/index.php +++ b/plugins/nextcloud/index.php @@ -4,8 +4,8 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin { const NAME = 'Nextcloud', - VERSION = '2.37.0', - RELEASE = '2024-08-11', + VERSION = '2.37.1', + RELEASE = '2024-09-16', CATEGORY = 'Integrations', DESCRIPTION = 'Integrate with Nextcloud v20+', REQUIRED = '2.36.2'; From 6f4f6bfd0325b8cc3054bf77580320fdc019b2cf Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 16 Sep 2024 13:49:59 +0200 Subject: [PATCH 13/57] Improved attempt for #1746 --- .../snappymail/lib/Util/SnappyMailHelper.php | 69 +++++++++++-------- plugins/nextcloud/index.php | 28 ++------ 2 files changed, 47 insertions(+), 50 deletions(-) diff --git a/integrations/nextcloud/snappymail/lib/Util/SnappyMailHelper.php b/integrations/nextcloud/snappymail/lib/Util/SnappyMailHelper.php index 2f23916cd..80a93d061 100644 --- a/integrations/nextcloud/snappymail/lib/Util/SnappyMailHelper.php +++ b/integrations/nextcloud/snappymail/lib/Util/SnappyMailHelper.php @@ -90,27 +90,23 @@ class SnappyMailHelper } */ if ($doLogin && $aCredentials[1] && $aCredentials[2]) { + $isOIDC = \str_starts_with($aCredentials[2], 'oidc_login|'); try { $ocSession = \OC::$server->getSession(); - if (true === $aCredentials[2]) { - // OIDC - $pwd = new \SnappyMail\SensitiveString($aCredentials[1]); - $oAccount = $oActions->LoginProcess($aCredentials[1], $pwd); - if ($oAccount) { - $oActions->SetSignMeToken($oAccount); - } - } else { - $oAccount = $oActions->LoginProcess($aCredentials[1], $aCredentials[2]); - if ($oAccount && $oConfig->Get('login', 'sign_me_auto', \RainLoop\Enumerations\SignMeType::DefaultOff) === \RainLoop\Enumerations\SignMeType::DefaultOn) { - $oActions->SetSignMeToken($oAccount); - } + $oAccount = $oActions->LoginProcess($aCredentials[1], $aCredentials[2]); + if (!$isOIDC && $oAccount + && $oConfig->Get('login', 'sign_me_auto', \RainLoop\Enumerations\SignMeType::DefaultOff) === \RainLoop\Enumerations\SignMeType::DefaultOn + ) { + $oActions->SetSignMeToken($oAccount); } } catch (\Throwable $e) { // Login failure, reset password to prevent more attempts - $sUID = \OC::$server->getUserSession()->getUser()->getUID(); - \OC::$server->getSession()['snappymail-passphrase'] = ''; - \OC::$server->getConfig()->setUserValue($sUID, 'snappymail', 'passphrase', ''); - \SnappyMail\Log::error('Nextcloud', $e->getMessage()); + if (!$isOIDC) { + $sUID = \OC::$server->getUserSession()->getUser()->getUID(); + \OC::$server->getSession()['snappymail-passphrase'] = ''; + \OC::$server->getConfig()->setUserValue($sUID, 'snappymail', 'passphrase', ''); + \SnappyMail\Log::error('Nextcloud', $e->getMessage()); + } } } } @@ -127,6 +123,32 @@ class SnappyMailHelper } } + // Check if OpenID Connect (OIDC) is enabled and used for login + // https://apps.nextcloud.com/apps/oidc_login + public static function isOIDCLogin() : bool + { + $config = \OC::$server->getConfig(); + if ($config->getAppValue('snappymail', 'snappymail-autologin-oidc', false)) { + // Check if the OIDC Login app is enabled + if (\OC::$server->getAppManager()->isEnabledForUser('oidc_login')) { + // Check if session is an OIDC Login + $ocSession = \OC::$server->getSession(); + if ($ocSession->get('is_oidc')) { + // IToken->getPassword() ??? + if ($ocSession->get('oidc_access_token')) { + return true; + } + \SnappyMail\Log::debug('Nextcloud', 'OIDC access_token missing'); + } else { + \SnappyMail\Log::debug('Nextcloud', 'No OIDC login'); + } + } else { + \SnappyMail\Log::debug('Nextcloud', 'OIDC login disabled'); + } + } + return false; + } + private static function getLoginCredentials() : array { $sUID = \OC::$server->getUserSession()->getUser()->getUID(); @@ -152,18 +174,9 @@ class SnappyMailHelper if ($ocSession['snappymail-nc-uid'] == $sUID) { // If OpenID Connect (OIDC) is enabled and used for login, use this. - // https://apps.nextcloud.com/apps/oidc_login - if ($config->getAppValue('snappymail', 'snappymail-autologin-oidc', false)) { - if ($ocSession->get('is_oidc')) { - // IToken->getPassword() ??? - if ($ocSession->get('oidc_access_token')) { - $sEmail = $config->getUserValue($sUID, 'settings', 'email'); - return [$sUID, $sEmail, true]; - } - \SnappyMail\Log::debug('Nextcloud', 'OIDC access_token missing'); - } else { - \SnappyMail\Log::debug('Nextcloud', 'No OIDC login'); - } + if (static::isOIDCLogin()) { + $sEmail = $config->getUserValue($sUID, 'settings', 'email'); + return [$sUID, $sEmail, "oidc_login|{$sUID}"]; } // Only use the user's password in the current session if they have diff --git a/plugins/nextcloud/index.php b/plugins/nextcloud/index.php index 971dd29e7..0e76a0cc4 100644 --- a/plugins/nextcloud/index.php +++ b/plugins/nextcloud/index.php @@ -90,33 +90,17 @@ class NextcloudPlugin extends \RainLoop\Plugins\AbstractPlugin public function beforeLogin(\RainLoop\Model\Account $oAccount, \MailSo\Net\NetClient $oClient, \MailSo\Net\ConnectSettings $oSettings) : void { - // https://apps.nextcloud.com/apps/oidc_login - $config = \OC::$server->getConfig(); - $oUser = \OC::$server->getUserSession()->getUser(); - $sUID = $oUser->getUID(); - - $sEmail = $config->getUserValue($sUID, 'snappymail', 'snappymail-email'); - $sPassword = $config->getUserValue($sUID, 'snappymail', 'passphrase') - ?: $config->getUserValue($sUID, 'snappymail', 'snappymail-password'); - $bAccountDefinedExplicitly = ($sEmail && $sPassword) && $sEmail === $oSettings->username; - - $sNcEmail = $oUser->getEMailAddress() ?: $oUser->getPrimaryEMailAddress(); - // Only login with OIDC access token if // it is enabled in config, the user is currently logged in with OIDC, // the current snappymail account is the OIDC account and no account defined explicitly - if (\OC::$server->getConfig()->getAppValue('snappymail', 'snappymail-autologin-oidc', false) - && \OC::$server->getSession()->get('is_oidc') - && $sNcEmail === $oSettings->username - && !$bAccountDefinedExplicitly - && $oAccount instanceof \RainLoop\Model\MainAccount + if ($oAccount instanceof \RainLoop\Model\MainAccount + && \OCA\SnappyMail\Util\SnappyMailHelper::isOIDCLogin() // && $oClient->supportsAuthType('OAUTHBEARER') // v2.28 + && \str_starts_with($oSettings->passphrase, 'oidc_login|') ) { - $sAccessToken = \OC::$server->getSession()->get('oidc_access_token'); - if ($sAccessToken) { - $oSettings->passphrase = $sAccessToken; - \array_unshift($oSettings->SASLMechanisms, 'OAUTHBEARER'); - } +// $oSettings->passphrase = \OC::$server->getSession()->get('snappymail-passphrase'); + $oSettings->passphrase = \OC::$server->getSession()->get('oidc_access_token'); + \array_unshift($oSettings->SASLMechanisms, 'OAUTHBEARER'); } } From e0236ea52dced5f120cf6bb2d460f466b48a0619 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 16 Sep 2024 14:25:09 +0200 Subject: [PATCH 14/57] Resolve #1733 --- vendors/bootstrap/less/forms.less | 6 ++++++ vendors/knockout/build/output/knockout-latest.debug.js | 3 ++- vendors/knockout/build/output/knockout-latest.js | 4 ++-- vendors/knockout/src/binding/defaultBindings/textInput.js | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/vendors/bootstrap/less/forms.less b/vendors/bootstrap/less/forms.less index 448ae63c1..f9da001c0 100644 --- a/vendors/bootstrap/less/forms.less +++ b/vendors/bootstrap/less/forms.less @@ -62,6 +62,12 @@ input { } } +//input:invalid, +input:user-invalid { + background-color: var(--error-bg-clr, #f2dede); + border-color: var(--error-border-clr, #eed3d7); + color: var(--error-clr, #b94a48); +} // Position radios and checkboxes better input[type="radio"], diff --git a/vendors/knockout/build/output/knockout-latest.debug.js b/vendors/knockout/build/output/knockout-latest.debug.js index 28307b49b..94087363f 100644 --- a/vendors/knockout/build/output/knockout-latest.debug.js +++ b/vendors/knockout/build/output/knockout-latest.debug.js @@ -51,6 +51,7 @@ ko.utils = { : node => node.cloneNode(true)), setDomNodeChildren: (domNode, childNodes) => { +// domNode.replaceChildren(...childNodes); ko.utils.emptyDomNode(domNode); childNodes && domNode.append(...childNodes); }, @@ -2821,7 +2822,7 @@ ko.bindingHandlers['textInput'] = { elementValueBeforeEvent = timeoutHandle = undefined; var elementValue = element.value; - if (previousElementValue !== elementValue) { + if (element.checkValidity() && previousElementValue !== elementValue) { // Provide a way for tests to know exactly which event was processed previousElementValue = elementValue; ko.expressionRewriting.writeValueToProperty(valueAccessor(), allBindings, 'textInput', elementValue); diff --git a/vendors/knockout/build/output/knockout-latest.js b/vendors/knockout/build/output/knockout-latest.js index 74ae8a38e..d76eca028 100644 --- a/vendors/knockout/build/output/knockout-latest.js +++ b/vendors/knockout/build/output/knockout-latest.js @@ -53,8 +53,8 @@ var ba={};c.i.options={init:a=>{if(!a.matches("SELECT"))throw Error("options bin e?f=b().map(c.C.M):0<=a.selectedIndex&&f.push(c.C.M(a.options[a.selectedIndex]));if(l){Array.isArray(l)||(l=[l]);var n=l.filter(m=>m??1)}var p=!1;l=k;d.has("optionsAfterRender")&&"function"==typeof d.get("optionsAfterRender")&&(l=(m,q)=>{k(m,q);c.u.I(d.get("optionsAfterRender"),null,[q[0],m!==ba?m:void 0])});c.g.Ab(a,n,(m,q,r)=>{r.length&&(f=r[0].selected?[c.C.M(r[0])]:[],p=!0);q=a.ownerDocument.createElement("option");m===ba?(c.g.Za(q),c.C.Fa(q,void 0)):(r=h(m,d.get("optionsValue"),m),c.C.Fa(q,c.g.h(r)), m=h(m,d.get("optionsText"),r),c.g.Za(q,m));return[q]},{},l);n=f.length;(e?n&&b().length{c.g.K(c.g.h(b()||{}),(d,e)=>{e=c.g.h(e);if(null==e||!1===e)e="";if(/^--/.test(d))a.style.setProperty(d,e);else{d=d.replace(/-(\w)/g,(l,f)=>f.toUpperCase());var g= a.style[d];a.style[d]=e;e===g||a.style[d]!=g||isNaN(e)||(a.style[d]=e+"px")}})}};c.i.submit={init:(a,b,d,e,g)=>{if("function"!=typeof b())throw Error("The value for a submit binding must be a function");a.addEventListener("submit",l=>{var f=b();try{var h=f.call(g.$data,a)}finally{!0!==h&&l.preventDefault()}})}};c.i.text={init:()=>({controlsDescendantBindings:!0}),update:(a,b)=>{8===a.nodeType&&(a.text||a.after(a.text=J.createTextNode("")),a=a.text);c.g.Za(a,b())}};c.m.aa.text=!0;c.i.textInput={init:(a, -b,d)=>{var e=a.value,g,l,f=()=>{clearTimeout(g);l=g=void 0;var k=a.value;e!==k&&(e=k,c.la.Ga(b(),d,"textInput",k))},h=()=>{var k=c.g.h(b())??"";void 0!==l&&k===l?setTimeout(h,4):a.value!==k&&(a.value=k,e=a.value)};a.addEventListener("input",f);a.addEventListener("change",f);c.o(h,{s:a})}};c.i.value={init:(a,b,d)=>{var e=a.matches("SELECT"),g=a.matches("INPUT");if(!g||"checkbox"!=a.type&&"radio"!=a.type){var l=new Set,f=d.get("valueUpdate"),h=null,k=()=>{h=null;var m=b(),q=c.C.M(a);c.la.Ga(m,d,"value", -q)};f&&("string"==typeof f?l.add(f):f.forEach(m=>l.add(m)),l.delete("change"));l.forEach(m=>{var q=k;(m||"").startsWith("after")&&(q=()=>{h=c.C.M(a);setTimeout(k,0)},m=m.slice(5));a.addEventListener(m,q)});var n=g&&"file"==a.type?()=>{var m=c.g.h(b());null==m||""===m?a.value="":c.u.I(k)}:()=>{var m=c.g.h(b()),q=c.C.M(a);if(null!==h&&m===h)setTimeout(n,0);else if(m!==q||void 0===q)e?(c.C.Fa(a,m),m!==c.C.M(a)&&c.u.I(k)):c.C.Fa(a,m)};if(e){var p;c.j.subscribe(a,c.j.F,()=>{p?d.get("valueAllowUnset")? +b,d)=>{var e=a.value,g,l,f=()=>{clearTimeout(g);l=g=void 0;var k=a.value;a.checkValidity()&&e!==k&&(e=k,c.la.Ga(b(),d,"textInput",k))},h=()=>{var k=c.g.h(b())??"";void 0!==l&&k===l?setTimeout(h,4):a.value!==k&&(a.value=k,e=a.value)};a.addEventListener("input",f);a.addEventListener("change",f);c.o(h,{s:a})}};c.i.value={init:(a,b,d)=>{var e=a.matches("SELECT"),g=a.matches("INPUT");if(!g||"checkbox"!=a.type&&"radio"!=a.type){var l=new Set,f=d.get("valueUpdate"),h=null,k=()=>{h=null;var m=b(),q=c.C.M(a); +c.la.Ga(m,d,"value",q)};f&&("string"==typeof f?l.add(f):f.forEach(m=>l.add(m)),l.delete("change"));l.forEach(m=>{var q=k;(m||"").startsWith("after")&&(q=()=>{h=c.C.M(a);setTimeout(k,0)},m=m.slice(5));a.addEventListener(m,q)});var n=g&&"file"==a.type?()=>{var m=c.g.h(b());null==m||""===m?a.value="":c.u.I(k)}:()=>{var m=c.g.h(b()),q=c.C.M(a);if(null!==h&&m===h)setTimeout(n,0);else if(m!==q||void 0===q)e?(c.C.Fa(a,m),m!==c.C.M(a)&&c.u.I(k)):c.C.Fa(a,m)};if(e){var p;c.j.subscribe(a,c.j.F,()=>{p?d.get("valueAllowUnset")? n():k():(a.addEventListener("change",k),p=c.o(n,{s:a}))},null,{notifyImmediately:!0})}else a.addEventListener("change",k),c.o(n,{s:a})}else c.applyBindingAccessorsToNode(a,{checkedValue:b})},update:()=>{}};c.i.visible={update:(a,b)=>{b=c.g.h(b());var d="none"!=a.style.display;b&&!d?a.style.display="":d&&!b&&(a.style.display="none")}};c.i.hidden={update:(a,b)=>a.hidden=!!c.g.h(b())};(function(a){c.i[a]={init:function(b,d,e,g,l){return c.i.event.init.call(this,b,()=>({[a]:d()}),e,g,l)}}})("click"); (()=>{let a=c.g.l.Z();class b{constructor(e){this.Na=e}Ua(...e){let g=this.Na;if(!e.length)return c.g.l.get(g,a)||(11===this.H?g.content:1===this.H?g:void 0);c.g.l.set(g,a,e[0])}}class d extends b{constructor(e){super(e);e&&(this.H=e.matches("TEMPLATE")&&e.content?e.content.nodeType:1)}}c.bb={Na:d,lb:b}})();(()=>{const a=(h,k,n)=>{var p;for(k=c.m.nextSibling(k);h&&(p=h)!==k;)h=c.m.nextSibling(p),n(p,h)},b=(h,k)=>{if(h.length){var n=h[0],p=n.parentNode;a(n,h[h.length-1],m=>(1===m.nodeType||8===m.nodeType)&& c.Ib(k,m));c.g.xa(h,p)}},d=(h,k,n,p)=>{var m=(h&&(h.nodeType?h:0 Date: Mon, 16 Sep 2024 14:28:19 +0200 Subject: [PATCH 15/57] Also prevent error for #1733 --- .../v/0.0.0/app/libraries/MailSo/Mail/MailClient.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php index a34dfdf0d..2b1d5b1a1 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php @@ -749,9 +749,14 @@ class MailClient */ public function MessageList(MessageListParams $oParams) : MessageCollection { - if (0 > $oParams->iOffset || 0 > $oParams->iLimit || 999 < $oParams->iLimit) { + if (0 > $oParams->iOffset || 0 > $oParams->iLimit) { throw new \ValueError; } + if (10 > $oParams->iLimit) { + $oParams->iLimit = 10; + } else if (999 < $oParams->iLimit) { + $oParams->iLimit = 50; + } $sSearch = \trim($oParams->sSearch); From 05812c6be197fcdf28e0133c7503d5d796cc3686 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 16 Sep 2024 14:30:54 +0200 Subject: [PATCH 16/57] Updated custom Squire 2.3.2 --- vendors/squire/build/squire-raw.js | 305 +++++++++++------------------ 1 file changed, 115 insertions(+), 190 deletions(-) diff --git a/vendors/squire/build/squire-raw.js b/vendors/squire/build/squire-raw.js index 6ba867e85..d12426e59 100644 --- a/vendors/squire/build/squire-raw.js +++ b/vendors/squire/build/squire-raw.js @@ -6,26 +6,39 @@ var SHOW_ELEMENT_OR_TEXT = 5; // source/node/TreeWalker.ts + var FILTER_ACCEPT = NodeFilter.FILTER_ACCEPT; TreeWalker.prototype.previousPONode = function() { + const root = this.root; let current = this.currentNode; - let node = current.lastChild; - while (!node && current) { - if (current === this.root) { - break; + let node; + while (true) { + node = current.lastChild; + while (!node && current) { + if (current === root) { + break; + } + node = current.previousSibling; + if (!node) { + current = current.parentNode; + } } - node = this.previousSibling(); if (!node) { - current = this.parentNode(); + return null; } + const nodeType = node.nodeType; + const nodeFilterType = nodeType === Node.ELEMENT_NODE ? NodeFilter.SHOW_ELEMENT : nodeType === Node.TEXT_NODE ? NodeFilter.SHOW_TEXT : 0; + if (!!(nodeFilterType & this.whatToShow) && FILTER_ACCEPT === this.filter.acceptNode(node)) { + this.currentNode = node; + return node; + } + current = node; } - node && (this.currentNode = node); - return node; }; var createTreeWalker = (root, whatToShow, filter) => document.createTreeWalker( root, whatToShow, { - acceptNode: (node) => !filter || filter(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP + acceptNode: (node) => !filter || filter(node) ? FILTER_ACCEPT : NodeFilter.FILTER_SKIP } ); @@ -46,7 +59,7 @@ // source/node/Category.ts var inlineNodeNames = /^(?:#text|A(?:BBR|CRONYM)?|B(?:R|D[IO])?|C(?:ITE|ODE)|D(?:ATA|EL|FN)|EM|FONT|HR|I(?:FRAME|MG|NPUT|NS)?|KBD|Q|R(?:P|T|UBY)|S(?:AMP|MALL|PAN|TR(?:IKE|ONG)|U[BP])?|TIME|U|VAR|WBR)$/; - var leafNodeNames = /* @__PURE__ */ new Set(["BR", "HR", "IFRAME", "IMG", "INPUT"]); + var leafNodeNames = /* @__PURE__ */ new Set(["BR", "HR", "IMG"]); var UNKNOWN = 0; var INLINE = 1; var BLOCK = 2; @@ -55,9 +68,7 @@ var resetNodeCategoryCache = () => { cache = /* @__PURE__ */ new WeakMap(); }; - var isLeaf = (node) => { - return leafNodeNames.has(node.nodeName); - }; + var isLeaf = (node) => leafNodeNames.has(node.nodeName); var getNodeCategory = (node) => { switch (node.nodeType) { case TEXT_NODE: @@ -82,15 +93,9 @@ cache.set(node, nodeCategory); return nodeCategory; }; - var isInline = (node) => { - return getNodeCategory(node) === INLINE; - }; - var isBlock = (node) => { - return getNodeCategory(node) === BLOCK; - }; - var isContainer = (node) => { - return getNodeCategory(node) === CONTAINER; - }; + var isInline = (node) => getNodeCategory(node) === INLINE; + var isBlock = (node) => getNodeCategory(node) === BLOCK; + var isContainer = (node) => getNodeCategory(node) === CONTAINER; // source/node/Node.ts var createElement = (tag, props, children) => { @@ -161,31 +166,17 @@ } return returnNode; }; - var getLength = (node) => { - return node instanceof Element || node instanceof DocumentFragment ? node.childNodes.length : node instanceof CharacterData ? node.length : 0; - }; + var getLength = (node) => node instanceof Element || node instanceof DocumentFragment ? node.childNodes.length : node instanceof CharacterData ? node.length : 0; var empty = (node) => { const frag = document.createDocumentFragment(); - let child = node.firstChild; - while (child) { - frag.append(child); - child = node.firstChild; - } + frag.append(...node.childNodes); return frag; }; var detach = (node) => { - const parent = node.parentNode; - if (parent) { - parent.removeChild(node); - } + node.parentNode?.removeChild(node); return node; }; - var replaceWith = (node, node2) => { - const parent = node.parentNode; - if (parent) { - parent.replaceChild(node2, node); - } - }; + var replaceWith = (node, node2) => node.parentNode?.replaceChild(node2, node); var getClosest = (node, root, selector) => { node = (node && !node.closest ? node.parentElement : node)?.closest(selector); return node && root.contains(node) ? node : null; @@ -203,12 +194,10 @@ }; // source/node/Whitespace.ts - var notWSTextNode = (node) => { - return node instanceof Element ? node.nodeName === "BR" : ( - // okay if data is 'undefined' here. - notWS.test(node.data) - ); - }; + var notWSTextNode = (node) => node instanceof Element ? node.nodeName === "BR" : ( + // okay if data is 'undefined' here. + notWS.test(node.data) + ); var isLineBreak = (br, isLBIfEmptyBlock) => { let block = br.parentNode; while (isInline(block)) { @@ -291,7 +280,7 @@ const child = endContainer.childNodes[endOffset - 1]; if (!child || isLeaf(child)) { if (child && child.nodeName === "BR" && !isLineBreak(child, false)) { - endOffset -= 1; + --endOffset; continue; } break; @@ -335,7 +324,7 @@ break; } if (endContainer.nodeType !== TEXT_NODE && endContainer.childNodes[endOffset] && endContainer.childNodes[endOffset].nodeName === "BR" && !isLineBreak(endContainer.childNodes[endOffset], false)) { - endOffset += 1; + ++endOffset; } if (endOffset !== getLength(endContainer)) { break; @@ -400,32 +389,24 @@ }; var fixContainer = (container, root) => { let wrapper = null; - Array.from(container.childNodes).forEach((child) => { + [...container.childNodes].forEach((child) => { const isBR = child.nodeName === "BR"; - if (!isBR && isInline(child)) { - if (!wrapper) { - wrapper = createElement("DIV"); - } + if (!isBR && child.parentNode == root && isInline(child)) { + wrapper || (wrapper = createElement("DIV")); wrapper.append(child); } else if (isBR || wrapper) { - if (!wrapper) { - wrapper = createElement("DIV"); - } + wrapper || (wrapper = createElement("DIV")); fixCursor(wrapper); if (isBR) { - container.replaceChild(wrapper, child); + child.replaceWith(wrapper); } else { container.insertBefore(wrapper, child); } wrapper = null; } - if (isContainer(child)) { - fixContainer(child, root); - } + isContainer(child) && fixContainer(child, root); }); - if (wrapper) { - container.append(fixCursor(wrapper)); - } + wrapper && container.append(fixCursor(wrapper)); return container; }; var split = (node, offset, stopNode, root) => { @@ -454,7 +435,7 @@ } fixCursor(node); fixCursor(clone); - parent.insertBefore(clone, node.nextSibling); + node.after(clone); return split(parent, clone, stopNode, root); }; var _mergeInlines = (node, fakeRange) => { @@ -475,7 +456,7 @@ } if (fakeRange.startContainer === node) { if (fakeRange.startOffset > l) { - fakeRange.startOffset -= 1; + --fakeRange.startOffset; } else if (fakeRange.startOffset === l) { fakeRange.startContainer = prev; fakeRange.startOffset = getLength(prev); @@ -483,7 +464,7 @@ } if (fakeRange.endContainer === node) { if (fakeRange.endOffset > l) { - fakeRange.endOffset -= 1; + --fakeRange.endOffset; } else if (fakeRange.endOffset === l) { fakeRange.endContainer = prev; fakeRange.endOffset = getLength(prev); @@ -529,8 +510,8 @@ offset = block.childNodes.length; const last = block.lastChild; if (last && last.nodeName === "BR") { - block.removeChild(last); - offset -= 1; + last.remove(); + --offset; } block.append(empty(next)); range.setStart(block, offset); @@ -546,23 +527,18 @@ } if (prev && areAlike(prev, node)) { if (!isContainer(prev)) { - if (isListItem) { - const block = createElement("DIV"); - block.append(empty(prev)); - prev.append(block); - } else { + if (!isListItem) { return; } + const block = createElement("DIV"); + block.append(empty(prev)); + prev.append(block); } detach(node); const needsFix = !isContainer(node); prev.append(empty(node)); - if (needsFix) { - fixContainer(prev, root); - } - if (first) { - mergeContainers(first, root); - } + needsFix && fixContainer(prev, root); + first && mergeContainers(first, root); } else if (isListItem) { const block = createElement("DIV"); node.insertBefore(block, first); @@ -645,7 +621,7 @@ return (node, parent) => { const el = createElement(tag); const attributes = node.attributes; - for (let i = 0, l = attributes.length; i < l; i += 1) { + for (let i = 0, l = attributes.length; i < l; ++i) { const attribute = attributes[i]; el.setAttribute(attribute.name, attribute.value); } @@ -655,13 +631,15 @@ }; }; var fontSizes = { - "1": "10", - "2": "13", - "3": "16", - "4": "18", - "5": "24", - "6": "32", - "7": "48" + "1": "x-small", + "2": "small", + "3": "medium", + "4": "large", + "5": "x-large", + "6": "xx-large", + "7": "xxx-large", + "-1": "smaller", + "+1": "larger" }; var stylesRewriters = { STRONG: replaceWithTag("B"), @@ -674,62 +652,31 @@ const face = font.face; const size = font.size; let color = font.color; - const classNames = config.classNames; - let fontSpan; - let sizeSpan; - let colorSpan; - let newTreeBottom; - let newTreeTop; + let newTag = createElement("SPAN"); + let css = newTag.style; + newTag.style.cssText = node.style.cssText; if (face) { - fontSpan = createElement("SPAN", { - class: classNames.fontFamily, - style: "font-family:" + face - }); - newTreeTop = fontSpan; - newTreeBottom = fontSpan; + css.fontFamily = face; } if (size) { - sizeSpan = createElement("SPAN", { - class: classNames.fontSize, - style: "font-size:" + fontSizes[size] + "px" - }); - if (!newTreeTop) { - newTreeTop = sizeSpan; - } - if (newTreeBottom) { - newTreeBottom.append(sizeSpan); - } - newTreeBottom = sizeSpan; + css.fontSize = fontSizes[size]; } if (color && /^#?([\dA-F]{3}){1,2}$/i.test(color)) { if (color.charAt(0) !== "#") { color = "#" + color; } - colorSpan = createElement("SPAN", { - class: classNames.color, - style: "color:" + color - }); - if (!newTreeTop) { - newTreeTop = colorSpan; - } - if (newTreeBottom) { - newTreeBottom.append(colorSpan); - } - newTreeBottom = colorSpan; + css.color = color; } - if (!newTreeTop || !newTreeBottom) { - newTreeTop = newTreeBottom = createElement("SPAN"); - } - parent.replaceChild(newTreeTop, font); - newTreeBottom.append(empty(font)); - return newTreeBottom; + replaceWith(node, newTag); + newTag.append(empty(node)); + return newTag; }, TT: (node, parent, config) => { const el = createElement("SPAN", { class: config.classNames.fontFamily, style: 'font-family:menlo,consolas,"courier new",monospace' }); - parent.replaceChild(el, node); + replaceWith(node, el); el.append(empty(node)); return el; } @@ -788,7 +735,7 @@ const brs = node.querySelectorAll("BR"); const brBreaksLine = []; let l = brs.length; - for (let i = 0; i < l; i += 1) { + for (let i = 0; i < l; ++i) { brBreaksLine[i] = isLineBreak(brs[i], keepForBlankLine); } while (l--) { @@ -869,7 +816,7 @@ let nodeAfterCursor; if (startContainer instanceof Text) { const text = startContainer.data; - for (let i = startOffset; i > 0; i -= 1) { + for (let i = startOffset; i > 0; --i) { if (text.charAt(i - 1) !== ZWS) { return false; } @@ -906,7 +853,7 @@ if (endContainer instanceof Text) { const text = endContainer.data; const length = text.length; - for (let i = endOffset; i < length; i += 1) { + for (let i = endOffset; i < length; ++i) { if (text.charAt(i) !== ZWS) { return false; } @@ -969,7 +916,7 @@ endOffset -= startOffset; endContainer = afterSplit; } else if (endContainer === parent) { - endOffset += 1; + ++endOffset; } startContainer = afterSplit; } @@ -1223,7 +1170,7 @@ let textContent = ""; let addedTextInBlock = false; let value; - if (!(node instanceof Element) && !(node instanceof Text) || NodeFilter.FILTER_ACCEPT !== walker.filter.acceptNode(node)) { + if (!(node instanceof Element) && !(node instanceof Text) || FILTER_ACCEPT !== walker.filter.acceptNode(node)) { node = walker.nextNode(); } while (node) { @@ -2271,7 +2218,7 @@ ); let endOffset = Array.from(endContainer.childNodes).indexOf(end); if (startContainer === endContainer) { - endOffset -= 1; + --endOffset; } start.remove(); end.remove(); @@ -2509,7 +2456,7 @@ } const html = this._getRawHTML(); if (replace) { - undoIndex -= 1; + --undoIndex; } if (undoThreshold > -1 && html.length * 2 > undoThreshold) { if (undoLimit > -1 && undoIndex > undoLimit) { @@ -2520,15 +2467,13 @@ } undoStack[undoIndex] = html; this._undoIndex = undoIndex; - this._undoStackLength += 1; + ++this._undoStackLength; this._isInUndoState = true; } return this; } saveUndoState(range) { - if (!range) { - range = this.getSelection(); - } + range || (range = this.getSelection()); this._recordUndoState(range, this._isInUndoState); this._getRangeAndRemoveBookmark(range); return this; @@ -2536,7 +2481,7 @@ undo() { if (this._undoIndex !== 0 || !this._isInUndoState) { this._recordUndoState(this.getSelection(), false); - this._undoIndex -= 1; + --this._undoIndex; this._setRawHTML(this._undoStack[this._undoIndex]); const range = this._getRangeAndRemoveBookmark(); if (range) { @@ -2555,7 +2500,7 @@ const undoIndex = this._undoIndex; const undoStackLength = this._undoStackLength; if (undoIndex + 1 < undoStackLength && this._isInUndoState) { - this._undoIndex += 1; + ++this._undoIndex; this._setRawHTML(this._undoStack[this._undoIndex]); const range = this._getRangeAndRemoveBookmark(); if (range) { @@ -2577,23 +2522,15 @@ return this._root.innerHTML; } _setRawHTML(html) { - const root = this._root; - root.innerHTML = html; - let node = root; - const child = node.firstChild; - if (!child || child.nodeName === "BR") { - const block = this.createDefaultBlock(); - if (child) { - node.replaceChild(block, child); - } else { - node.append(block); - } - } else { - while (node = getNextBlock(node, root)) { + if (html !== void 0) { + const root = this._root; + let node = root; + root.innerHTML = html; + do { fixCursor(node); - } + } while (node = getNextBlock(node, root)); + this._ignoreChange = true; } - this._ignoreChange = true; return this; } getHTML(withBookmark) { @@ -2798,7 +2735,7 @@ openBlock += " " + attr + '="' + escapeHTML(attributes[attr]) + '"'; } openBlock += ">"; - for (let i = 0, l = lines.length; i < l; i += 1) { + for (let i = 0, l = lines.length; i < l; ++i) { let line = lines[i]; line = escapeHTML(line).replace(/ (?=(?: |$))/g, " "); if (i) { @@ -2839,22 +2776,22 @@ const color = style.color; if (!fontInfo.color && color) { fontInfo.color = color; - seenAttributes += 1; + ++seenAttributes; } const backgroundColor = style.backgroundColor; if (!fontInfo.backgroundColor && backgroundColor) { fontInfo.backgroundColor = backgroundColor; - seenAttributes += 1; + ++seenAttributes; } const fontFamily = style.fontFamily; if (!fontInfo.fontFamily && fontFamily) { fontInfo.fontFamily = fontFamily; - seenAttributes += 1; + ++seenAttributes; } const fontSize = style.fontSize; if (!fontInfo.fontSize && fontSize) { fontInfo.fontSize = fontSize; - seenAttributes += 1; + ++seenAttributes; } } element = element.parentNode; @@ -2868,12 +2805,8 @@ */ hasFormat(tag, attributes, range) { tag = tag.toUpperCase(); - if (!attributes) { - attributes = {}; - } - if (!range) { - range = this.getSelection(); - } + attributes || (attributes = {}); + range || (range = this.getSelection()); if (!range.collapsed && range.startContainer instanceof Text && range.startOffset === range.startContainer.length && range.startContainer.nextSibling) { range.setStartBefore(range.startContainer.nextSibling); } @@ -2888,9 +2821,11 @@ if (common instanceof Text) { return false; } - const walker = createTreeWalker(common, SHOW_TEXT, (node2) => { - return isNodeContainedInRange(range, node2, true); - }); + const walker = createTreeWalker( + common, + SHOW_TEXT, + (node2) => isNodeContainedInRange(range, node2, true) + ); let seenNode = false; let node; while (node = walker.nextNode()) { @@ -2949,7 +2884,7 @@ ); let { startContainer, startOffset, endContainer, endOffset } = range; walker.currentNode = startContainer; - if (!(startContainer instanceof Element) && !(startContainer instanceof Text) || NodeFilter.FILTER_ACCEPT !== walker.filter.acceptNode(startContainer)) { + if (!(startContainer instanceof Element) && !(startContainer instanceof Text) || FILTER_ACCEPT !== walker.filter.acceptNode(startContainer)) { const next = walker.nextNode(); if (!next) { return range; @@ -2970,7 +2905,7 @@ endContainer = node; endOffset -= startOffset; } else if (endContainer === startContainer.parentNode) { - endOffset += 1; + ++endOffset; } startContainer = node; startOffset = 0; @@ -3041,33 +2976,23 @@ ).filter((el) => { return isNodeContainedInRange(range, el, true) && hasTagAttributes(el, tag, attributes); }); - if (!partial) { - formatTags.forEach((node) => { - examineNode(node, node); - }); - } + partial || formatTags.forEach((node) => examineNode(node, node)); toWrap.forEach(([el, node]) => { el = el.cloneNode(false); replaceWith(node, el); el.append(node); }); - formatTags.forEach((el) => { - replaceWith(el, empty(el)); - }); + formatTags.forEach((el) => replaceWith(el, empty(el))); if (cantFocusEmptyTextNodes && fixer) { fixer = fixer.parentNode; let block = fixer; while (block && isInline(block)) { block = block.parentNode; } - if (block) { - removeZWS(block, fixer); - } + block && removeZWS(block, fixer); } this._getRangeAndRemoveBookmark(range); - if (fixer) { - range.collapse(false); - } + fixer && range.collapse(false); mergeInlines(root, range); return range; } @@ -3097,7 +3022,7 @@ let protocolEnd = url.indexOf(":") + 1; if (protocolEnd) { while (url[protocolEnd] === "/") { - protocolEnd += 1; + ++protocolEnd; } } insertNodeInRange( @@ -3574,13 +3499,13 @@ const lists = frag.querySelectorAll("UL, OL"); const items = frag.querySelectorAll("LI"); const root = this._root; - for (let i = 0, l = lists.length; i < l; i += 1) { + for (let i = 0, l = lists.length; i < l; ++i) { const list = lists[i]; const listFrag = empty(list); fixContainer(listFrag, root); replaceWith(list, listFrag); } - for (let i = 0, l = items.length; i < l; i += 1) { + for (let i = 0, l = items.length; i < l; ++i) { const item = items[i]; if (isBlock(item)) { replaceWith(item, this.createDefaultBlock([empty(item)])); @@ -3656,7 +3581,7 @@ let nodes = node.querySelectorAll("BR"); const brBreaksLine = []; let l = nodes.length; - for (let i = 0; i < l; i += 1) { + for (let i = 0; i < l; ++i) { brBreaksLine[i] = isLineBreak(nodes[i], false); } while (l--) { From cfbc47488a6b2e2ae4be484f501ee1a3485f542e Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Mon, 16 Sep 2024 14:37:26 +0200 Subject: [PATCH 17/57] cleanHtml use allowedTags instead of disallowedTags and improved CSS handling --- dev/Common/Html.js | 85 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 25 deletions(-) diff --git a/dev/Common/Html.js b/dev/Common/Html.js index 07d975224..0b55792e0 100644 --- a/dev/Common/Html.js +++ b/dev/Common/Html.js @@ -17,14 +17,34 @@ const "'": ''' }, - disallowedTags = [ - 'svg','script','title','link','base','meta', - 'input','output','select','button','textarea', - 'bgsound','keygen','source','object','embed','applet','iframe','frame','frameset','video','audio','area','map' - // not supported by