From 68b2444a762ab9bf8a604a6c5d34891225f51c3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Tue, 19 Sep 2023 17:41:02 +0200 Subject: [PATCH 1/8] migrate SelectionScraper --- .../{index.native.js => index.native.ts} | 0 .../SelectionScraper/{index.js => index.ts} | 71 ++++++++++--------- 2 files changed, 37 insertions(+), 34 deletions(-) rename src/libs/SelectionScraper/{index.native.js => index.native.ts} (100%) rename src/libs/SelectionScraper/{index.js => index.ts} (72%) diff --git a/src/libs/SelectionScraper/index.native.js b/src/libs/SelectionScraper/index.native.ts similarity index 100% rename from src/libs/SelectionScraper/index.native.js rename to src/libs/SelectionScraper/index.native.ts diff --git a/src/libs/SelectionScraper/index.js b/src/libs/SelectionScraper/index.ts similarity index 72% rename from src/libs/SelectionScraper/index.js rename to src/libs/SelectionScraper/index.ts index 44b87deba796..79639cd0f28a 100644 --- a/src/libs/SelectionScraper/index.js +++ b/src/libs/SelectionScraper/index.ts @@ -1,7 +1,7 @@ import render from 'dom-serializer'; import {parseDocument} from 'htmlparser2'; -import _ from 'underscore'; import Str from 'expensify-common/lib/str'; +import {DataNode, Element, Node} from 'domhandler'; import CONST from '../../CONST'; const elementsWillBeSkipped = ['html', 'body']; @@ -9,17 +9,17 @@ const tagAttribute = 'data-testid'; /** * Reads html of selection. If browser doesn't support Selection API, returns empty string. - * @returns {String} HTML of selection as String */ -const getHTMLOfSelection = () => { +const getHTMLOfSelection = (): string => { // If browser doesn't support Selection API, return an empty string. - if (!window.getSelection) { + const selection = window.getSelection(); + + if (!selection || !window.getSelection) { return ''; } - const selection = window.getSelection(); if (selection.rangeCount <= 0) { - return window.getSelection().toString(); + return window.getSelection()?.toString() ?? ''; } const div = document.createElement('div'); @@ -64,17 +64,17 @@ const getHTMLOfSelection = () => { // and finally commonAncestorContainer.parentNode.closest('data-testid') is targeted dom. if (range.commonAncestorContainer instanceof HTMLElement) { parent = range.commonAncestorContainer.closest(`[${tagAttribute}]`); - } else { - parent = range.commonAncestorContainer.parentNode.closest(`[${tagAttribute}]`); + } else if (range.commonAncestorContainer.parentNode) { + parent = (range.commonAncestorContainer.parentNode as HTMLElement).closest(`[${tagAttribute}]`); } // Keep traversing up to clone all parents with 'data-testid' attribute. while (parent) { const cloned = parent.cloneNode(); cloned.appendChild(child); - child = cloned; + child = cloned as DocumentFragment; - parent = parent.parentNode.closest(`[${tagAttribute}]`); + parent = (parent.parentNode as HTMLElement).closest(`[${tagAttribute}]`); } div.appendChild(child); @@ -96,58 +96,61 @@ const getHTMLOfSelection = () => { /** * Clears all attributes from dom elements - * @param {Object} dom htmlparser2 dom representation - * @param {Boolean} isChildOfEditorElement - * @returns {Object} htmlparser2 dom representation */ -const replaceNodes = (dom, isChildOfEditorElement) => { - let domName = dom.name; - let domChildren; - const domAttribs = {}; - let data; +const replaceNodes = (dom: Node, isChildOfEditorElement: boolean): Node => { + const domElement = dom as Element; + let domName = domElement.name; + let domChildren: Node[] = []; + const domAttribs = {} as Element['attribs']; + let data = ''; // Encoding HTML chars '< >' in the text, because any HTML will be removed in stripHTML method. - if (dom.type === 'text') { - data = Str.htmlEncode(dom.data); + if (dom.type.toString() === 'text') { + data = Str.htmlEncode((dom as DataNode).data); } // We are skipping elements which has html and body in data-testid, since ExpensiMark can't parse it. Also this data // has no meaning for us. - if (dom.attribs && dom.attribs[tagAttribute]) { - if (!elementsWillBeSkipped.includes(dom.attribs[tagAttribute])) { - domName = dom.attribs[tagAttribute]; + if (domElement.attribs?.[tagAttribute]) { + if (!elementsWillBeSkipped.includes(domElement.attribs[tagAttribute])) { + domName = domElement.attribs[tagAttribute]; } - } else if (dom.name === 'div' && dom.children.length === 1 && isChildOfEditorElement) { + } else if (domElement.name === 'div' && domElement.children.length === 1 && isChildOfEditorElement) { // We are excluding divs that are children of our editor element and have only one child to prevent // additional newlines from being added in the HTML to Markdown conversion process. - return replaceNodes(dom.children[0], isChildOfEditorElement); + return replaceNodes(domElement.children[0], isChildOfEditorElement); } // We need to preserve href attribute in order to copy links. - if (dom.attribs && dom.attribs.href) { - domAttribs.href = dom.attribs.href; + if (domElement.attribs?.href) { + domAttribs.href = domElement.attribs.href; + } + + if (domElement.children) { + domChildren = domElement.children.map((c) => replaceNodes(c, isChildOfEditorElement || !!domElement.attribs?.[tagAttribute])); } - if (dom.children) { - domChildren = _.map(dom.children, (c) => replaceNodes(c, isChildOfEditorElement || !_.isEmpty(dom.attribs && dom.attribs[tagAttribute]))); + if (data) { + return { + ...dom, + data, + } as DataNode; } return { ...dom, - data, name: domName, attribs: domAttribs, children: domChildren, - }; + } as Element; }; /** * Resolves the current selection to values and produces clean HTML. - * @returns {String} resolved selection in the HTML format */ -const getCurrentSelection = () => { +const getCurrentSelection = (): string => { const domRepresentation = parseDocument(getHTMLOfSelection()); - domRepresentation.children = _.map(domRepresentation.children, replaceNodes); + domRepresentation.children = domRepresentation.children.map((item) => replaceNodes(item, false)); // Newline characters need to be removed here because the HTML could contain both newlines and
tags, and when //
tags are converted later to markdown, it creates duplicate newline characters. This means that when the content From 43adfe9e58fd0c7e78ec5edaef30be0d9ab8d01f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Thu, 21 Sep 2023 15:06:18 +0200 Subject: [PATCH 2/8] review changes --- src/libs/SelectionScraper/index.native.ts | 8 ++++++-- src/libs/SelectionScraper/index.ts | 7 ++++--- src/libs/SelectionScraper/types.ts | 3 +++ 3 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 src/libs/SelectionScraper/types.ts diff --git a/src/libs/SelectionScraper/index.native.ts b/src/libs/SelectionScraper/index.native.ts index 3872ece30b66..7712906f05e6 100644 --- a/src/libs/SelectionScraper/index.native.ts +++ b/src/libs/SelectionScraper/index.native.ts @@ -1,4 +1,8 @@ +import GetCurrentSelection from './types'; + +// This is a no-op function for native devices because they wouldn't be able to support Selection API like a website. +const getCurrentSelection: GetCurrentSelection = () => ''; + export default { - // This is a no-op function for native devices because they wouldn't be able to support Selection API like a website. - getCurrentSelection: () => '', + getCurrentSelection, }; diff --git a/src/libs/SelectionScraper/index.ts b/src/libs/SelectionScraper/index.ts index 79639cd0f28a..6660d5a394cb 100644 --- a/src/libs/SelectionScraper/index.ts +++ b/src/libs/SelectionScraper/index.ts @@ -3,6 +3,7 @@ import {parseDocument} from 'htmlparser2'; import Str from 'expensify-common/lib/str'; import {DataNode, Element, Node} from 'domhandler'; import CONST from '../../CONST'; +import GetCurrentSelection from './types'; const elementsWillBeSkipped = ['html', 'body']; const tagAttribute = 'data-testid'; @@ -44,7 +45,7 @@ const getHTMLOfSelection = (): string => { // If clonedSelection has no text content this data has no meaning to us. if (clonedSelection.textContent) { - let parent; + let parent: globalThis.Element | null = null; let child = clonedSelection; // If selection starts and ends within same text node we use its parentNode. This is because we can't @@ -101,7 +102,7 @@ const replaceNodes = (dom: Node, isChildOfEditorElement: boolean): Node => { const domElement = dom as Element; let domName = domElement.name; let domChildren: Node[] = []; - const domAttribs = {} as Element['attribs']; + const domAttribs: Element['attribs'] = {}; let data = ''; // Encoding HTML chars '< >' in the text, because any HTML will be removed in stripHTML method. @@ -148,7 +149,7 @@ const replaceNodes = (dom: Node, isChildOfEditorElement: boolean): Node => { /** * Resolves the current selection to values and produces clean HTML. */ -const getCurrentSelection = (): string => { +const getCurrentSelection: GetCurrentSelection = () => { const domRepresentation = parseDocument(getHTMLOfSelection()); domRepresentation.children = domRepresentation.children.map((item) => replaceNodes(item, false)); diff --git a/src/libs/SelectionScraper/types.ts b/src/libs/SelectionScraper/types.ts new file mode 100644 index 000000000000..d33338883dd4 --- /dev/null +++ b/src/libs/SelectionScraper/types.ts @@ -0,0 +1,3 @@ +type GetCurrentSelection = () => string; + +export default GetCurrentSelection; From 6841a5407f3303696ebde4c00ea920215ca99109 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Fri, 22 Sep 2023 11:40:35 +0200 Subject: [PATCH 3/8] add return comment --- src/libs/SelectionScraper/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/SelectionScraper/index.ts b/src/libs/SelectionScraper/index.ts index 6660d5a394cb..52fe69fcb75e 100644 --- a/src/libs/SelectionScraper/index.ts +++ b/src/libs/SelectionScraper/index.ts @@ -10,6 +10,7 @@ const tagAttribute = 'data-testid'; /** * Reads html of selection. If browser doesn't support Selection API, returns empty string. + * @returns HTML of selection as String */ const getHTMLOfSelection = (): string => { // If browser doesn't support Selection API, return an empty string. From 59a32831104a465c7386dfc1d5090af93f61570a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Mon, 2 Oct 2023 16:24:36 +0200 Subject: [PATCH 4/8] review changes --- src/libs/SelectionScraper/index.ts | 36 +++++++++++++++--------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/libs/SelectionScraper/index.ts b/src/libs/SelectionScraper/index.ts index 52fe69fcb75e..4b679734b3a0 100644 --- a/src/libs/SelectionScraper/index.ts +++ b/src/libs/SelectionScraper/index.ts @@ -14,9 +14,11 @@ const tagAttribute = 'data-testid'; */ const getHTMLOfSelection = (): string => { // If browser doesn't support Selection API, return an empty string. + if (!window.getSelection) { + return ''; + } const selection = window.getSelection(); - - if (!selection || !window.getSelection) { + if (!selection) { return ''; } @@ -66,8 +68,8 @@ const getHTMLOfSelection = (): string => { // and finally commonAncestorContainer.parentNode.closest('data-testid') is targeted dom. if (range.commonAncestorContainer instanceof HTMLElement) { parent = range.commonAncestorContainer.closest(`[${tagAttribute}]`); - } else if (range.commonAncestorContainer.parentNode) { - parent = (range.commonAncestorContainer.parentNode as HTMLElement).closest(`[${tagAttribute}]`); + } else { + parent = (range.commonAncestorContainer.parentNode as HTMLElement | null)?.closest(`[${tagAttribute}]`) ?? null; } // Keep traversing up to clone all parents with 'data-testid' attribute. @@ -76,7 +78,7 @@ const getHTMLOfSelection = (): string => { cloned.appendChild(child); child = cloned as DocumentFragment; - parent = (parent.parentNode as HTMLElement).closest(`[${tagAttribute}]`); + parent = (parent.parentNode as HTMLElement | null)?.closest(`[${tagAttribute}]`) ?? null; } div.appendChild(child); @@ -100,16 +102,21 @@ const getHTMLOfSelection = (): string => { * Clears all attributes from dom elements */ const replaceNodes = (dom: Node, isChildOfEditorElement: boolean): Node => { + // Encoding HTML chars '< >' in the text, because any HTML will be removed in stripHTML method. + const domDataNode = dom as DataNode; + let data = ''; + if (dom.type.toString() === 'text' && domDataNode.data) { + data = Str.htmlEncode(domDataNode.data); + return { + ...dom, + data, + } as DataNode; + } + const domElement = dom as Element; let domName = domElement.name; let domChildren: Node[] = []; const domAttribs: Element['attribs'] = {}; - let data = ''; - - // Encoding HTML chars '< >' in the text, because any HTML will be removed in stripHTML method. - if (dom.type.toString() === 'text') { - data = Str.htmlEncode((dom as DataNode).data); - } // We are skipping elements which has html and body in data-testid, since ExpensiMark can't parse it. Also this data // has no meaning for us. @@ -132,13 +139,6 @@ const replaceNodes = (dom: Node, isChildOfEditorElement: boolean): Node => { domChildren = domElement.children.map((c) => replaceNodes(c, isChildOfEditorElement || !!domElement.attribs?.[tagAttribute])); } - if (data) { - return { - ...dom, - data, - } as DataNode; - } - return { ...dom, name: domName, From 9b266fae39474f474ebd21dd08bc38fb342999a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Wed, 4 Oct 2023 10:47:09 +0200 Subject: [PATCH 5/8] fix replaceNodes function --- src/libs/SelectionScraper/index.ts | 59 +++++++++++++++--------------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/src/libs/SelectionScraper/index.ts b/src/libs/SelectionScraper/index.ts index 4b679734b3a0..d865cd75e850 100644 --- a/src/libs/SelectionScraper/index.ts +++ b/src/libs/SelectionScraper/index.ts @@ -100,51 +100,50 @@ const getHTMLOfSelection = (): string => { /** * Clears all attributes from dom elements + * @param dom - dom htmlparser2 dom representation */ const replaceNodes = (dom: Node, isChildOfEditorElement: boolean): Node => { - // Encoding HTML chars '< >' in the text, because any HTML will be removed in stripHTML method. - const domDataNode = dom as DataNode; - let data = ''; - if (dom.type.toString() === 'text' && domDataNode.data) { - data = Str.htmlEncode(domDataNode.data); - return { - ...dom, - data, - } as DataNode; - } - - const domElement = dom as Element; - let domName = domElement.name; + let domName; let domChildren: Node[] = []; const domAttribs: Element['attribs'] = {}; + let data = ''; - // We are skipping elements which has html and body in data-testid, since ExpensiMark can't parse it. Also this data - // has no meaning for us. - if (domElement.attribs?.[tagAttribute]) { - if (!elementsWillBeSkipped.includes(domElement.attribs[tagAttribute])) { - domName = domElement.attribs[tagAttribute]; - } - } else if (domElement.name === 'div' && domElement.children.length === 1 && isChildOfEditorElement) { - // We are excluding divs that are children of our editor element and have only one child to prevent - // additional newlines from being added in the HTML to Markdown conversion process. - return replaceNodes(domElement.children[0], isChildOfEditorElement); + // Encoding HTML chars '< >' in the text, because any HTML will be removed in stripHTML method. + if (dom.type.toString() === 'text' && dom instanceof DataNode) { + data = Str.htmlEncode(dom.data); } - // We need to preserve href attribute in order to copy links. - if (domElement.attribs?.href) { - domAttribs.href = domElement.attribs.href; - } + if (dom instanceof Element) { + domName = dom.name; + // We are skipping elements which has html and body in data-testid, since ExpensiMark can't parse it. Also this data + // has no meaning for us. + if (dom.attribs?.[tagAttribute]) { + if (!elementsWillBeSkipped.includes(dom.attribs[tagAttribute])) { + domName = dom.attribs[tagAttribute]; + } + } else if (dom.name === 'div' && dom.children.length === 1 && isChildOfEditorElement) { + // We are excluding divs that are children of our editor element and have only one child to prevent + // additional newlines from being added in the HTML to Markdown conversion process. + return replaceNodes(dom.children[0], isChildOfEditorElement); + } - if (domElement.children) { - domChildren = domElement.children.map((c) => replaceNodes(c, isChildOfEditorElement || !!domElement.attribs?.[tagAttribute])); + // We need to preserve href attribute in order to copy links. + if (dom.attribs?.href) { + domAttribs.href = dom.attribs.href; + } + + if (dom.children) { + domChildren = dom.children.map((c) => replaceNodes(c, isChildOfEditorElement || !!dom.attribs?.[tagAttribute])); + } } return { ...dom, + data, name: domName, attribs: domAttribs, children: domChildren, - } as Element; + } as unknown as Node; }; /** From 772975082c0725c5ac88f20065adb437944ca3b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Wed, 4 Oct 2023 16:21:49 +0200 Subject: [PATCH 6/8] change replaceNodes return object type --- src/libs/SelectionScraper/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/SelectionScraper/index.ts b/src/libs/SelectionScraper/index.ts index d865cd75e850..0368087b5f38 100644 --- a/src/libs/SelectionScraper/index.ts +++ b/src/libs/SelectionScraper/index.ts @@ -143,7 +143,7 @@ const replaceNodes = (dom: Node, isChildOfEditorElement: boolean): Node => { name: domName, attribs: domAttribs, children: domChildren, - } as unknown as Node; + } as Element & DataNode; }; /** From 3ebd5f97bdd31d5689ac8a9f75a577c70d1808a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Mon, 9 Oct 2023 11:40:09 +0200 Subject: [PATCH 7/8] fix replaceNodes condition --- src/libs/SelectionScraper/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/SelectionScraper/index.ts b/src/libs/SelectionScraper/index.ts index 0368087b5f38..abe9a235fca8 100644 --- a/src/libs/SelectionScraper/index.ts +++ b/src/libs/SelectionScraper/index.ts @@ -111,9 +111,7 @@ const replaceNodes = (dom: Node, isChildOfEditorElement: boolean): Node => { // Encoding HTML chars '< >' in the text, because any HTML will be removed in stripHTML method. if (dom.type.toString() === 'text' && dom instanceof DataNode) { data = Str.htmlEncode(dom.data); - } - - if (dom instanceof Element) { + } else if (dom instanceof Element) { domName = dom.name; // We are skipping elements which has html and body in data-testid, since ExpensiMark can't parse it. Also this data // has no meaning for us. @@ -135,6 +133,8 @@ const replaceNodes = (dom: Node, isChildOfEditorElement: boolean): Node => { if (dom.children) { domChildren = dom.children.map((c) => replaceNodes(c, isChildOfEditorElement || !!dom.attribs?.[tagAttribute])); } + } else { + throw new Error(`Unknown dom type: ${dom.type}`); } return { From 5fd75e1c201fec2174d6b9cd039bb30e54fda05c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ska=C5=82ka?= Date: Mon, 30 Oct 2023 11:18:09 +0100 Subject: [PATCH 8/8] fix prettier diff --- src/libs/SelectionScraper/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/SelectionScraper/index.ts b/src/libs/SelectionScraper/index.ts index 203660b56ad8..1f62f83e1c91 100644 --- a/src/libs/SelectionScraper/index.ts +++ b/src/libs/SelectionScraper/index.ts @@ -1,7 +1,7 @@ import render from 'dom-serializer'; +import {DataNode, Element, Node} from 'domhandler'; import Str from 'expensify-common/lib/str'; import {parseDocument} from 'htmlparser2'; -import {DataNode, Element, Node} from 'domhandler'; import CONST from '@src/CONST'; import GetCurrentSelection from './types';