diff --git a/.gitignore b/.gitignore index 496ee2c..91dfed8 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.DS_Store \ No newline at end of file +.DS_Store +node_modules \ No newline at end of file diff --git a/README.md b/README.md index 9e50e4a..1a2df84 100644 --- a/README.md +++ b/README.md @@ -11,4 +11,4 @@ Install [the extension](https://chrome.google.com/webstore/detail/detect-zero-wi ## License -Detect Zero-Width Characters Chrome Extension is an Open Source project covered by the [GNU General Public License version 2](LICENSE). +Detect Zero-Width Characters Chrome Extension is an Open Source project covered by the [GNU General Public License version 3](LICENSE). diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..0647581 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "module": "ESNext", + "target": "ES2020", + "checkJs": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + } +} \ No newline at end of file diff --git a/manifest.json b/manifest.json index cd1f0d5..d1b3dd6 100644 --- a/manifest.json +++ b/manifest.json @@ -1,19 +1,12 @@ { "name": "Detect Zero-Width Characters", - "version": "0.0.3", - "manifest_version": 2, + "version": "0.0.4", + "manifest_version": 3, "description": "Detects zero-width characters, highlights the characters and containing DOM element, and allows sanitization and copying of text.", "homepage_url": "https://github.com/roymckenzie/detect-zero-width-characters-chrome-extension", - "permissions": [ - "contextMenus", - "clipboardWrite" - ], + "permissions": ["contextMenus", "clipboardWrite"], "background": { - "scripts": [ - "src/constants.js", - "src/utils.js", - "src/background/background.js" - ] + "service_worker": "src/background/service-worker.js" }, "icons": { "16": "src/icon/16x16.png", @@ -22,23 +15,12 @@ }, "content_scripts": [ { - "matches": [ - "http://*/*", - "https://*/*" - ], - "css": [ - "src/inject/inject.css" - ] + "matches": ["http://*/*", "https://*/*"], + "css": ["src/inject/inject.css"] }, { - "matches": [ - "http://*/*", - "https://*/*" - ], - "js": [ - "src/constants.js", - "src/inject/inject.js" - ] + "matches": ["http://*/*", "https://*/*"], + "js": ["src/helpers/constants.js", "src/helpers/utils.js", "src/inject/inject.js"] } ] -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ac37508 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,76 @@ +{ + "name": "detect-zero-width-characters-chrome-extension", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@types/chrome": "^0.0.248" + } + }, + "node_modules/@types/chrome": { + "version": "0.0.248", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.248.tgz", + "integrity": "sha512-qtBzxZD1v3eWZn8XxH1i07pAhzJDHnxJBBVy7bmntXxXKxjzNXYxD41teqa5yOcX/Yy8brRFGZESEzGoINvBDg==", + "dev": true, + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, + "node_modules/@types/filesystem": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.34.tgz", + "integrity": "sha512-La4bGrgck8/rosDUA1DJJP8hrFcKq0BV6JaaVlNnOo1rJdJDcft3//slEbAmsWNUJwXRCc0DXpeO40yuATlexw==", + "dev": true, + "dependencies": { + "@types/filewriter": "*" + } + }, + "node_modules/@types/filewriter": { + "version": "0.0.31", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.31.tgz", + "integrity": "sha512-12df1utOvPC80+UaVoOO1d81X8pa5MefHNS+gWX9R2ucSESpMz9K5QwlTWDGKASrzCpSFwj7NPYh+nTsolgEGA==", + "dev": true + }, + "node_modules/@types/har-format": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.14.tgz", + "integrity": "sha512-pEmBAoccWvO6XbSI8A7KvIDGEoKtlLWtdqVCKoVBcCDSFvR4Ijd7zGLu7MWGEqk2r8D54uWlMRt+VZuSrfFMzQ==", + "dev": true + } + }, + "dependencies": { + "@types/chrome": { + "version": "0.0.248", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.248.tgz", + "integrity": "sha512-qtBzxZD1v3eWZn8XxH1i07pAhzJDHnxJBBVy7bmntXxXKxjzNXYxD41teqa5yOcX/Yy8brRFGZESEzGoINvBDg==", + "dev": true, + "requires": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, + "@types/filesystem": { + "version": "0.0.34", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.34.tgz", + "integrity": "sha512-La4bGrgck8/rosDUA1DJJP8hrFcKq0BV6JaaVlNnOo1rJdJDcft3//slEbAmsWNUJwXRCc0DXpeO40yuATlexw==", + "dev": true, + "requires": { + "@types/filewriter": "*" + } + }, + "@types/filewriter": { + "version": "0.0.31", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.31.tgz", + "integrity": "sha512-12df1utOvPC80+UaVoOO1d81X8pa5MefHNS+gWX9R2ucSESpMz9K5QwlTWDGKASrzCpSFwj7NPYh+nTsolgEGA==", + "dev": true + }, + "@types/har-format": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.14.tgz", + "integrity": "sha512-pEmBAoccWvO6XbSI8A7KvIDGEoKtlLWtdqVCKoVBcCDSFvR4Ijd7zGLu7MWGEqk2r8D54uWlMRt+VZuSrfFMzQ==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..454036b --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "@types/chrome": "^0.0.248" + } +} diff --git a/src/background/background.js b/src/background/background.js deleted file mode 100644 index 72319d6..0000000 --- a/src/background/background.js +++ /dev/null @@ -1,41 +0,0 @@ -(function() { - let contextMenuOptionId, selectionText; - - /** - * Sanitizes and copies some text. - * - * @since 0.0.2 - */ - const sanitizeAndCopy = function() { - copyTextToClipboard( sanitize( selectionText ) ); - }; - - /** - * Builds a context menu in Chrome. - * - * @param {object} request The chrome.runtime.onMessage object. - * - * @since 0.0.2 - */ - const handleContextMenu = function( request ) { - if ( request.shouldSanitizeSelection ) { - selectionText = request.selection; - - if ( !contextMenuOptionId ) { - contextMenuOptionId = chrome.contextMenus.create({ - "title" : "Sanitize and copy", - "type" : "normal", - "contexts" : [ "selection" ], - "onclick" : sanitizeAndCopy - }); - } - } else { - if ( contextMenuOptionId ) { - chrome.contextMenus.remove( contextMenuOptionId ); - contextMenuOptionId = null; - } - } - }; - - chrome.runtime.onMessage.addListener( handleContextMenu ); -})(); diff --git a/src/background/service-worker.js b/src/background/service-worker.js new file mode 100644 index 0000000..d63bb7c --- /dev/null +++ b/src/background/service-worker.js @@ -0,0 +1,36 @@ +/** + * Builds a context menu in Chrome. + * + * @param {SanitizeAndCopyContextMenuMessage} message + * + * @since 0.0.2 + */ +const handleContextMenu = (message) => { + if (message.type !== "sanitizeAndCopyContextMenu") return; + + chrome.contextMenus.removeAll(); + + if (!message.shouldSanitizeSelection) { + return; + } + + chrome.contextMenus.create({ + id: "sanitizeAndCopyContextMenuItem", + title: "Sanitize and copy", + type: "normal", + contexts: ["selection"], + }); + + chrome.contextMenus.onClicked.addListener((info, tab) => { + if (!tab) return; + if (!tab.id) return; + if (info.menuItemId !== "sanitizeAndCopyContextMenuItem") return; + + chrome.tabs.sendMessage(tab.id, { + type: "sanitizeAndCopyContextMenuItemAction", + text: message.textSelection, + }); + }); +}; + +chrome.runtime.onMessage.addListener(handleContextMenu); diff --git a/src/constants.js b/src/helpers/constants.js similarity index 52% rename from src/constants.js rename to src/helpers/constants.js index 3604dfc..eb2d5e4 100644 --- a/src/constants.js +++ b/src/helpers/constants.js @@ -3,4 +3,4 @@ * * @since 0.0.1 */ -const zeroWidthCharacterCodes = [ 8203, 8204, 8205, 8288 ]; +const zeroWidthCharacterCodes = [8203, 8204, 8205, 8288]; diff --git a/src/helpers/utils.js b/src/helpers/utils.js new file mode 100644 index 0000000..df4a878 --- /dev/null +++ b/src/helpers/utils.js @@ -0,0 +1,34 @@ +/** + * Removes zero-width characters from `text`. + * + * @param {string} text - String to sanitize. + * @returns Sanitized string. + */ +const sanitize = (text) => { + return [...text] + .filter((char) => { + const unicodeCode = char.codePointAt(0); + return !zeroWidthCharacterCodes.includes(unicodeCode); + }) + .join(""); +}; + +// https://stackoverflow.com/a/18455088/6591929 +/** + * Copies `text` to clipboard. + * + * @param {string} text - String to copy to clipboard. + */ +const copyTextToClipboard = (text) => { + const textArea = document.createElement("textarea"); + textArea.setAttribute("name", "copyTextArea"); + textArea.textContent = text; + + const body = document.body; + body.appendChild(textArea); + + textArea.select(); + document.execCommand("copy"); + + body.removeChild(textArea); +}; diff --git a/src/inject/inject.js b/src/inject/inject.js index 34114c5..0e46a81 100644 --- a/src/inject/inject.js +++ b/src/inject/inject.js @@ -1,124 +1,140 @@ -(function() { +(function () { + /** @type {Element[]} */ let elementsWithZWCC = []; /** - * Highlight zero-width character in DOM element + * Highlight zero-width character in DOM element. * - * @param {dom node} element A DOM node. + * @param {Element} element - A DOM node Element. */ - const highlightCharacters = function(element) { - const zeroWidthCharacters = String.fromCodePoint(...zeroWidthCharacterCodes); - const regExp = new RegExp(`([${zeroWidthCharacters}])`, 'g') - - element.innerHTML = element.innerHTML - .replace(regExp, '$1'); + const highlightCharacters = (element) => { + const zeroWidthCharacters = String.fromCodePoint( + ...zeroWidthCharacterCodes + ); + const regExp = new RegExp(`([${zeroWidthCharacters}])`, "g"); + + element.innerHTML = element.innerHTML.replace( + regExp, + '$1' + ); }; /** - * Checks DOM element for zero-width character. - * - * @param {dom node} element A DOM node. - * - * @since 0.0.1 - */ + * Checks DOM element for zero-width character. + * + * @param {Element} element - A DOM node Element. + * + * @since 0.0.1 + */ // From: https://jsfiddle.net/tim333/np874wae/13/ - const checkElement = function( element ) { - const text = textWithoutChildren( element ); + const checkElement = (element) => { + const text = textWithoutChildren(element); + + [...text].forEach((character) => { + const unicodeCode = character.codePointAt(0); - [...text].forEach( function( character ) { - unicodeCode = character.codePointAt(0); + if (!unicodeCode) return; if ( - zeroWidthCharacterCodes.includes( unicodeCode ) - && !elementsWithZWCC.includes(element) + zeroWidthCharacterCodes.includes(unicodeCode) && + !elementsWithZWCC.includes(element) ) { - elementsWithZWCC.push(element) + elementsWithZWCC.push(element); } }); - } + }; /** - * Pulls text from DOM node not including child DOM nodes. - * - * @param {node} element A DOM node. - * - * @since 0.0.1 - * - * @return {string} The text inside the DOM node. - */ + * Pulls text from DOM node not including child DOM nodes. + * + * @param {Element} element - A DOM node Element. + * + * @since 0.0.1 + * + * @return {string} The text inside the DOM node. + */ // From: https://stackoverflow.com/a/9340862/535363 - const textWithoutChildren = function( element ) { + const textWithoutChildren = (element) => { let child = element.firstChild, - texts = []; + texts = []; while (child) { - if (child.nodeType == 3) { - texts.push(child.data); - } - child = child.nextSibling; + if (child.nodeType == 3) { + texts.push(child.data); + } + child = child.nextSibling; } return texts.join(""); - } + }; /** - * Checks current document for zero-width characters. - * - * @since 0.0.1 - */ - const checkPage = function() { - const allElements = document.getElementsByTagName('*'); + * Checks current document for zero-width characters. + * + * @since 0.0.1 + */ + const checkPage = () => { + const allElements = document.getElementsByTagName("*"); [...allElements].forEach(checkElement); - elementsWithZWCC.forEach(function( element ) { - element.classList.add('zero-width-characters'); + elementsWithZWCC.forEach((element) => { + element.classList.add("zero-width-characters"); highlightCharacters(element); }); - } - - chrome.extension.sendMessage({}, function(response) { - var readyStateCheckInterval = setInterval(function() { - if (document.readyState === "complete") { - clearInterval(readyStateCheckInterval); - - // Check Page - checkPage(); - - // Check page again when any input field is changed - const inputs = document.querySelectorAll('input'); - - [...inputs].forEach( function( input ) { - input.addEventListener( 'change', checkPage ); - }); - } - }, 10); - }); + }; - document.body.addEventListener('mouseup', function ( event ) { + document.body.addEventListener("mouseup", () => { const selection = window.getSelection(); + if (!selection) return; + const shouldSanitizeSelection = elementsWithZWCC - .map(function(element) { - return selection.containsNode(element, true); - }) + .map((element) => selection.containsNode(element, true)) .includes(true); try { - chrome.runtime.sendMessage({ - "shouldSanitizeSelection": shouldSanitizeSelection, - "selection": selection.toString() - }); - } catch(event) { + /** @type {SanitizeAndCopyContextMenuMessage} */ + const message = { + type: "sanitizeAndCopyContextMenu", + shouldSanitizeSelection: shouldSanitizeSelection, + textSelection: selection.toString(), + } + + chrome.runtime.sendMessage(message); + } catch (event) { if ( event.message.match(/Invocation of form runtime\.connect/) && event.message.match(/doesn't match definition runtime\.connect/) ) { - console.error('Chrome extension has been reloaded. Please refresh the page'); + console.error( + "Chrome extension has been reloaded. Please refresh the page" + ); } else { - throw(event); + throw event; } } }); + /** + * Handle Sanitize and Copy context menu item action + * @param {SanitizeAndCopyContextMenuActionMessage} message + */ + const handleMenuItemAction = (message) => { + if (message.type !== "sanitizeAndCopyContextMenuItemAction") return; + sanitizeAndCopy(message.text); + } + + /** + * Sanitizes and copies some text. + * + * @since 0.0.2 + */ + const sanitizeAndCopy = (text) => { + copyTextToClipboard(sanitize(text)); + }; + + chrome.runtime.onMessage.addListener(handleMenuItemAction); + + checkPage(); })(); diff --git a/src/typdef.js b/src/typdef.js new file mode 100644 index 0000000..c0fce7a --- /dev/null +++ b/src/typdef.js @@ -0,0 +1,14 @@ +/** + * @typedef {object} SanitizeAndCopyContextMenuMessage + * + * @property {"sanitizeAndCopyContextMenu"} type + * @property {boolean} shouldSanitizeSelection - Should the menu item be added to the context menu. + * @property {string} textSelection - The text to copy to clipboard. + */ + +/** + * @typedef {object} SanitizeAndCopyContextMenuActionMessage + * + * @property {"sanitizeAndCopyContextMenuItemAction"} type + * @property {string} text - The text to copy to clipboard. + */ diff --git a/src/utils.js b/src/utils.js deleted file mode 100644 index 9fca27c..0000000 --- a/src/utils.js +++ /dev/null @@ -1,17 +0,0 @@ -const sanitize = function( text ) { - return [...text].filter( function( char ) { - const unicodeCode = char.codePointAt(0); - return !zeroWidthCharacterCodes.includes( unicodeCode ); - }).join(""); -} - -//https://stackoverflow.com/a/18455088/6591929 -const copyTextToClipboard = function (text) { - const copyFrom = document.createElement("textarea"); - const body = document.body; - copyFrom.textContent = text; - body.appendChild( copyFrom ); - copyFrom.select(); - document.execCommand('copy'); - body.removeChild( copyFrom ); -}