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 );
-}