From 671373f556998702473af3909c06f6c5aa09665b Mon Sep 17 00:00:00 2001 From: Hardik Pratap Singh <97048877+hardik-pratap-singh@users.noreply.github.com> Date: Fri, 13 Sep 2024 15:40:14 +0530 Subject: [PATCH] feat: Add Slur Metadata Checklist Functionality (#615) * v 1.0.0 prototype * uli icon updated * added tests for EnableSlurMetadata functions * integrated slur_metadata.json --- .../api-server/assets/slur_metadata.json | 222 +++--- browser-extension/plugin/package-lock.json | 684 +++++++++++++++++- browser-extension/plugin/package.json | 5 + .../plugin/src/content-script.js | 10 +- .../plugin/src/transform-general.js | 219 ++++-- .../plugin/src/ui-components/pages/App.jsx | 3 +- .../src/ui-components/pages/Preferences.jsx | 42 +- .../plugin/test/enableSlurMetadata.test.js | 249 +++++++ .../test/enableSlurMetadataFunctions.js | 197 +++++ 9 files changed, 1434 insertions(+), 197 deletions(-) create mode 100644 browser-extension/plugin/test/enableSlurMetadata.test.js create mode 100644 browser-extension/plugin/test/enableSlurMetadataFunctions.js diff --git a/browser-extension/api-server/assets/slur_metadata.json b/browser-extension/api-server/assets/slur_metadata.json index a140d4c4..c3104a7a 100644 --- a/browser-extension/api-server/assets/slur_metadata.json +++ b/browser-extension/api-server/assets/slur_metadata.json @@ -1,112 +1,112 @@ [ - { - "fuck": { - "Level of Severity": "Low", - "Casual": "Yes", - "Appropriated": "No", - "If, Appropriated, Is it by Community or Others?": "", - "What Makes it Problematic?": "a casual slur used to convey dislike, disrespect or lack of care. Sometimes also used aggresively or intensely", - "Categories": ["sexualized"] - } - }, - { - "cunt": { - "Level of Severity": "High", - "Casual": "Yes", - "Appropriated": "No", - "If, Appropriated, Is it by Community or Others?": "", - "What Makes it Problematic?": "slang for women's genetalia", - "Categories": ["sexualized"] - } - }, - { - "slut": { - "Level of Severity": "High", - "Casual": "No", - "Appropriated": "Yes", - "If, Appropriated, Is it by Community or Others?": "Community", - "What Makes it Problematic?": "", - "Categories": ["gendered", "sexualized", "body shaming"] - } - }, - { - "whore": { - "Level of Severity": "Low", - "Casual": "Yes", - "Appropriated": "Yes", - "If, Appropriated, Is it by Community or Others?": "Community", - "What Makes it Problematic?": "", - "Categories": ["gendered", "sexualized"] - } - }, - { - "Randi": { - "Level of Severity": "High", - "Casual": "No", - "Appropriated": "No", - "If, Appropriated, Is it by Community or Others?": "", - "What Makes it Problematic?": "", - "Categories": ["gendered", "sexualized"] - } - }, - { - "chutiya": { - "Level of Severity": "Low", - "Casual": "Yes", - "Appropriated": "No", - "If, Appropriated, Is it by Community or Others?": "", - "What Makes it Problematic?": "", - "Categories": [] - } - }, - { - "bsdk": { - "Level of Severity": "Low", - "Casual": "Yes", - "Appropriated": "No", - "If, Appropriated, Is it by Community or Others?": "", - "What Makes it Problematic?": "", - "Categories": ["sexualized"] - } - }, - { - "jihadi": { - "Level of Severity": "High", - "Casual": "No", - "Appropriated": "No", - "If, Appropriated, Is it by Community or Others?": "", - "What Makes it Problematic?": "", - "Categories": ["religion", "ethnicity", "political affiliation"] - } - }, - { - "Madarchod": { - "Level of Severity": "Medium", - "Casual": "Yes", - "Appropriated": "No", - "If, Appropriated, Is it by Community or Others?": "", - "What Makes it Problematic?": "sexualized act and describing incest, acted on a woman's (sister) body", - "Categories": ["gendered", "sexualized"] - } - }, - { - "bhenchod": { - "Level of Severity": "Medium", - "Casual": "Yes", - "Appropriated": "No", - "If, Appropriated, Is it by Community or Others?": "", - "What Makes it Problematic?": "sexualized act and describing incest, acted on a woman's (sister) body", - "Categories": ["gendered", "sexualized"] - } - }, - { - "motherfucker": { - "Level of Severity": "Medium", - "Casual": "Yes", - "Appropriated": "No", - "If, Appropriated, Is it by Community or Others?": "", - "What Makes it Problematic?": "slur based on incest which is a common theme for derogatory words. Incest based slurs imply immorality since it is a deviation from established moral codes. But such insults express action over female bodies", - "Categories": ["sexualized", "gendered"] - } - } -] + { + "fuck": { + "Level of Severity": "Low", + "Casual": "Yes", + "Appropriated": "No", + "If, Appropriated, Is it by Community or Others?": "", + "What Makes it Problematic?": "a casual slur used to convey dislike, disrespect or lack of care. Sometimes also used aggresively or intensely", + "Categories": ["sexualized"] + } + }, + { + "cunt": { + "Level of Severity": "High", + "Casual": "Yes", + "Appropriated": "No", + "If, Appropriated, Is it by Community or Others?": "", + "What Makes it Problematic?": "slang for women's genetalia", + "Categories": ["sexualized"] + } + }, + { + "slut": { + "Level of Severity": "High", + "Casual": "No", + "Appropriated": "Yes", + "If, Appropriated, Is it by Community or Others?": "Community", + "What Makes it Problematic?": "", + "Categories": ["gendered", "sexualized", "body shaming"] + } + }, + { + "whore": { + "Level of Severity": "Low", + "Casual": "Yes", + "Appropriated": "Yes", + "If, Appropriated, Is it by Community or Others?": "Community", + "What Makes it Problematic?": "", + "Categories": ["gendered", "sexualized"] + } + }, + { + "Randi": { + "Level of Severity": "High", + "Casual": "No", + "Appropriated": "No", + "If, Appropriated, Is it by Community or Others?": "", + "What Makes it Problematic?": "", + "Categories": ["gendered", "sexualized"] + } + }, + { + "chutiya": { + "Level of Severity": "Low", + "Casual": "Yes", + "Appropriated": "No", + "If, Appropriated, Is it by Community or Others?": "", + "What Makes it Problematic?": "", + "Categories": [] + } + }, + { + "bsdk": { + "Level of Severity": "Low", + "Casual": "Yes", + "Appropriated": "No", + "If, Appropriated, Is it by Community or Others?": "", + "What Makes it Problematic?": "", + "Categories": ["sexualized"] + } + }, + { + "jihadi": { + "Level of Severity": "High", + "Casual": "No", + "Appropriated": "No", + "If, Appropriated, Is it by Community or Others?": "", + "What Makes it Problematic?": "", + "Categories": ["religion", "ethnicity", "political affiliation"] + } + }, + { + "Madarchod": { + "Level of Severity": "Medium", + "Casual": "Yes", + "Appropriated": "No", + "If, Appropriated, Is it by Community or Others?": "", + "What Makes it Problematic?": "sexualized act and describing incest, acted on a woman's (sister) body", + "Categories": ["gendered", "sexualized"] + } + }, + { + "bhenchod": { + "Level of Severity": "Medium", + "Casual": "Yes", + "Appropriated": "No", + "If, Appropriated, Is it by Community or Others?": "", + "What Makes it Problematic?": "sexualized act and describing incest, acted on a woman's (sister) body", + "Categories": ["gendered", "sexualized"] + } + }, + { + "motherfucker": { + "Level of Severity": "Medium", + "Casual": "Yes", + "Appropriated": "No", + "If, Appropriated, Is it by Community or Others?": "", + "What Makes it Problematic?": "slur based on incest which is a common theme for derogatory words. Incest based slurs imply immorality since it is a deviation from established moral codes. But such insults express action over female bodies", + "Categories": ["sexualized", "gendered"] + } + } + ] \ No newline at end of file diff --git a/browser-extension/plugin/package-lock.json b/browser-extension/plugin/package-lock.json index afe69d03..5dd5a5da 100644 --- a/browser-extension/plugin/package-lock.json +++ b/browser-extension/plugin/package-lock.json @@ -34,6 +34,8 @@ "eslint-plugin-react": "7.33.2", "husky": "8.0.3", "jest": "29.7.0", + "jest-environment-jsdom": "^29.7.0", + "jsdom": "^25.0.0", "lint-staged": "15.1.0", "parcel": "2.10.3", "prettier": "3.1.0", @@ -3512,6 +3514,15 @@ "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", "dev": true }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", @@ -3601,6 +3612,17 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, "node_modules/@types/node": { "version": "20.9.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz", @@ -3616,6 +3638,12 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true + }, "node_modules/@types/yargs": { "version": "17.0.31", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.31.tgz", @@ -3647,6 +3675,13 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", "dev": true }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true + }, "node_modules/abortcontroller-polyfill": { "version": "1.7.5", "resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.5.tgz", @@ -3665,6 +3700,16 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -3674,6 +3719,18 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", + "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", @@ -4794,6 +4851,30 @@ "optional": true, "peer": true }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true + }, + "node_modules/cssstyle": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.0.1.tgz", + "integrity": "sha512-8ZYiJ3A/3OkDd093CBT/0UKDWry7ak4BdPTFP2+QEP7cmhouyq/Up709ASSj2cK02BbZiMgk7kYjZNS4QP5qrQ==", + "dev": true, + "dependencies": { + "rrweb-cssom": "^0.6.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "dev": true + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -4809,6 +4890,53 @@ "node": ">= 14" } }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dev": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dev": true, + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -4840,6 +4968,12 @@ } } }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, "node_modules/dedent": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", @@ -5029,6 +5163,28 @@ } ] }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/domexception/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/domhandler": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", @@ -6129,6 +6285,18 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -6210,9 +6378,9 @@ } }, "node_modules/http-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", - "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "dependencies": { "agent-base": "^7.1.0", @@ -6223,9 +6391,9 @@ } }, "node_modules/https-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", - "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dev": true, "dependencies": { "agent-base": "^7.0.2", @@ -6281,6 +6449,18 @@ "@babel/runtime": "^7.23.2" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -6621,6 +6801,12 @@ "node": ">=8" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -7077,6 +7263,237 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-environment-jsdom": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/jsdom": "^20.0.0", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", + "jsdom": "^20.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom/node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, + "node_modules/jest-environment-jsdom/node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-environment-jsdom/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jest-environment-jsdom/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-environment-jsdom/node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/jest-environment-node": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", @@ -7492,6 +7909,101 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.0.tgz", + "integrity": "sha512-OhoFVT59T7aEq75TVw9xxEfkXgacpqAhQaYgP9y/fDqWQCMB/b1H66RfmPm/MaeaAIU9nDwMOVTlPN51+ao6CQ==", + "dev": true, + "dependencies": { + "cssstyle": "^4.0.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.4", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dev": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dev": true, + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jsdom/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -8490,6 +9002,12 @@ "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", "dev": true }, + "node_modules/nwsapi": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz", + "integrity": "sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==", + "dev": true + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -8785,6 +9303,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -9117,6 +9659,12 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -9184,6 +9732,12 @@ } ] }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -9365,6 +9919,12 @@ "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, "node_modules/resolve": { "version": "2.0.0-next.5", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", @@ -9468,6 +10028,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -9551,6 +10117,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", @@ -10112,6 +10696,12 @@ "node": ">= 10" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, "node_modules/tar-fs": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", @@ -10204,6 +10794,30 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -10423,6 +11037,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/urlpattern-polyfill": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-9.0.0.tgz", @@ -10460,6 +11084,18 @@ "node": ">=0.10.0" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -10481,6 +11117,27 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "dev": true }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -10678,6 +11335,21 @@ } } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/browser-extension/plugin/package.json b/browser-extension/plugin/package.json index 576b2708..34b78467 100644 --- a/browser-extension/plugin/package.json +++ b/browser-extension/plugin/package.json @@ -18,6 +18,9 @@ "dev:chrome": "mkdir -p dist && concurrently \"npm run start:options\" \"npm run start:contentScript\" \"npm run moveBuildArtefactsToDistDir\"", "dev:clean": "rm -rf dist/" }, + "jest": { + "testEnvironment": "jsdom" + }, "keywords": [], "author": "", "license": "ISC", @@ -29,6 +32,8 @@ "eslint-plugin-react": "7.33.2", "husky": "8.0.3", "jest": "29.7.0", + "jest-environment-jsdom": "^29.7.0", + "jsdom": "^25.0.0", "lint-staged": "15.1.0", "parcel": "2.10.3", "prettier": "3.1.0", diff --git a/browser-extension/plugin/src/content-script.js b/browser-extension/plugin/src/content-script.js index e31fb03f..dece1a77 100644 --- a/browser-extension/plugin/src/content-script.js +++ b/browser-extension/plugin/src/content-script.js @@ -150,9 +150,13 @@ window.addEventListener( async () => { console.log('content loaded'); const pref = await getPreferenceData(); - console.log(pref); - const { enableSlurReplacement } = pref; - if (enableSlurReplacement) { + const { enableSlurReplacement , enableSlurMetadata } = pref; + if (enableSlurMetadata) { + let body = document.getElementsByTagName('body'); + let first_body = body[0]; + transformGeneral.processNewlyAddedNodesGeneral2(first_body); + } + else if (enableSlurReplacement) { processPage(location.href); } }, diff --git a/browser-extension/plugin/src/transform-general.js b/browser-extension/plugin/src/transform-general.js index 07c724ba..5161957f 100644 --- a/browser-extension/plugin/src/transform-general.js +++ b/browser-extension/plugin/src/transform-general.js @@ -2,6 +2,7 @@ import { replaceSlur } from './slur-replace'; import { log } from './logger'; import repository from './repository'; const { getPreferenceData } = repository; +const jsonData = require('../../api-server/assets/slur_metadata.json') // Traverse dom nodes to find leaf node that are text nodes and process function bft(node) { @@ -49,6 +50,19 @@ function setCaretPosition(element, offset) { sel.addRange(range); } +const processNewlyAddedNodesGeneral2 = function (firstBody) { + let targetWords = [] ; + jsonData.forEach(slur => { + const slurWord = Object.keys(slur)[0]; + targetWords.push(slurWord) ; + targetWords.push(slurWord.charAt(0).toUpperCase() + slurWord.slice(1)); + }) + let uliStore = [] + getAllTextNodes(firstBody, uliStore) + abc = locateSlur(uliStore, targetWords) + addMetaData(targetWords) +} + const processNewlyAddedNodesGeneral = function (firstBody) { log('processing new nodes'); const config = { attributes: true, childList: true, subtree: true }; @@ -72,114 +86,179 @@ const processNewlyAddedNodesGeneral = function (firstBody) { observer.observe(firstBody, config); }; - -// Code inserted below is for uliStore - - -/* getAllTextNodes() STARTS HERE */ - function checkFalseTextNode(text, actualLengthOfText) { - let n = text.length; let totalNewlineAndWhitespaces = 0; - for (let i = 0; i < n; i++) { - if (text[i] === "\n") { - totalNewlineAndWhitespaces += 1; - } - - else if (text[i] === " ") { - totalNewlineAndWhitespaces += 1; + for (let i = 0; i < text.length; i++) { + if (text[i] === "\n" || text[i] === " " || text[i] === "\t") { + totalNewlineAndWhitespaces++; } - - } - if (totalNewlineAndWhitespaces === actualLengthOfText) { - //False Text Node Confirmed - return true; - } - else { - //True Text Node Confirmed - return false; } + return totalNewlineAndWhitespaces === actualLengthOfText; } - -function getAllTextNodes(node) { - let uliStore = [] - if (node.nodeType === 3) { - //If node.data contains just whitespaces and \n, then its a false text node - - // let whitespaces = (node.data.split(" ").length - 1); - // console.log(node.data) ; - if (checkFalseTextNode(node.data, node.length) === false) { +// Function to recursively get all text nodes for a given node +function getAllTextNodes(node, uliStore) { + if (node.nodeType === 3) { + if (!checkFalseTextNode(node.data, node.length)) { uliStore.push({ node: node, parent: node.parentNode }); } - // textNodes.push({ node: node, parent: node.parentNode }); - return; - } - - let children = Array.from(node.childNodes); - for (let i = 0; i < children.length; i++) { - getAllTextNodes(children[i]); + } else { + let children = Array.from(node.childNodes); + children.forEach(child => getAllTextNodes(child, uliStore)); } - - return uliStore ; } - -/* getAllTextNodes() ENDS HERE */ - - -/* locateSlur() STARTS HERE */ - function findPositions(word, text) { let positions = {}; - let len = word.length let loc = [] let index = text.toString().indexOf(word); while (index !== -1) { - let obj = {} ; - loc.push([index , index + len]); + let obj = {}; + loc.push([index, index + len]); index = text.toString().indexOf(word, index + 1); } - - - if(loc.length !== 0){ - positions.slurText = word - positions.slurLocation = loc ; + if (loc.length !== 0) { + positions.slurText = word + positions.slurLocation = loc; } return positions; } -function locateSlur(uliStore, targetWords){ - let n = uliStore.length ; - for(let i = 0 ; i < n ; i++){ - let store = uliStore[i] ; //This will contain the textNode - let parentNode = store.parent - let text = store.node.textContent - //We have to look into this store for all the slurWords - let slurs = [] ; +function locateSlur(uliStore, targetWords) { + let n = uliStore.length; + for (let i = 0; i < n; i++) { + let store = uliStore[i]; + let parentNode = store.parent + let textnode = store.node + let text = store.node.textContent + let tempParent = document.createElement("span"); + tempParent.textContent = text; + let slurs = []; + let slurPresentInTempParent = false; targetWords.forEach(targetWord => { - let slurWord = targetWord ; - let pos = findPositions(slurWord , text) ; - if(Object.keys(pos).length !== 0){ + let slurWord = targetWord; + let pos = findPositions(slurWord, text); + if (Object.keys(pos).length !== 0) { slurs.push(pos) } + + if (tempParent.innerHTML.includes(targetWord)) { + const className = `icon-container-${targetWord}`; + const parts = tempParent.innerHTML.split(targetWord); + const replacedHTML = parts.join(`${targetWord}`); + tempParent.innerHTML = replacedHTML + slurPresentInTempParent = true; + } }) - uliStore[i].slurs = slurs ; + uliStore[i].nodeToParent = tempParent + uliStore[i].slurs = slurs; + + //O(1) complexity + if (slurPresentInTempParent) { + textnode.replaceWith(tempParent) + } } - return uliStore ; //This will return the final uliStore (after appending slurs) + return uliStore; } +function addMetaData(targetWords) { + targetWords.forEach(targetWord => { + const className = `icon-container-${targetWord}` + const elements = Array.from(document.querySelectorAll(`.${className}`)) + elements.forEach(element => { + + let sup = document.createElement("sup"); + let img = document.createElement("img"); + img.style.height = "1.5%" + img.style.width = "1.5%" + img.style.border = "1px solid black" + img.style.cursor = "pointer" + + img.src = "https://raw.githubusercontent.com/tattle-made/Uli/main/uli-website/src/images/favicon-32x32.png" + img.alt = "altText" + + let span = document.createElement("span") + span.style.display = "none" + span.style.position = "absolute" + span.style.backgroundColor = "antiquewhite" + span.style.border = "1px solid black" + span.style.borderRadius = "12px" + span.style.padding = "2px 6px" + span.style.width = "16rem" + span.style.textAlign = "justify" + span.style.fontWeight = "lighter" + span.style.color = "black" + span.style.zIndex = "1000000000"; // This ensures it appears above other elements + span.style.fontSize = "14px" + span.style.textDecoration = "none" + span.style.fontStyle = "normal" + span.innerHTML = `${targetWord} is an offensive word` + + + jsonData.forEach(slur => { + const slurWord = Object.keys(slur)[0]; + if (slurWord.toLowerCase() === targetWord.toLowerCase()) { + const slurDetails = slur[slurWord]; + let levelOfSeverity = slurDetails["Level of Severity"] ; + let casual = slurDetails["Casual"] ; + let approapriated = slurDetails["Appropriated"] ; + let reason = slurDetails["If, Appropriated, Is it by Community or Others?"] ; + let problematic = slurDetails["What Makes it Problematic?"]; + let categories = slurDetails["Categories"] ; + let htmlContent = `` ; + if(levelOfSeverity){ + htmlContent += `

Level of Severity: ${levelOfSeverity}

` + } + if(casual){ + htmlContent += `

Casual: ${casual}

` + } + if(approapriated){ + htmlContent += `

Appropriated: ${approapriated}

` + } + if(reason){ + htmlContent += `

If, Appropriated, Is it by Community or Others?: ${reason}

` + } + if(problematic){ + htmlContent += `

What Makes it Problematic?: ${problematic}

` + } + if(categories.length > 0){ + htmlContent += `

Categories: ${slurDetails["Categories"].join(', ')}

` + } + span.innerHTML = htmlContent; + } + }); + -/* locateSlur() ENDS HERE */ + sup.appendChild(img) + sup.appendChild(span) + element.append(sup) + let sups = element.children[0] + let spans = element.children[0].children[1] + const svgs = element.children[0].children[0]; + svgs.addEventListener('mouseover', function () { + sups.children[1].style.display = "inline-block" + }); + svgs.addEventListener('mouseout', function () { + sups.children[1].style.display = "none" + }); + }) + }) +} export default { - processNewlyAddedNodesGeneral + processNewlyAddedNodesGeneral, + processNewlyAddedNodesGeneral2, + addMetaData, + locateSlur, + findPositions, + getAllTextNodes, + checkFalseTextNode }; diff --git a/browser-extension/plugin/src/ui-components/pages/App.jsx b/browser-extension/plugin/src/ui-components/pages/App.jsx index ced7519b..a4165a48 100644 --- a/browser-extension/plugin/src/ui-components/pages/App.jsx +++ b/browser-extension/plugin/src/ui-components/pages/App.jsx @@ -78,7 +78,8 @@ export function App() { setUser(user); setAccountActivated(true); await setPreferenceData({ - enableSlurReplacement: true + enableSlurReplacement: true, + enableSlurMetadata: false }); userBrowserTabs.reload(); navigate('/preferences'); diff --git a/browser-extension/plugin/src/ui-components/pages/Preferences.jsx b/browser-extension/plugin/src/ui-components/pages/Preferences.jsx index 07dde087..6c1d0810 100644 --- a/browser-extension/plugin/src/ui-components/pages/Preferences.jsx +++ b/browser-extension/plugin/src/ui-components/pages/Preferences.jsx @@ -30,6 +30,7 @@ export function Preferences() { const [enable, setEnable] = useState(true); const [enableML, setEnableMLOption] = useState(false); const [enableSlurReplacement, setEnableSlurReplacement] = useState(true); + const [enableSlurMetadata, setEnableSlurMetadata] = useState(false); const [storeLocally, setStoreLocally] = useState(true); const [language, setLanguage] = useState('English'); const { t, i18n } = useTranslation(); @@ -39,6 +40,7 @@ export function Preferences() { async function getPrefsLocalStorage() { try { const preference = await getPreferenceData(); + console.log("preference " , preference) if (!ignore) { // console.log({ preference }); setLocalPreferences(preference); @@ -51,7 +53,8 @@ export function Preferences() { enableML, storeLocally, language, - enableSlurReplacement + enableSlurReplacement, + enableSlurMetadata } = preference; if (enable != undefined) { setEnable(enable); @@ -68,6 +71,9 @@ export function Preferences() { if (enableSlurReplacement != undefined) { setEnableSlurReplacement(enableSlurReplacement); } + if (enableSlurMetadata != undefined) { + setEnableSlurMetadata(enableSlurMetadata); + } } } } catch (err) { @@ -86,8 +92,10 @@ export function Preferences() { }; }, [user]); - async function handleSlurReplacement(enableSlurReplacement) { + async function handleSlurReplacementAndSlurMetadata(enableSlurReplacement, enableSlurMetadata) { try { + + const confirmed = window.confirm( 'This action requires a page reload. Do you want to continue?' ); @@ -100,7 +108,8 @@ export function Preferences() { await setPreferenceData({ ...localPreferences, - enableSlurReplacement + enableSlurReplacement, + enableSlurMetadata }); userBrowserTabs.sendMessage(tabId, { @@ -116,9 +125,16 @@ export function Preferences() { async function changeEnableSlurReplacementOption(checked) { console.log(checked); + if(checked === true) setEnableSlurMetadata(false); setEnableSlurReplacement(checked); } + async function changeEnableSlurMetadataOption(checked) { + console.log(checked); + if(checked === true) setEnableSlurReplacement(false); + setEnableSlurMetadata(checked); + } + async function clickSave(preference) { const preferenceInLS = await getPreferenceData(); // alert(JSON.stringify({preferenceInLS, preference})) @@ -135,15 +151,19 @@ export function Preferences() { enableML, storeLocally, language, - enableSlurReplacement + enableSlurReplacement, + enableSlurMetadata }); const enableSlurReplacementChanged = enableSlurReplacement !== preferenceInLS.enableSlurReplacement; - if (enableSlurReplacementChanged) { + const enableSlurMetadataChanged = + enableSlurMetadata !== preferenceInLS.enableSlurMetadata; + + if (enableSlurReplacementChanged || enableSlurMetadataChanged) { console.log('enable val changed', enableSlurReplacementChanged); - await handleSlurReplacement(enableSlurReplacement); + await handleSlurReplacementAndSlurMetadata(enableSlurReplacement, enableSlurMetadata); } showNotification({ @@ -224,6 +244,16 @@ export function Preferences() { /> + + + changeEnableSlurMetadataOption(e.target.checked) + } + /> + + { + const actualModule = jest.requireActual('./enableSlurMetadataFunctions.js'); + return { + ...actualModule, + checkFalseTextNode: jest.fn() + }; +}); + +describe('getAllTextNodes', () => { + let dom; + + beforeEach(() => { + dom = new JSDOM(` + + +
+

Valid text node 1

+ Valid text node 2 +
+ Valid text node 3 +
+
+ + + `); + + checkFalseTextNode.mockImplementation((text, length) => { + return text.trim().length === 0; + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should collect all valid text nodes', () => { + const root = dom.window.document.querySelector('#root'); + const uliStore = []; + + getAllTextNodes(root, uliStore); + + expect(uliStore.length).toBe(3); + + expect(uliStore[0].node.textContent.trim()).toBe('Valid text node 1'); + expect(uliStore[1].node.textContent.trim()).toBe('Valid text node 2'); + }); + + it('should ignore text nodes that only contain whitespace', () => { + const root = dom.window.document.querySelector('#root'); + const uliStore = []; + + getAllTextNodes(root, uliStore); + + uliStore.forEach(item => { + expect(item.node.textContent.trim()).not.toBe(''); + }); + }); + + it('should correctly associate the parent node with the text node', () => { + const root = dom.window.document.querySelector('#root'); + const uliStore = []; + + getAllTextNodes(root, uliStore); + + expect(uliStore[0].parent.tagName).toBe('P'); + expect(uliStore[1].parent.tagName).toBe('SPAN'); + }); +}); + +describe('locateSlur', () => { + function setupMockDOM(uliStore) { + uliStore.forEach(store => { + const parentElement = document.createElement('div'); + const textNode = document.createTextNode(store.text); + + parentElement.appendChild(textNode); + store.parent = parentElement; + store.node = textNode; + + document.body.appendChild(parentElement); + }); + } + + beforeEach(() => { + document.body.innerHTML = ''; + }); + + afterEach(() => { + document.body.innerHTML = ''; + }); + + it('replaces target words with annotated spans and updates the DOM correctly', () => { + const targetWords = ['crazy', 'mad']; + + const uliStore = [ + { text: 'This is a crazy situation.', parent: null, node: null }, + { text: 'Don\'t be mad at me!', parent: null, node: null }, + { text: 'This is a normal sentence.', parent: null, node: null } + ]; + + setupMockDOM(uliStore); + + const result = locateSlur(uliStore, targetWords); + + const firstParentElement = result[0].parent; + expect(firstParentElement.innerHTML).toContain('crazy'); + + const secondParentElement = result[1].parent; + expect(secondParentElement.innerHTML).toContain('mad'); + + const thirdParentElement = result[2].parent; + expect(thirdParentElement.innerHTML).toBe('This is a normal sentence.'); + + expect(firstParentElement.querySelector('span')).not.toBeNull(); + expect(secondParentElement.querySelector('span')).not.toBeNull(); + + expect(firstParentElement.textContent).toContain('crazy'); + expect(secondParentElement.textContent).toContain('mad'); + }); + + it('returns the updated uliStore with the correct slur positions', () => { + const targetWords = ['stupid']; + + const uliStore = [ + { text: 'This is a stupid mistake.', parent: null, node: null } + ]; + + setupMockDOM(uliStore); + + const result = locateSlur(uliStore, targetWords); + + const slurs = result[0].slurs; + expect(slurs).toHaveLength(1); + expect(slurs[0]).toHaveProperty('slurText'); + + const parentElement = result[0].parent; + expect(parentElement.innerHTML).toContain('stupid'); + }); +}); + +describe('findPositions', () => { + test('finds positions of a single occurrence of a word', () => { + const result = findPositions('word', 'This is a word in a sentence.'); + expect(result).toEqual({ + slurText: 'word', + slurLocation: [[10, 14]] + }); + }); + + test('finds positions of multiple occurrences of a word', () => { + const result = findPositions('test', 'This test is just a test.'); + expect(result).toEqual({ + slurText: 'test', + slurLocation: [[5, 9], [20, 24]] + }); + }); + + test('returns an empty object if the word is not found', () => { + const result = findPositions('missing', 'This sentence has no such word.'); + expect(result).toEqual({}); + }); +}); + +describe('checkFalseTextNode', () => { + test('returns true for text nodes with only spaces', () => { + expect(checkFalseTextNode(" ", 4)).toBe(true); + }); + + test('returns true for text nodes with newline characters', () => { + expect(checkFalseTextNode("\n\n", 2)).toBe(true); + }); + + test('returns false for text nodes with non-whitespace characters', () => { + expect(checkFalseTextNode("text\n ", 5)).toBe(false); + }); + + test('returns true for text nodes with mixed spaces and tabs', () => { + expect(checkFalseTextNode(" \t \t", 4)).toBe(true); + }); +}); + +describe('addMetaData', () => { + beforeEach(() => { + document.body.innerHTML = ` +
crazy
+
mad
+
stupid
+ `; + }); + + afterEach(() => { + document.body.innerHTML = ''; + }); + + it('adds metadata with tooltip for target words', () => { + const targetWords = ['crazy', 'mad', 'stupid']; + + addMetaData(targetWords); + + targetWords.forEach(targetWord => { + const element = document.querySelector(`.icon-container-${targetWord}`); + expect(element).not.toBeNull(); + + const sup = element.querySelector('sup'); + expect(sup).not.toBeNull(); + + const img = sup.querySelector('img'); + expect(img).not.toBeNull(); + expect(img.src).toContain('Minimalist_info_Icon.png'); + + const span = sup.querySelector('span'); + expect(span).not.toBeNull(); + expect(span.style.display).toBe('none'); + + if (targetWord === 'crazy') { + expect(span.textContent).toContain('perpetuate stereotypes about mental health'); + } else if (targetWord === 'mad') { + expect(span.textContent).toContain('Using "mad" to describe someone negatively can be insensitive'); + } else if (targetWord === 'stupid') { + expect(span.textContent).toContain('Describing actions or decisions as "stupid" can be demeaning'); + } + }); + }); + + it('displays and hides tooltip on mouseover and mouseout', () => { + const targetWords = ['crazy']; + + addMetaData(targetWords); + + const element = document.querySelector('.icon-container-crazy'); + const img = element.querySelector('img'); + const span = element.querySelector('span'); + + const mouseOverEvent = new Event('mouseover'); + img.dispatchEvent(mouseOverEvent); + expect(span.style.display).toBe('inline-block'); + + const mouseOutEvent = new Event('mouseout'); + img.dispatchEvent(mouseOutEvent); + expect(span.style.display).toBe('none'); + }); +}); diff --git a/browser-extension/plugin/test/enableSlurMetadataFunctions.js b/browser-extension/plugin/test/enableSlurMetadataFunctions.js new file mode 100644 index 00000000..e2589fc8 --- /dev/null +++ b/browser-extension/plugin/test/enableSlurMetadataFunctions.js @@ -0,0 +1,197 @@ + +/* getAllTextNodes() STARTS HERE */ + +function checkFalseTextNode(text, actualLengthOfText) { + let totalNewlineAndWhitespaces = 0; + for (let i = 0; i < text.length; i++) { + if (text[i] === "\n" || text[i] === " " || text[i] === "\t") { + // I think not only \n and " " , if a textNode is comprised of any escape characters and whitespace, it should be a false textNode + totalNewlineAndWhitespaces++; + } + } + return totalNewlineAndWhitespaces === actualLengthOfText; +} + + +// Function to recursively get all text nodes under a given node +function getAllTextNodes(node, uliStore) { + if (node.nodeType === 3) { // Text node + if (!checkFalseTextNode(node.data, node.length)) { + uliStore.push({ node: node, parent: node.parentNode }); + } + } else { + let children = Array.from(node.childNodes); + children.forEach(child => getAllTextNodes(child, uliStore)); + } +} + + + +/* getAllTextNodes() ENDS HERE */ + + +/* locateSlur() STARTS HERE */ + +function findPositions(word, text) { + let positions = {}; + + let len = word.length + let loc = [] + let index = text.toString().indexOf(word); + while (index !== -1) { + let obj = {}; + loc.push([index, index + len]); + index = text.toString().indexOf(word, index + 1); + } + + + if (loc.length !== 0) { + positions.slurText = word + positions.slurLocation = loc; + } + return positions; +} + + + +function locateSlur(uliStore, targetWords) { + let n = uliStore.length; + + for (let i = 0; i < n; i++) { + let store = uliStore[i]; //This will contain the textNode + let parentNode = store.parent + let textnode = store.node + let text = store.node.textContent + let tempParent = document.createElement("span"); //I chose span over p because span is an inline element and p is a block element, injecting block + //element would cause a lot of change in the stylings and can damage the overall webpage to a greater extent + tempParent.textContent = text; + + let slurs = []; + let slurPresentInTempParent = false; + targetWords.forEach(targetWord => { + let slurWord = targetWord; + let pos = findPositions(slurWord, text); + if (Object.keys(pos).length !== 0) { + slurs.push(pos) + } + + if (tempParent.innerHTML.includes(targetWord)) { + const className = `icon-container-${targetWord}`; + const parts = tempParent.innerHTML.split(targetWord); + const replacedHTML = parts.join(`${targetWord}`); + tempParent.innerHTML = replacedHTML + slurPresentInTempParent = true; + } + }) + // for(let i = 0 ; i < ) + // console.log("tempParent " , tempParent) + uliStore[i].nodeToParent = tempParent + uliStore[i].slurs = slurs; + + //O(1) complexity + if (slurPresentInTempParent) { + // parentNode.replaceChild(tempParent, textnode) + textnode.replaceWith(tempParent) + // textnode.replaceWith(createTextNode(tempParent.innerHTML)) + + } + + } + return uliStore; +} + + +/* locateSlur() ENDS HERE */ + + + + +/* addMetaData() STARTS HERE */ + +function addMetaData(targetWords) { + targetWords.forEach(targetWord => { + const className = `icon-container-${targetWord}` + const elements = Array.from(document.querySelectorAll(`.${className}`)) + elements.forEach(element => { + + let sup = document.createElement("sup"); + + let img = document.createElement("img"); + img.style.height = "2%" + img.style.width = "2%" + img.style.cursor = "pointer" + img.src = "https://upload.wikimedia.org/wikipedia/commons/4/43/Minimalist_info_Icon.png" + // img.src = "./info.png" + img.alt = "altText" + + let span = document.createElement("span") + // span.style.all = "unset" + span.style.display = "none" + span.style.position = "absolute" + span.style.backgroundColor = "antiquewhite" + span.style.border = "1px solid black" + span.style.borderRadius = "12px" + span.style.padding = "2px 6px" + // span.style.width = "12rem" + span.style.textAlign = "justify" + span.style.fontWeight = "lighter" + span.style.color = "black" + span.style.zIndex = "1000000000"; // This ensures it appears above other elements + span.style.fontSize = "14px" + span.style.textDecoration = "none" + span.style.fontStyle = "normal" + // span.style.height = "fit-content" + span.innerHTML = `This is ${targetWord}` + + // span.style.display = "none"; + // span.style.position = "absolute"; + // span.style.backgroundColor = "antiquewhite !important"; + // span.style.border = "1px solid black !important"; + // span.style.borderRadius = "12px !important"; + // span.style.padding = "2px 6px !important"; + // span.style.width = "12rem !important"; + // span.style.textAlign = "justify !important"; + // span.innerHTML = `This is ${targetWord}`; + + + + + if (targetWord.toLowerCase() === "crazy") { + span.innerHTML = `It can perpetuate stereotypes about mental health and may be hurtful to those with mental health conditions.` + } + else if (targetWord.toLowerCase() === "mad") { + span.innerHTML = `Using "mad" to describe someone negatively can be insensitive.` + } + else if (targetWord.toLowerCase() === 'stupid') { + span.innerHTML = `Describing actions or decisions as "stupid" can be demeaning and hurtful.` + } + else { + span.innerHTML = `This word is considered offensive.` + } + + + sup.appendChild(img) + sup.appendChild(span) + + element.append(sup) + let sups = element.children[0] + let spans = element.children[0].children[1] + const svgs = element.children[0].children[0]; + svgs.addEventListener('mouseover', function () { + sups.children[1].style.display = "inline-block" + // sups.children[1].style.display = "block" + }); + + svgs.addEventListener('mouseout', function () { + sups.children[1].style.display = "none" + }); + }) + }) +} + + +/* addMetaData() ENDS HERE */ + + + +module.exports = { addMetaData, locateSlur, findPositions, getAllTextNodes, checkFalseTextNode } \ No newline at end of file