@govtechsg/oobee 0.10.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/.dockerignore +22 -0
  2. package/.github/pull_request_template.md +11 -0
  3. package/.github/workflows/docker-test.yml +54 -0
  4. package/.github/workflows/image.yml +107 -0
  5. package/.github/workflows/publish.yml +18 -0
  6. package/.idea/modules.xml +8 -0
  7. package/.idea/purple-a11y.iml +9 -0
  8. package/.idea/vcs.xml +6 -0
  9. package/.prettierrc.json +12 -0
  10. package/.vscode/extensions.json +5 -0
  11. package/.vscode/settings.json +10 -0
  12. package/CODE_OF_CONDUCT.md +128 -0
  13. package/DETAILS.md +163 -0
  14. package/Dockerfile +60 -0
  15. package/INSTALLATION.md +146 -0
  16. package/INTEGRATION.md +785 -0
  17. package/LICENSE +22 -0
  18. package/README.md +587 -0
  19. package/SECURITY.md +5 -0
  20. package/__mocks__/mock-report.html +1431 -0
  21. package/__mocks__/mockFunctions.ts +32 -0
  22. package/__mocks__/mockIssues.ts +64 -0
  23. package/__mocks__/mock_all_issues/000000001.json +64 -0
  24. package/__mocks__/mock_all_issues/000000002.json +53 -0
  25. package/__mocks__/mock_all_issues/fake-file.txt +0 -0
  26. package/__tests__/logs.test.ts +25 -0
  27. package/__tests__/mergeAxeResults.test.ts +278 -0
  28. package/__tests__/utils.test.ts +118 -0
  29. package/a11y-scan-results.zip +0 -0
  30. package/eslint.config.js +53 -0
  31. package/exclusions.txt +2 -0
  32. package/gitlab-pipeline-template.yml +54 -0
  33. package/jest.config.js +1 -0
  34. package/package.json +96 -0
  35. package/scripts/copyFiles.js +44 -0
  36. package/scripts/install_oobee_dependencies.cmd +13 -0
  37. package/scripts/install_oobee_dependencies.command +101 -0
  38. package/scripts/install_oobee_dependencies.ps1 +110 -0
  39. package/scripts/oobee_shell.cmd +13 -0
  40. package/scripts/oobee_shell.command +11 -0
  41. package/scripts/oobee_shell.sh +55 -0
  42. package/scripts/oobee_shell_ps.ps1 +54 -0
  43. package/src/cli.ts +401 -0
  44. package/src/combine.ts +240 -0
  45. package/src/constants/__tests__/common.test.ts +44 -0
  46. package/src/constants/cliFunctions.ts +305 -0
  47. package/src/constants/common.ts +1840 -0
  48. package/src/constants/constants.ts +443 -0
  49. package/src/constants/errorMeta.json +319 -0
  50. package/src/constants/itemTypeDescription.ts +11 -0
  51. package/src/constants/oobeeAi.ts +141 -0
  52. package/src/constants/questions.ts +181 -0
  53. package/src/constants/sampleData.ts +187 -0
  54. package/src/crawlers/__tests__/commonCrawlerFunc.test.ts +51 -0
  55. package/src/crawlers/commonCrawlerFunc.ts +656 -0
  56. package/src/crawlers/crawlDomain.ts +877 -0
  57. package/src/crawlers/crawlIntelligentSitemap.ts +156 -0
  58. package/src/crawlers/crawlLocalFile.ts +193 -0
  59. package/src/crawlers/crawlSitemap.ts +356 -0
  60. package/src/crawlers/custom/extractAndGradeText.ts +57 -0
  61. package/src/crawlers/custom/flagUnlabelledClickableElements.ts +964 -0
  62. package/src/crawlers/custom/utils.ts +486 -0
  63. package/src/crawlers/customAxeFunctions.ts +82 -0
  64. package/src/crawlers/pdfScanFunc.ts +468 -0
  65. package/src/crawlers/runCustom.ts +117 -0
  66. package/src/index.ts +173 -0
  67. package/src/logs.ts +66 -0
  68. package/src/mergeAxeResults.ts +964 -0
  69. package/src/npmIndex.ts +284 -0
  70. package/src/screenshotFunc/htmlScreenshotFunc.ts +411 -0
  71. package/src/screenshotFunc/pdfScreenshotFunc.ts +762 -0
  72. package/src/static/ejs/partials/components/categorySelector.ejs +4 -0
  73. package/src/static/ejs/partials/components/categorySelectorDropdown.ejs +57 -0
  74. package/src/static/ejs/partials/components/pagesScannedModal.ejs +70 -0
  75. package/src/static/ejs/partials/components/reportSearch.ejs +47 -0
  76. package/src/static/ejs/partials/components/ruleOffcanvas.ejs +105 -0
  77. package/src/static/ejs/partials/components/scanAbout.ejs +263 -0
  78. package/src/static/ejs/partials/components/screenshotLightbox.ejs +13 -0
  79. package/src/static/ejs/partials/components/summaryScanAbout.ejs +141 -0
  80. package/src/static/ejs/partials/components/summaryScanResults.ejs +16 -0
  81. package/src/static/ejs/partials/components/summaryTable.ejs +20 -0
  82. package/src/static/ejs/partials/components/summaryWcagCompliance.ejs +94 -0
  83. package/src/static/ejs/partials/components/topFive.ejs +6 -0
  84. package/src/static/ejs/partials/components/wcagCompliance.ejs +70 -0
  85. package/src/static/ejs/partials/footer.ejs +21 -0
  86. package/src/static/ejs/partials/header.ejs +230 -0
  87. package/src/static/ejs/partials/main.ejs +40 -0
  88. package/src/static/ejs/partials/scripts/bootstrap.ejs +8 -0
  89. package/src/static/ejs/partials/scripts/categorySelectorDropdownScript.ejs +190 -0
  90. package/src/static/ejs/partials/scripts/categorySummary.ejs +141 -0
  91. package/src/static/ejs/partials/scripts/highlightjs.ejs +335 -0
  92. package/src/static/ejs/partials/scripts/popper.ejs +7 -0
  93. package/src/static/ejs/partials/scripts/reportSearch.ejs +248 -0
  94. package/src/static/ejs/partials/scripts/ruleOffcanvas.ejs +801 -0
  95. package/src/static/ejs/partials/scripts/screenshotLightbox.ejs +71 -0
  96. package/src/static/ejs/partials/scripts/summaryScanResults.ejs +14 -0
  97. package/src/static/ejs/partials/scripts/summaryTable.ejs +78 -0
  98. package/src/static/ejs/partials/scripts/utils.ejs +441 -0
  99. package/src/static/ejs/partials/styles/bootstrap.ejs +12375 -0
  100. package/src/static/ejs/partials/styles/highlightjs.ejs +54 -0
  101. package/src/static/ejs/partials/styles/styles.ejs +1843 -0
  102. package/src/static/ejs/partials/styles/summaryBootstrap.ejs +12458 -0
  103. package/src/static/ejs/partials/summaryHeader.ejs +70 -0
  104. package/src/static/ejs/partials/summaryMain.ejs +75 -0
  105. package/src/static/ejs/report.ejs +420 -0
  106. package/src/static/ejs/summary.ejs +47 -0
  107. package/src/static/mustache/.prettierrc +4 -0
  108. package/src/static/mustache/Attention Deficit.mustache +11 -0
  109. package/src/static/mustache/Blind.mustache +11 -0
  110. package/src/static/mustache/Cognitive.mustache +7 -0
  111. package/src/static/mustache/Colorblindness.mustache +20 -0
  112. package/src/static/mustache/Deaf.mustache +12 -0
  113. package/src/static/mustache/Deafblind.mustache +7 -0
  114. package/src/static/mustache/Dyslexia.mustache +14 -0
  115. package/src/static/mustache/Low Vision.mustache +7 -0
  116. package/src/static/mustache/Mobility.mustache +15 -0
  117. package/src/static/mustache/Sighted Keyboard Users.mustache +42 -0
  118. package/src/static/mustache/report.mustache +1709 -0
  119. package/src/types/print-message.d.ts +28 -0
  120. package/src/types/types.ts +46 -0
  121. package/src/types/xpath-to-css.d.ts +3 -0
  122. package/src/utils.ts +332 -0
  123. package/tsconfig.json +15 -0
@@ -0,0 +1,71 @@
1
+ <%# functions used to show lightbox of screenshot when thumbnail is clicked on %>
2
+ <script>
3
+ const lightbox = document.getElementsByClassName('screenshot-lightbox')[0];
4
+ const lightboxHeader = document.getElementsByClassName('lightbox-header')[0];
5
+ const lightboxTitle = document.querySelector('.lightbox-header h5');
6
+ const lightboxContent = document.getElementsByClassName('lightbox-content')[0];
7
+ const lightboxImg = document.getElementById('lightbox-image');
8
+
9
+ var customFlowScreenshots = document.getElementsByClassName('custom-flow-screenshot');
10
+ Array.from(customFlowScreenshots).forEach(screenshot => {
11
+ screenshot.onerror = function(event) {
12
+ screenshot.onerror = null;
13
+ screenshot.remove();
14
+ }
15
+ screenshot.onclick = event => {
16
+ event.preventDefault();
17
+ const pageTitle = screenshot.parentNode.getElementsByTagName('a')[0].textContent;
18
+ const pageUrl = screenshot.parentNode.getElementsByTagName('a')[0].href;
19
+ openLightbox(screenshot.src, pageTitle, pageUrl);
20
+ }
21
+ })
22
+
23
+ lightbox.addEventListener('click', event => {
24
+ if (event.target === lightbox || event.target === lightboxHeader || event.target === lightboxTitle) {
25
+ closeLightbox();
26
+ }
27
+ })
28
+
29
+ const offcanvasElem = document.getElementsByClassName('offcanvas')[0];
30
+ const offcanvasItem = new bootstrap.Offcanvas(offcanvasElem);
31
+ offcanvasItem._config.keyboard = false; // Disable default keyboard handling
32
+
33
+ const pagesScannedModalElem = document.getElementById('pagesScannedModal');
34
+ const pagesScannedModalItem = new bootstrap.Modal(pagesScannedModalElem);
35
+ pagesScannedModalItem._config.keyboard = false;
36
+
37
+ document.addEventListener('keydown', event => {
38
+ if (event.key === 'Escape') {
39
+ if (offcanvasItem._isShown) {
40
+ if (lightbox.style.display === 'block') {
41
+ event.preventDefault(); // Prevent default bootstrap behaviour
42
+ closeLightbox();
43
+ } else {
44
+ offcanvasItem.hide();
45
+ }
46
+ }
47
+
48
+ if (pagesScannedModalItem._isShown) {
49
+ if (lightbox.style.display === 'block') {
50
+ event.preventDefault(); // Prevent default bootstrap behaviour
51
+ closeLightbox();
52
+ } else {
53
+ pagesScannedModalItem.hide();
54
+ }
55
+ }
56
+ }
57
+ })
58
+
59
+ const openLightbox = (imgSrc, pageTitle, pageUrl) => {
60
+ lightbox.style.display = 'block';
61
+
62
+ lightboxImg.src = imgSrc;
63
+ lightboxImg.alt = `Screenshot of ${pageUrl}`;
64
+
65
+ lightboxTitle.textContent = pageTitle;
66
+ }
67
+
68
+ const closeLightbox = () => {
69
+ lightbox.style.display = 'none';
70
+ }
71
+ </script>
@@ -0,0 +1,14 @@
1
+ <script>
2
+ document.getElementById('summarymustFixIcon').src =
3
+ "data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17.1429 0H2.85714C1.27919 0 0 1.27919 0 2.85714V17.1429C0 18.7208 1.27919 20 2.85714 20H17.1429C18.7208 20 20 18.7208 20 17.1429V2.85714C20 1.27919 18.7208 0 17.1429 0Z' fill='%23f26949'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M11.4292 9.99822V5.71251C11.4292 4.92353 10.7896 4.28394 10.0006 4.28394C9.21161 4.28394 8.57202 4.92353 8.57202 5.71251V9.99822C8.57202 10.7872 9.21161 11.4268 10.0006 11.4268C10.7896 11.4268 11.4292 10.7872 11.4292 9.99822ZM10.0006 12.8554C9.21202 12.8554 8.57202 13.4954 8.57202 14.2839C8.57202 15.0725 9.21202 15.7125 10.0006 15.7125C10.7892 15.7125 11.4292 15.0725 11.4292 14.2839C11.4292 13.4954 10.7892 12.8554 10.0006 12.8554Z' fill='white'/%3E%3C/svg%3E%0A";
4
+ document.getElementById('summarymustFixIcon').alt = 'must fix icon';
5
+ document.getElementById('summarygoodToFixIcon').src =
6
+ "data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17.1429 0H2.85714C1.27919 0 0 1.27919 0 2.85714V17.1429C0 18.7208 1.27919 20 2.85714 20H17.1429C18.7208 20 20 18.7208 20 17.1429V2.85714C20 1.27919 18.7208 0 17.1429 0Z' fill='%23ffb200'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M10 6.5625C8.10131 6.5625 6.5625 8.10131 6.5625 10C6.5625 11.8987 8.10131 13.4375 10 13.4375C11.8987 13.4375 13.4375 11.8987 13.4375 10C13.4375 8.10131 11.8987 6.5625 10 6.5625ZM4.84375 10C4.84375 7.15207 7.15207 4.84375 10 4.84375C12.8479 4.84375 15.1562 7.15207 15.1562 10C15.1562 12.8479 12.8479 15.1562 10 15.1562C7.15207 15.1562 4.84375 12.8479 4.84375 10Z' fill='white'/%3E%3C/svg%3E%0A";
7
+ document.getElementById('summarygoodToFixIcon').alt = 'good to fix icon';
8
+ document.getElementById('summaryneedsReviewIcon').src =
9
+ "data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17.1429 0H2.85714C1.27919 0 0 1.27919 0 2.85714V17.1429C0 18.7208 1.27919 20 2.85714 20H17.1429C18.7208 20 20 18.7208 20 17.1429V2.85714C20 1.27919 18.7208 0 17.1429 0Z' fill='%23c9c8c6'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M8.09759 7.26594C8.23445 6.67496 8.46462 6.23328 8.7881 5.9409C9.14891 5.65474 9.57815 5.51166 10.0758 5.51166C10.5984 5.51166 11.0152 5.64852 11.3262 5.92224C11.6372 6.19596 11.7928 6.53188 11.7928 6.93002C11.7928 7.22862 11.7057 7.48367 11.5315 7.69518C11.4195 7.84448 11.0712 8.15552 10.4864 8.6283C9.90785 9.11353 9.51594 9.54277 9.31065 9.91602C9.12403 10.3079 9.03072 10.7745 9.03072 11.3157V11.7449L9.04938 11.8569C9.04938 11.9316 9.06026 12 9.08204 12.0622C9.10381 12.1244 9.12558 12.1742 9.14736 12.2115C9.16913 12.2488 9.20645 12.2815 9.25933 12.3095C9.31221 12.3375 9.35264 12.3593 9.38064 12.3748C9.40863 12.3904 9.45995 12.4012 9.5346 12.4075C9.60925 12.4137 9.66058 12.4168 9.68857 12.4168H9.85187H10.0012C10.2065 12.4168 10.3589 12.409 10.4584 12.3935C10.5579 12.3779 10.6466 12.3313 10.7243 12.2535C10.8021 12.1757 10.841 12.056 10.841 11.8942C10.841 11.6827 10.8534 11.465 10.8783 11.2411C10.897 11.0109 10.9343 10.8305 10.9903 10.6998C11.0712 10.5008 11.3324 10.2208 11.7741 9.86003C12.6077 9.15707 13.1427 8.61587 13.3791 8.23639C13.6279 7.83204 13.7523 7.41524 13.7523 6.986C13.7523 6.17729 13.4102 5.48678 12.7259 4.91446C12.0603 4.30482 11.1458 4 9.9825 4C8.88763 4 8.00428 4.2986 7.33243 4.8958C6.71656 5.41835 6.36198 6.07154 6.26866 6.85537C6.25622 6.89269 6.25 6.9549 6.25 7.04199C6.25 7.29705 6.3402 7.51633 6.52061 7.69984C6.70101 7.88336 6.91563 7.97512 7.16446 7.97512C7.37597 7.97512 7.56571 7.90824 7.73367 7.77449C7.90163 7.64075 8.0105 7.47123 8.06026 7.26594H8.09759ZM10.0012 13.2939C9.62791 13.2939 9.30754 13.4246 9.04005 13.6858C8.77255 13.9471 8.6388 14.2644 8.6388 14.6376C8.6388 15.0047 8.77255 15.3235 9.04005 15.5941C9.30754 15.8647 9.62791 16 10.0012 16C10.3744 16 10.6948 15.8647 10.9623 15.5941C11.2298 15.3235 11.3635 15.0047 11.3635 14.6376C11.3635 14.2644 11.2298 13.9471 10.9623 13.6858C10.6948 13.4246 10.3744 13.2939 10.0012 13.2939Z' fill='white'/%3E%3C/svg%3E%0A";
10
+ document.getElementById('summaryneedsReviewIcon').alt = 'needs review icon';
11
+ document.getElementById('summarypassedIcon').src =
12
+ "data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17.1429 0H2.85714C1.27919 0 0 1.27919 0 2.85714V17.1429C0 18.7208 1.27919 20 2.85714 20H17.1429C18.7208 20 20 18.7208 20 17.1429V2.85714C20 1.27919 18.7208 0 17.1429 0Z' fill='%233aa566'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M15.1781 5.3131C15.7942 5.80597 15.8941 6.70496 15.4012 7.32105L9.68694 14.4639C9.19407 15.08 8.29508 15.1799 7.67899 14.687C7.0629 14.1941 6.96301 13.2951 7.45589 12.6791L13.1702 5.5362C13.663 4.92011 14.562 4.82023 15.1781 5.3131Z' fill='white'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M4.70415 9.70455C5.26204 9.14666 6.16656 9.14666 6.72446 9.70455L9.5816 12.5617C10.1395 13.1196 10.1395 14.0241 9.5816 14.582C9.02371 15.1399 8.11919 15.1399 7.56129 14.582L4.70415 11.7249C4.14626 11.167 4.14626 10.2624 4.70415 9.70455Z' fill='white'/%3E%3C/svg%3E%0A";
13
+ document.getElementById('summarypassedIcon').alt = 'passed icon';
14
+ </script>
@@ -0,0 +1,78 @@
1
+ <script>
2
+ Object.keys(scanItems).forEach(category => {
3
+ if (category !== 'passed') {
4
+ const ruleInCategory = scanItems[category].rules.forEach((rule, index) => {
5
+ helpUrl = `<a href=${rule.helpUrl} target="_blank" class="help-link"></a>`;
6
+ wcagLink =
7
+ rule.conformance[0] === 'best-practice'
8
+ ? `<span style="color:#26241b">Best practice</span>`
9
+ : generateWcagConformanceLinks(rule.conformance);
10
+ description =
11
+ `${rule.description}`.replace(/</g, '&lt;').replace(/>/g, '&gt;') + `\n${wcagLink}`;
12
+
13
+ rowStrings = [
14
+ `${category}-description-${index}`,
15
+ `${category}-occurrences-${index}`,
16
+ `${category}-pages-${index}`,
17
+ `${category}-helpUrl-${index}`,
18
+ ];
19
+
20
+ var newRow = document.createElement('tr');
21
+ var newCell = document.createElement('td');
22
+ newCell.setAttribute('class', `table-${category}-icon`);
23
+ newRow.append(newCell);
24
+
25
+ rowStrings.forEach(string => {
26
+ var newCell = document.createElement('td');
27
+ newCell.setAttribute('id', `table-${string}`);
28
+ newRow.append(newCell);
29
+ });
30
+
31
+ document.getElementById('summary-table-contents').appendChild(newRow);
32
+
33
+ document.getElementById(`table-${category}-description-${index}`).innerHTML = description;
34
+ document
35
+ .getElementById(`table-${category}-description-${index}`)
36
+ .setAttribute('style', 'text-align: left; padding-left: 10px;white-space:break-spaces');
37
+
38
+ document.getElementById(`table-${category}-occurrences-${index}`).innerHTML =
39
+ rule.totalItems;
40
+ document
41
+ .getElementById(`table-${category}-occurrences-${index}`)
42
+ .setAttribute('style', 'text-align: right; padding-right: 10px');
43
+
44
+ document.getElementById(`table-${category}-pages-${index}`).innerHTML =
45
+ rule.pagesAffected.length;
46
+ document
47
+ .getElementById(`table-${category}-pages-${index}`)
48
+ .setAttribute('style', 'text-align: right;');
49
+
50
+ document.getElementById(`table-${category}-helpUrl-${index}`).innerHTML = helpUrl;
51
+ });
52
+ }
53
+ });
54
+
55
+ const allMustFixIcons = document.getElementsByClassName('table-mustFix-icon');
56
+ for (let i = 0; i < allMustFixIcons.length; i++) {
57
+ const mustFixImg = document.createElement('img');
58
+ mustFixImg.alt = 'must fix icon';
59
+ mustFixImg.src = `data:image/svg+xml,%3Csvg width='25' height='25' viewBox='0 0 20 21' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17.1429 0.102879H2.85714C1.27919 0.102879 0 1.38206 0 2.96002V17.2457C0 18.8237 1.27919 20.1029 2.85714 20.1029H17.1429C18.7208 20.1029 20 18.8237 20 17.2457V2.96002C20 1.38206 18.7208 0.102879 17.1429 0.102879Z' fill='%23f26949'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M11.4294 10.101V5.81531C11.4294 5.02633 10.7898 4.38673 10.0008 4.38673C9.21186 4.38673 8.57227 5.02633 8.57227 5.81531V10.101C8.57227 10.89 9.21186 11.5296 10.0008 11.5296C10.7898 11.5296 11.4294 10.89 11.4294 10.101ZM10.0008 12.9582C9.21227 12.9582 8.57227 13.5982 8.57227 14.3867C8.57227 15.1753 9.21227 15.8153 10.0008 15.8153C10.7894 15.8153 11.4294 15.1753 11.4294 14.3867C11.4294 13.5982 10.7894 12.9582 10.0008 12.9582Z' fill='white'/%3E%3C/svg%3E%0A`;
60
+ allMustFixIcons[i].appendChild(mustFixImg);
61
+ }
62
+
63
+ const allGoodToFixIcons = document.getElementsByClassName('table-goodToFix-icon');
64
+ for (let i = 0; i < allGoodToFixIcons.length; i++) {
65
+ const goodToFixImg = document.createElement('img');
66
+ goodToFixImg.alt = 'good to fix icon';
67
+ goodToFixImg.src = `data:image/svg+xml,%3Csvg width='25' height='25' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17.1429 0H2.85714C1.27919 0 0 1.27919 0 2.85714V17.1429C0 18.7208 1.27919 20 2.85714 20H17.1429C18.7208 20 20 18.7208 20 17.1429V2.85714C20 1.27919 18.7208 0 17.1429 0Z' fill='%23ffb200'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M10 6.5625C8.10131 6.5625 6.5625 8.10131 6.5625 10C6.5625 11.8987 8.10131 13.4375 10 13.4375C11.8987 13.4375 13.4375 11.8987 13.4375 10C13.4375 8.10131 11.8987 6.5625 10 6.5625ZM4.84375 10C4.84375 7.15207 7.15207 4.84375 10 4.84375C12.8479 4.84375 15.1562 7.15207 15.1562 10C15.1562 12.8479 12.8479 15.1562 10 15.1562C7.15207 15.1562 4.84375 12.8479 4.84375 10Z' fill='white'/%3E%3C/svg%3E%0A`;
68
+ allGoodToFixIcons[i].appendChild(goodToFixImg);
69
+ }
70
+
71
+ const allNeedsReviewIcons = document.getElementsByClassName('table-needsReview-icon');
72
+ for (let i = 0; i < allNeedsReviewIcons.length; i++) {
73
+ const needsReviewImg = document.createElement('img');
74
+ needsReviewImg.alt = 'needs review icon';
75
+ needsReviewImg.src = `data:image/svg+xml,%3Csvg width='20' height='20' viewBox='0 0 20 20' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17.1429 0H2.85714C1.27919 0 0 1.27919 0 2.85714V17.1429C0 18.7208 1.27919 20 2.85714 20H17.1429C18.7208 20 20 18.7208 20 17.1429V2.85714C20 1.27919 18.7208 0 17.1429 0Z' fill='%23c9c8c6'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M8.09759 7.26594C8.23445 6.67496 8.46462 6.23328 8.7881 5.9409C9.14891 5.65474 9.57815 5.51166 10.0758 5.51166C10.5984 5.51166 11.0152 5.64852 11.3262 5.92224C11.6372 6.19596 11.7928 6.53188 11.7928 6.93002C11.7928 7.22862 11.7057 7.48367 11.5315 7.69518C11.4195 7.84448 11.0712 8.15552 10.4864 8.6283C9.90785 9.11353 9.51594 9.54277 9.31065 9.91602C9.12403 10.3079 9.03072 10.7745 9.03072 11.3157V11.7449L9.04938 11.8569C9.04938 11.9316 9.06026 12 9.08204 12.0622C9.10381 12.1244 9.12558 12.1742 9.14736 12.2115C9.16913 12.2488 9.20645 12.2815 9.25933 12.3095C9.31221 12.3375 9.35264 12.3593 9.38064 12.3748C9.40863 12.3904 9.45995 12.4012 9.5346 12.4075C9.60925 12.4137 9.66058 12.4168 9.68857 12.4168H9.85187H10.0012C10.2065 12.4168 10.3589 12.409 10.4584 12.3935C10.5579 12.3779 10.6466 12.3313 10.7243 12.2535C10.8021 12.1757 10.841 12.056 10.841 11.8942C10.841 11.6827 10.8534 11.465 10.8783 11.2411C10.897 11.0109 10.9343 10.8305 10.9903 10.6998C11.0712 10.5008 11.3324 10.2208 11.7741 9.86003C12.6077 9.15707 13.1427 8.61587 13.3791 8.23639C13.6279 7.83204 13.7523 7.41524 13.7523 6.986C13.7523 6.17729 13.4102 5.48678 12.7259 4.91446C12.0603 4.30482 11.1458 4 9.9825 4C8.88763 4 8.00428 4.2986 7.33243 4.8958C6.71656 5.41835 6.36198 6.07154 6.26866 6.85537C6.25622 6.89269 6.25 6.9549 6.25 7.04199C6.25 7.29705 6.3402 7.51633 6.52061 7.69984C6.70101 7.88336 6.91563 7.97512 7.16446 7.97512C7.37597 7.97512 7.56571 7.90824 7.73367 7.77449C7.90163 7.64075 8.0105 7.47123 8.06026 7.26594H8.09759ZM10.0012 13.2939C9.62791 13.2939 9.30754 13.4246 9.04005 13.6858C8.77255 13.9471 8.6388 14.2644 8.6388 14.6376C8.6388 15.0047 8.77255 15.3235 9.04005 15.5941C9.30754 15.8647 9.62791 16 10.0012 16C10.3744 16 10.6948 15.8647 10.9623 15.5941C11.2298 15.3235 11.3635 15.0047 11.3635 14.6376C11.3635 14.2644 11.2298 13.9471 10.9623 13.6858C10.6948 13.4246 10.3744 13.2939 10.0012 13.2939Z' fill='white'/%3E%3C/svg%3E%0A`;
76
+ allNeedsReviewIcons[i].appendChild(needsReviewImg);
77
+ }
78
+ </script>
@@ -0,0 +1,441 @@
1
+ <%# utility functions %>
2
+ <script>
3
+ function createElementFromString(htmlString) {
4
+ const tempContainer = document.createElement('div');
5
+ tempContainer.innerHTML = htmlString.trim();
6
+ return tempContainer.firstChild;
7
+ }
8
+
9
+ const oobeeAiHtmlETL = <%- oobeeAi.htmlETL %>;
10
+ const oobeeAiRules = <%- JSON.stringify(oobeeAi.rules) %>;
11
+
12
+ // extract tagname and attribute name from html tag
13
+ // e.g. ["input", "type", "value", "role"] from <input type="text" value="..." role="..." />
14
+ const getHtmlTagAndAttributes = (htmlString) => {
15
+ const regex = /<(\w+)(\s+(\w+)(\s*=\s*"[^"]*")?)*\s*\/?>/;
16
+ const match = htmlString.match(regex); // check if structure of html tag is valid
17
+
18
+ if (match) {
19
+ const tagName = match[1];
20
+ const attributes = match[0]
21
+ .match(/\w+\s*=\s*"[^"]*"/g) // extract attributes e.g. ['type="text"', 'value="..."']
22
+ .map((attr) => attr.match(/(\w+)\s*=/)[1]); // get the name e.g. "type" from each
23
+ return [tagName, ...attributes];
24
+ }
25
+ return [];
26
+ };
27
+
28
+ const rulesUsingRoles = [
29
+ 'aria-allowed-attr',
30
+ 'aria-required-attr',
31
+ 'aria-required-children',
32
+ 'aria-required-parent',
33
+ 'aria-roles',
34
+ 'aria-allowed-role',
35
+ ];
36
+
37
+ const escapeHtmlForAI = html => {
38
+ return html
39
+ .replace(/&/g, '&amp;')
40
+ .replace(/</g, '&lt;')
41
+ .replace(/>/g, '&gt;')
42
+ .replace(/"/g, '&quot;')
43
+ .replace(/'/g, '&#039;');
44
+ };
45
+
46
+ // getLocalStorageUUID returns the local storage's uuid if exists if not generates one
47
+ // and returns the newly generated uuid
48
+ const getLocalStorageUUID = () => {
49
+ const storagePath = '<%= storagePath %>';
50
+ let uuid = localStorage.getItem(`${storagePath}-uuid`);
51
+ if (uuid) {
52
+ return uuid;
53
+ }
54
+
55
+ const generateUUID = () => {
56
+ return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
57
+ (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16),
58
+ );
59
+ };
60
+ uuid = generateUUID();
61
+ localStorage.setItem(`${storagePath}-uuid`, uuid);
62
+
63
+ return uuid;
64
+ }
65
+
66
+ const formatFeedbackFormUrl = (uuid, websiteUrl, ruleId, basicHtml, usefulCount, notUsefulCount) => {
67
+ // AI Response Feedback Data Fields
68
+ const
69
+ formUrl = `https://docs.google.com/forms/d/e/1FAIpQLSdWbOHX9ggWlL7JUAz8s-Jmv-fjvM-R7dcFwqm1gTZ-sNcxHg/formResponse`,
70
+ uuidField = 'entry.1599149312',
71
+ websiteUrlField = 'entry.1146860880',
72
+ ruleIdField = 'entry.942691547',
73
+ basicHtmlField = 'entry.2085981400',
74
+ usefulCountField = 'entry.809957654',
75
+ notUsefulCountField = 'entry.89318735';
76
+
77
+ return `${formUrl}?${uuidField}=${uuid}&${websiteUrlField}=${websiteUrl}&${ruleIdField}=${ruleId}&${basicHtmlField}=${basicHtml}&${usefulCountField}=${usefulCount}&${notUsefulCountField}=${notUsefulCount}`;
78
+ }
79
+
80
+ const logAiResponseFeedback = (feedbackType, ruleId, buttonsDiv, basicHtml) => {
81
+ const isUseful = feedbackType === 'useful';
82
+ const websiteUrl = '<%= urlScanned %>';
83
+ const storagePath = '<%= storagePath %>';
84
+ const finalUrl = formatFeedbackFormUrl(getLocalStorageUUID(), websiteUrl, ruleId, escapeHtmlForAI(basicHtml), isUseful ? 1 : 0, isUseful ? 0 : 1);
85
+ try {
86
+ fetch(finalUrl, {
87
+ mode: 'no-cors'
88
+ });
89
+
90
+ // Store useful value in storage
91
+ let aiUsefulFeedbackLog = localStorage.getItem(storagePath);
92
+ if (aiUsefulFeedbackLog === null) {
93
+ aiUsefulFeedbackLog = {};
94
+ } else {
95
+ aiUsefulFeedbackLog = JSON.parse(aiUsefulFeedbackLog);
96
+ }
97
+ aiUsefulFeedbackLog[buttonsDiv] = isUseful ? 'useful' : 'notUseful';
98
+ localStorage.setItem(storagePath, JSON.stringify(aiUsefulFeedbackLog));
99
+ } catch (err) {
100
+ console.log('Error sending feedback to backend: ', err);
101
+ }
102
+
103
+ document
104
+ .getElementById(buttonsDiv)
105
+ .replaceChildren(
106
+ createElementFromString(
107
+ `<p class="aiFeedbackResponse">You rated this AI suggestion ${isUseful ? 'useful': 'not useful'}. <a class="undoAiFeedback" href="#" onclick="undoAiFeedback(\'${buttonsDiv}\', \'${ruleId}\', \'${escapeHtmlForAI(
108
+ basicHtml,
109
+ )}\')">Undo</a></p>`,
110
+ ),
111
+ );
112
+ };
113
+
114
+ const undoAiFeedback = (buttonDiv, ruleId, basicHtml) => {
115
+ const storagePath = '<%= storagePath %>';
116
+ const websiteUrl = '<%= urlScanned %>';
117
+ var aiUsefulFeedbackLog = JSON.parse(localStorage.getItem(storagePath)) || {};
118
+ const feedbackType = aiUsefulFeedbackLog[buttonDiv];
119
+ const isUseful = feedbackType === 'useful';
120
+ const finalUrl = formatFeedbackFormUrl(getLocalStorageUUID(), websiteUrl, ruleId, escapeHtmlForAI(basicHtml), isUseful ? -1 : 0, isUseful ? 0 : -1);
121
+ try {
122
+ fetch(finalUrl, {
123
+ mode: 'no-cors'
124
+ });
125
+
126
+ // Remove useful value in storage
127
+ delete aiUsefulFeedbackLog[buttonDiv];
128
+ localStorage.setItem(storagePath, JSON.stringify(aiUsefulFeedbackLog));
129
+ } catch (err) {
130
+ console.log('Error undoing AI feedback: ', err);
131
+ }
132
+
133
+ document.getElementById(buttonDiv)
134
+ .innerHTML = `<button class="aiFeedbackButton" onClick="logAiResponseFeedback('useful', '${ruleId}', '${buttonDiv}', '${basicHtml}')">
135
+ <svg
136
+ width="16"
137
+ height="16"
138
+ viewBox="0 0 16 16"
139
+ fill="none"
140
+ xmlns="http://www.w3.org/2000/svg"
141
+ >
142
+ <path
143
+ d="M8.48027 2.03518C7.7505 1.85274 7.07264 2.40464 7.02379 3.11914C6.96883 3.92143 6.84821 4.65807 6.69707 5.09623C6.60165 5.37104 6.33142 5.86951 5.90318 6.34737C5.47799 6.82294 4.92456 7.2466 4.27647 7.4237C3.7635 7.56339 3.2406 8.00767 3.2406 8.65652V11.7107C3.2406 12.3557 3.76121 12.8283 4.34594 12.8901C5.16273 12.9771 5.53982 13.2069 5.92456 13.442L5.9612 13.4649C6.16883 13.5908 6.40242 13.7305 6.70165 13.8344C7.0047 13.9382 7.3589 14 7.82073 14H10.4925C11.2077 14 11.7131 13.6359 11.9688 13.1878C12.0924 12.9763 12.1592 12.7365 12.1627 12.4916C12.1627 12.3756 12.1451 12.2534 12.1039 12.1374C12.2573 11.9367 12.394 11.6962 12.4764 11.4496C12.5604 11.1977 12.6077 10.868 12.4795 10.5725C12.5322 10.4733 12.5711 10.3672 12.6009 10.2649C12.6596 10.0588 12.6871 9.83132 12.6871 9.61071C12.6871 9.39086 12.6596 9.16415 12.6009 8.95728C12.5742 8.86218 12.5389 8.76969 12.4955 8.68094C12.6292 8.49077 12.7152 8.27126 12.7463 8.0409C12.7773 7.81054 12.7526 7.57609 12.6741 7.35729C12.5169 6.90538 12.1535 6.5176 11.7581 6.3863C11.1116 6.17103 10.3818 6.17561 9.83751 6.22523C9.72452 6.2354 9.61177 6.24813 9.49935 6.2634C9.76416 5.12935 9.74791 3.94777 9.45202 2.82143C9.40054 2.64097 9.30179 2.47753 9.16597 2.34802C9.03016 2.21852 8.86221 2.12764 8.6795 2.0848L8.48027 2.03518ZM10.4925 13.2374H7.82073C7.43142 13.2374 7.16195 13.1847 6.9505 13.1122C6.736 13.0382 6.56425 12.9382 6.35814 12.8122L6.32761 12.7939C5.90395 12.5351 5.41311 12.2359 4.42685 12.1313C4.17266 12.1038 4.00396 11.9099 4.00396 11.7115V8.65652C4.00396 8.46263 4.17647 8.24202 4.47724 8.16034C5.31311 7.93133 5.98639 7.40004 6.47264 6.85653C6.95737 6.31455 7.28485 5.73058 7.41768 5.34661C7.60317 4.81226 7.72836 3.997 7.78561 3.17105C7.8047 2.89472 8.06042 2.71762 8.29477 2.77563L8.49477 2.82601C8.61691 2.85655 8.69172 2.93517 8.71462 3.02067C9.02636 4.20847 8.98792 5.46108 8.60393 6.62752C8.5822 6.6924 8.5783 6.76192 8.59265 6.82882C8.607 6.89572 8.63907 6.95753 8.6855 7.00778C8.73193 7.05803 8.79101 7.09488 8.85657 7.11447C8.92212 7.13406 8.99174 7.13566 9.05813 7.11912L9.06042 7.11836L9.0711 7.11607L9.11538 7.10538C9.37641 7.04995 9.64048 7.00993 9.90621 6.98553C10.4123 6.93973 11.0184 6.94431 11.5169 7.11072C11.6505 7.155 11.8604 7.33973 11.952 7.6069C12.0337 7.84202 12.0184 8.11835 11.749 8.38705L11.4795 8.65652L11.749 8.92675C11.7818 8.95957 11.8291 9.03438 11.8665 9.1672C11.9032 9.29468 11.9238 9.44964 11.9238 9.61071C11.9238 9.77254 11.9032 9.92674 11.8665 10.055C11.8283 10.1878 11.7818 10.2626 11.749 10.2954L11.4795 10.5649L11.749 10.8351C11.7848 10.871 11.8322 10.9702 11.7528 11.2076C11.67 11.4379 11.5385 11.6475 11.3673 11.8221L11.0978 12.0916L11.3673 12.3618C11.3719 12.3657 11.3986 12.4 11.3986 12.4916C11.3952 12.6036 11.3634 12.7129 11.3062 12.8092C11.1803 13.029 10.9222 13.2366 10.4925 13.2366V13.2374Z"
144
+ fill="#9021a6"
145
+ />
146
+ </svg>
147
+ Useful</button
148
+ > <button class="aiFeedbackButton" style="margin-left:16px" onClick="logAiResponseFeedback('notUseful', '${ruleId}', '${buttonDiv}', '${basicHtml}')">
149
+ <svg
150
+ width="16"
151
+ height="16"
152
+ viewBox="0 0 16 16"
153
+ fill="none"
154
+ xmlns="http://www.w3.org/2000/svg"
155
+ >
156
+ <path
157
+ d="M8.48066 13.9646C7.7509 14.1478 7.07382 13.5951 7.0242 12.8807C6.96924 12.0791 6.84864 11.3425 6.69749 10.9036C6.60208 10.6288 6.33185 10.1311 5.90362 9.65325C5.47844 9.17693 4.92502 8.75327 4.27694 8.57694C3.76398 8.43648 3.24109 7.99222 3.24109 7.34338V4.29002C3.24109 3.645 3.76169 3.17249 4.34641 3.1099C5.16318 3.02364 5.54027 2.79311 5.92499 2.558L5.96163 2.53586C6.16926 2.40915 6.40284 2.26946 6.70207 2.16641C7.00512 2.06107 7.35931 2 7.82113 2H10.4928C11.2081 2 11.7134 2.36488 11.9691 2.81219C12.0943 3.03127 12.163 3.27554 12.163 3.50912C12.163 3.62515 12.1455 3.74729 12.1042 3.86331C12.2577 4.06331 12.3943 4.30376 12.4767 4.55032C12.5607 4.80222 12.608 5.13199 12.4798 5.42816C12.5325 5.5274 12.5714 5.63274 12.6012 5.73579C12.6599 5.94189 12.6874 6.1686 12.6874 6.38921C12.6874 6.60981 12.6599 6.83652 12.6012 7.04263C12.5745 7.13423 12.5401 7.22888 12.4958 7.31972C12.7966 7.75559 12.8149 8.23573 12.6744 8.64259C12.5172 9.09448 12.1539 9.48226 11.7584 9.61356C11.1119 9.82958 10.3821 9.82424 9.83788 9.77462C9.72488 9.76444 9.61214 9.75172 9.49972 9.73645C9.76468 10.8707 9.74843 12.0526 9.45239 13.1791C9.34705 13.5669 9.03255 13.8264 8.67989 13.915L8.48066 13.9646ZM10.4928 2.76334H7.82113C7.43183 2.76334 7.16237 2.81525 6.95092 2.88776C6.73642 2.96181 6.56467 3.06257 6.35857 3.18776L6.32804 3.20684C5.90438 3.46485 5.41356 3.76408 4.42732 3.86942C4.17313 3.89614 4.00443 4.09079 4.00443 4.28926V7.34338C4.00443 7.53803 4.17694 7.75788 4.4777 7.83955C5.31356 8.06856 5.98682 8.6006 6.47307 9.1441C6.95779 9.68607 7.28527 10.27 7.41809 10.6532C7.60358 11.1876 7.72877 12.0028 7.78602 12.8287C7.8051 13.1051 8.06082 13.2829 8.29517 13.2242L8.49516 13.1745C8.6173 13.144 8.6921 13.0646 8.715 12.9799C9.02689 11.7919 8.98845 10.539 8.60432 9.37234C8.58276 9.30751 8.57901 9.23808 8.59344 9.17131C8.60787 9.10453 8.63996 9.04285 8.68636 8.9927C8.73276 8.94256 8.79177 8.90579 8.85723 8.88623C8.92269 8.86667 8.9922 8.86504 9.05851 8.88151H9.0608L9.07148 8.88457L9.11576 8.89449C9.37678 8.94991 9.64085 8.98993 9.90658 9.01433C10.4127 9.06013 11.0188 9.05555 11.5172 8.88991C11.6508 8.84487 11.8607 8.66014 11.9523 8.39297C12.034 8.15787 12.0187 7.88154 11.7493 7.6136L11.4798 7.34338L11.7493 7.07316C11.7821 7.0411 11.8294 6.96629 11.8668 6.83271C11.9035 6.70523 11.9241 6.55027 11.9241 6.38921C11.9241 6.22814 11.9035 6.07318 11.8668 5.94571C11.8287 5.81288 11.7821 5.73731 11.7493 5.70525L11.4798 5.43503L11.7493 5.16481C11.7852 5.12893 11.8325 5.03046 11.7531 4.7923C11.6703 4.56236 11.5388 4.35302 11.3676 4.17857L11.0982 3.90835L11.3676 3.63813C11.3722 3.63431 11.3989 3.59996 11.3989 3.50836C11.3954 3.39665 11.3636 3.28766 11.3065 3.19157C11.1798 2.97097 10.9226 2.76334 10.4928 2.76334Z"
158
+ fill="#9021a6"
159
+ />
160
+ </svg>
161
+ Not useful
162
+ </button>`;
163
+ };
164
+
165
+ const memoizeApiCall = () => {
166
+ // cache
167
+ let ongoingPromise = {};
168
+ let catalogCache = {};
169
+ let requestedDateTime = {};
170
+
171
+ return async (apiUrl) => {
172
+ const key = JSON.stringify(apiUrl);
173
+ const currDateTime = new Date();
174
+ const diffInMinutes = (currDateTime - (requestedDateTime[key] || 0)) / (1000 * 60);
175
+
176
+ // do not refresh api call if previous call made <= 10mins ago
177
+ if (requestedDateTime[key] && diffInMinutes <= 10) {
178
+ // check if the api request has already been made
179
+ if (ongoingPromise[key]) {
180
+ return ongoingPromise[key];
181
+ }
182
+
183
+ // check if the data is already in the cache
184
+ if (catalogCache[key]) {
185
+ return Promise.resolve(catalogCache[key]);
186
+ }
187
+ }
188
+
189
+ requestedDateTime[key] = currDateTime;
190
+ // if not in the cache, make the API call and store the result
191
+ const apiPromise = fetch(apiUrl)
192
+ .then((response) => {
193
+ if (!response.ok) {
194
+ throw new Error('response status not ok');
195
+ }
196
+
197
+ return response.json()
198
+ })
199
+ .then((data) => {
200
+ catalogCache[key] = data; // store the result in the cache
201
+ delete ongoingPromise[key]; // remove the ongoing promise
202
+
203
+ return data;
204
+ })
205
+ .catch((error) => {
206
+ delete ongoingPromise[key]; // remove the promise from the queue in case of an error
207
+ throw new Error('Network Error');
208
+ });
209
+
210
+ // add the promise to the queue
211
+ ongoingPromise[key] = apiPromise;
212
+
213
+ return apiPromise;
214
+ }
215
+ };
216
+
217
+ const api = memoizeApiCall();
218
+
219
+ const apiUrls = {
220
+ catalog: 'https://govtechsg.github.io/purple-ai/catalog.json',
221
+ getRuleIdData: (ruleId) => `https://govtechsg.github.io/purple-ai/results/${ruleId}.json`
222
+ }
223
+
224
+ const isOffline = () => !window.navigator.onLine;
225
+
226
+ const checkPurpleAiQueryLabel = async (ruleId, ruleHtml) => {
227
+ const oobeeAiQueryLabel = {
228
+ label: null,
229
+ hasNetworkError: false,
230
+ hasGenericError: false
231
+ }
232
+
233
+ return api(apiUrls.catalog).then(catalogData => {
234
+ // no information for current rule
235
+ if (!catalogData[ruleId] || catalogData[ruleId].length === 0) {
236
+ return oobeeAiQueryLabel;
237
+ }
238
+
239
+ if (rulesUsingRoles.includes(ruleId)) {
240
+ const ariaValidAttrValueHtml = ruleHtml.replace(/<|>/g, '');
241
+ const ariaValidAttrValueHtmlList = ariaValidAttrValueHtml.split(' ');
242
+ const htmlElement = ariaValidAttrValueHtmlList[0];
243
+ const roleForHtml = ariaValidAttrValueHtmlList.find(item => /^role="\w+"/.test(item));
244
+ if (roleForHtml) {
245
+ const currentLabel = `${htmlElement}_${roleForHtml}`.trim();
246
+ const foundLabel = catalogData[ruleId].find(label => label === currentLabel);
247
+ oobeeAiQueryLabel.label = foundLabel ? escapeHtmlForAI(currentLabel) : null;
248
+ return oobeeAiQueryLabel;
249
+ }
250
+ }
251
+
252
+ // e.g. li_aria-controls_aria-selected_role
253
+ const currentLabelList = getHtmlTagAndAttributes(oobeeAiHtmlETL(ruleHtml));
254
+ const currentLabel = currentLabelList.join('_');
255
+
256
+ if (catalogData[ruleId].includes(currentLabel)) {
257
+ oobeeAiQueryLabel.label = escapeHtmlForAI(currentLabel);
258
+ return oobeeAiQueryLabel;
259
+ }
260
+
261
+ // count the number of elements in keyArr that
262
+ // have matching elements at the same index in currentLabelList
263
+ // return match if >= 3 elements matching
264
+ const currentLabelSet = new Set(currentLabelList);
265
+ const foundLabel = catalogData[ruleId].find(label => {
266
+ const keyArr = label.split('_');
267
+ const attrMatch = keyArr.filter(key => currentLabelSet.has(key));
268
+
269
+ return attrMatch.length >= 3;
270
+ })
271
+
272
+ oobeeAiQueryLabel.label = foundLabel ? escapeHtmlForAI(foundLabel) : null;
273
+ return oobeeAiQueryLabel;
274
+ })
275
+ .catch(err => {
276
+ console.error(`An error has occurred while checking if ${ruleId} needs AI query`);
277
+ if (err.message === 'Network Error') {
278
+ return {
279
+ label: null,
280
+ hasNetworkError: true,
281
+ hasGenericError: false
282
+ }
283
+ } else {
284
+ return {
285
+ label: null,
286
+ hasNetworkError: false,
287
+ hasGenericError: true
288
+ }
289
+ }
290
+ });
291
+ }
292
+
293
+ const handleOfflinePurpleAi = async (ruleId, accordionDiv, html, buttonsDiv, aiErrorDiv) => {
294
+ let oobeeAiQueryLabel = await checkPurpleAiQueryLabel(ruleId, html);
295
+ if (oobeeAiQueryLabel.hasNetworkError) {
296
+ document
297
+ .getElementById(aiErrorDiv)
298
+ .replaceChildren(
299
+ createElementFromString(
300
+ `<div class="generateAiError">This feature requires internet connection. Please try again</div>`,
301
+ ),
302
+ );
303
+ } else if (oobeeAiQueryLabel.hasNetworkError) {
304
+ document
305
+ .getElementById(aiErrorDiv)
306
+ .replaceChildren(
307
+ createElementFromString(
308
+ `<div class="generateAiError">Something went wrong. Please try again</div>`,
309
+ ),
310
+ );
311
+ } else if (!oobeeAiQueryLabel.label) {
312
+ document.getElementById(accordionDiv).innerHTML = `<span class="processAI">Processing AI suggestions, please check back later.</span>`
313
+ } else {
314
+ await getPurpleAiAnswer(ruleId, accordionDiv, oobeeAiQueryLabel.label, buttonsDiv, aiErrorDiv);
315
+ }
316
+ }
317
+
318
+ const getPurpleAiAnswer = async (ruleId, accordionDiv, ruleHtmlLabel, buttonsDiv, aiErrorDiv, html) => {
319
+ const storagePath = '<%= storagePath %>';
320
+ document.getElementById(buttonsDiv).disabled = true;
321
+ document.getElementById(buttonsDiv).textContent = 'Generating...';
322
+
323
+ api(apiUrls.getRuleIdData(ruleId))
324
+ .then(ruleIdData => {
325
+ const escapedHtml = escapeHtmlForAI(ruleIdData[ruleHtmlLabel]);
326
+ const replacedString = escapedHtml.replaceAll(
327
+ /```(?:html)?([\s\S]*?)```/g,
328
+ `<code class="codeForAiResponse language-html hljs">$1</code>`,
329
+ );
330
+ const replacedRuleHtmlLabel = escapeHtmlForAI(ruleHtmlLabel);
331
+ document.getElementById(accordionDiv).innerHTML = `
332
+ <div class="ai-response-card">
333
+ <p class="mb-0">
334
+ ${replacedString.replace(/\n/g, '<br />')}
335
+ </p>
336
+ </div>`
337
+
338
+ if (!isOffline()) {
339
+ const aiVoteFeedback = JSON.parse(localStorage.getItem(storagePath));
340
+ if (aiVoteFeedback && aiVoteFeedback[buttonsDiv]) {
341
+ var voteString = aiVoteFeedback[buttonsDiv] === 'useful' ?
342
+ `You rated this AI suggestion useful. ` :
343
+ `You rated this AI suggestion not useful. `;
344
+ const votedElem = createElementFromString(`
345
+ <div id=${buttonsDiv} style="display: flex;justify-content: flex-end;margin-top: 16px;">
346
+ <p class="aiFeedbackResponse">${voteString}<a class="undoAiFeedback" href="#" onclick="undoAiFeedback(\'${buttonsDiv}\', \'${ruleId}\', \'${escapeHtmlForAI(
347
+ ruleHtmlLabel,
348
+ )}\')">Undo</a>
349
+ </p>
350
+ </div>
351
+ `)
352
+ document.getElementById(accordionDiv).getElementsByClassName('ai-response-card')[0].appendChild(votedElem);
353
+ } else {
354
+ const feedbackButtonsElem = createElementFromString( `
355
+ <div id=${buttonsDiv} class="feedbackButtons">
356
+ <button class="aiFeedbackButton" onClick="logAiResponseFeedback('useful', '${ruleId}', '${buttonsDiv}', '${replacedRuleHtmlLabel}')">
357
+ <svg
358
+ width="16"
359
+ height="16"
360
+ viewBox="0 0 16 16"
361
+ fill="none"
362
+ xmlns="http://www.w3.org/2000/svg"
363
+ >
364
+ <path
365
+ d="M8.48027 2.03518C7.7505 1.85274 7.07264 2.40464 7.02379 3.11914C6.96883 3.92143 6.84821 4.65807 6.69707 5.09623C6.60165 5.37104 6.33142 5.86951 5.90318 6.34737C5.47799 6.82294 4.92456 7.2466 4.27647 7.4237C3.7635 7.56339 3.2406 8.00767 3.2406 8.65652V11.7107C3.2406 12.3557 3.76121 12.8283 4.34594 12.8901C5.16273 12.9771 5.53982 13.2069 5.92456 13.442L5.9612 13.4649C6.16883 13.5908 6.40242 13.7305 6.70165 13.8344C7.0047 13.9382 7.3589 14 7.82073 14H10.4925C11.2077 14 11.7131 13.6359 11.9688 13.1878C12.0924 12.9763 12.1592 12.7365 12.1627 12.4916C12.1627 12.3756 12.1451 12.2534 12.1039 12.1374C12.2573 11.9367 12.394 11.6962 12.4764 11.4496C12.5604 11.1977 12.6077 10.868 12.4795 10.5725C12.5322 10.4733 12.5711 10.3672 12.6009 10.2649C12.6596 10.0588 12.6871 9.83132 12.6871 9.61071C12.6871 9.39086 12.6596 9.16415 12.6009 8.95728C12.5742 8.86218 12.5389 8.76969 12.4955 8.68094C12.6292 8.49077 12.7152 8.27126 12.7463 8.0409C12.7773 7.81054 12.7526 7.57609 12.6741 7.35729C12.5169 6.90538 12.1535 6.5176 11.7581 6.3863C11.1116 6.17103 10.3818 6.17561 9.83751 6.22523C9.72452 6.2354 9.61177 6.24813 9.49935 6.2634C9.76416 5.12935 9.74791 3.94777 9.45202 2.82143C9.40054 2.64097 9.30179 2.47753 9.16597 2.34802C9.03016 2.21852 8.86221 2.12764 8.6795 2.0848L8.48027 2.03518ZM10.4925 13.2374H7.82073C7.43142 13.2374 7.16195 13.1847 6.9505 13.1122C6.736 13.0382 6.56425 12.9382 6.35814 12.8122L6.32761 12.7939C5.90395 12.5351 5.41311 12.2359 4.42685 12.1313C4.17266 12.1038 4.00396 11.9099 4.00396 11.7115V8.65652C4.00396 8.46263 4.17647 8.24202 4.47724 8.16034C5.31311 7.93133 5.98639 7.40004 6.47264 6.85653C6.95737 6.31455 7.28485 5.73058 7.41768 5.34661C7.60317 4.81226 7.72836 3.997 7.78561 3.17105C7.8047 2.89472 8.06042 2.71762 8.29477 2.77563L8.49477 2.82601C8.61691 2.85655 8.69172 2.93517 8.71462 3.02067C9.02636 4.20847 8.98792 5.46108 8.60393 6.62752C8.5822 6.6924 8.5783 6.76192 8.59265 6.82882C8.607 6.89572 8.63907 6.95753 8.6855 7.00778C8.73193 7.05803 8.79101 7.09488 8.85657 7.11447C8.92212 7.13406 8.99174 7.13566 9.05813 7.11912L9.06042 7.11836L9.0711 7.11607L9.11538 7.10538C9.37641 7.04995 9.64048 7.00993 9.90621 6.98553C10.4123 6.93973 11.0184 6.94431 11.5169 7.11072C11.6505 7.155 11.8604 7.33973 11.952 7.6069C12.0337 7.84202 12.0184 8.11835 11.749 8.38705L11.4795 8.65652L11.749 8.92675C11.7818 8.95957 11.8291 9.03438 11.8665 9.1672C11.9032 9.29468 11.9238 9.44964 11.9238 9.61071C11.9238 9.77254 11.9032 9.92674 11.8665 10.055C11.8283 10.1878 11.7818 10.2626 11.749 10.2954L11.4795 10.5649L11.749 10.8351C11.7848 10.871 11.8322 10.9702 11.7528 11.2076C11.67 11.4379 11.5385 11.6475 11.3673 11.8221L11.0978 12.0916L11.3673 12.3618C11.3719 12.3657 11.3986 12.4 11.3986 12.4916C11.3952 12.6036 11.3634 12.7129 11.3062 12.8092C11.1803 13.029 10.9222 13.2366 10.4925 13.2366V13.2374Z"
366
+ fill="#9021a6"
367
+ />
368
+ </svg>
369
+ Useful
370
+ </button>
371
+ <button class="aiFeedbackButton" style="margin-left:16px" onClick="logAiResponseFeedback('notUseful', '${ruleId}', '${buttonsDiv}', '${replacedRuleHtmlLabel}')">
372
+ <svg
373
+ width="16"
374
+ height="16"
375
+ viewBox="0 0 16 16"
376
+ fill="none"
377
+ xmlns="http://www.w3.org/2000/svg"
378
+ >
379
+ <path
380
+ d="M8.48066 13.9646C7.7509 14.1478 7.07382 13.5951 7.0242 12.8807C6.96924 12.0791 6.84864 11.3425 6.69749 10.9036C6.60208 10.6288 6.33185 10.1311 5.90362 9.65325C5.47844 9.17693 4.92502 8.75327 4.27694 8.57694C3.76398 8.43648 3.24109 7.99222 3.24109 7.34338V4.29002C3.24109 3.645 3.76169 3.17249 4.34641 3.1099C5.16318 3.02364 5.54027 2.79311 5.92499 2.558L5.96163 2.53586C6.16926 2.40915 6.40284 2.26946 6.70207 2.16641C7.00512 2.06107 7.35931 2 7.82113 2H10.4928C11.2081 2 11.7134 2.36488 11.9691 2.81219C12.0943 3.03127 12.163 3.27554 12.163 3.50912C12.163 3.62515 12.1455 3.74729 12.1042 3.86331C12.2577 4.06331 12.3943 4.30376 12.4767 4.55032C12.5607 4.80222 12.608 5.13199 12.4798 5.42816C12.5325 5.5274 12.5714 5.63274 12.6012 5.73579C12.6599 5.94189 12.6874 6.1686 12.6874 6.38921C12.6874 6.60981 12.6599 6.83652 12.6012 7.04263C12.5745 7.13423 12.5401 7.22888 12.4958 7.31972C12.7966 7.75559 12.8149 8.23573 12.6744 8.64259C12.5172 9.09448 12.1539 9.48226 11.7584 9.61356C11.1119 9.82958 10.3821 9.82424 9.83788 9.77462C9.72488 9.76444 9.61214 9.75172 9.49972 9.73645C9.76468 10.8707 9.74843 12.0526 9.45239 13.1791C9.34705 13.5669 9.03255 13.8264 8.67989 13.915L8.48066 13.9646ZM10.4928 2.76334H7.82113C7.43183 2.76334 7.16237 2.81525 6.95092 2.88776C6.73642 2.96181 6.56467 3.06257 6.35857 3.18776L6.32804 3.20684C5.90438 3.46485 5.41356 3.76408 4.42732 3.86942C4.17313 3.89614 4.00443 4.09079 4.00443 4.28926V7.34338C4.00443 7.53803 4.17694 7.75788 4.4777 7.83955C5.31356 8.06856 5.98682 8.6006 6.47307 9.1441C6.95779 9.68607 7.28527 10.27 7.41809 10.6532C7.60358 11.1876 7.72877 12.0028 7.78602 12.8287C7.8051 13.1051 8.06082 13.2829 8.29517 13.2242L8.49516 13.1745C8.6173 13.144 8.6921 13.0646 8.715 12.9799C9.02689 11.7919 8.98845 10.539 8.60432 9.37234C8.58276 9.30751 8.57901 9.23808 8.59344 9.17131C8.60787 9.10453 8.63996 9.04285 8.68636 8.9927C8.73276 8.94256 8.79177 8.90579 8.85723 8.88623C8.92269 8.86667 8.9922 8.86504 9.05851 8.88151H9.0608L9.07148 8.88457L9.11576 8.89449C9.37678 8.94991 9.64085 8.98993 9.90658 9.01433C10.4127 9.06013 11.0188 9.05555 11.5172 8.88991C11.6508 8.84487 11.8607 8.66014 11.9523 8.39297C12.034 8.15787 12.0187 7.88154 11.7493 7.6136L11.4798 7.34338L11.7493 7.07316C11.7821 7.0411 11.8294 6.96629 11.8668 6.83271C11.9035 6.70523 11.9241 6.55027 11.9241 6.38921C11.9241 6.22814 11.9035 6.07318 11.8668 5.94571C11.8287 5.81288 11.7821 5.73731 11.7493 5.70525L11.4798 5.43503L11.7493 5.16481C11.7852 5.12893 11.8325 5.03046 11.7531 4.7923C11.6703 4.56236 11.5388 4.35302 11.3676 4.17857L11.0982 3.90835L11.3676 3.63813C11.3722 3.63431 11.3989 3.59996 11.3989 3.50836C11.3954 3.39665 11.3636 3.28766 11.3065 3.19157C11.1798 2.97097 10.9226 2.76334 10.4928 2.76334Z"
381
+ fill="#9021a6"
382
+ />
383
+ </svg>
384
+ Not useful
385
+ </button>
386
+ </div>
387
+ `);
388
+ document.getElementById(accordionDiv).getElementsByClassName('ai-response-card')[0].appendChild(feedbackButtonsElem);
389
+ }
390
+ }
391
+
392
+ document.querySelectorAll('.codeForAiResponse').forEach(el => {
393
+ hljs.highlightElement(el);
394
+ });
395
+ }).catch((err) => {
396
+ console.log('Error fetching AI response', err);
397
+ document.getElementById(buttonsDiv).disabled = false;
398
+ document.getElementById(buttonsDiv).style = `border: 1px solid #6E52EF;color: #6E52EF;`;
399
+ document.getElementById(buttonsDiv).textContent = 'Generate response';
400
+ const errorMessageToDisplay = err.message === 'Network Error'
401
+ ? 'This feature requires internet connection. Please try again'
402
+ : 'Something went wrong. Please try again.'
403
+ document
404
+ .getElementById(aiErrorDiv)
405
+ .replaceChildren(
406
+ createElementFromString(
407
+ `<div class="generateAiError">${errorMessageToDisplay}</div>`,
408
+ ),
409
+ );
410
+ });
411
+ }
412
+
413
+ function getFormattedCategoryTitle(category) {
414
+ const titles = {
415
+ mustFix: 'Must Fix',
416
+ goodToFix: 'Good to Fix',
417
+ needsReview: 'Needs Review',
418
+ passed: 'Passed',
419
+ };
420
+
421
+ return titles[category];
422
+ }
423
+
424
+ function escapeHtmlStringForArg(string) {
425
+ return htmlEscapeString(string)
426
+ .replaceAll('&#039;', "\\'")
427
+ .replace(/&gt;\s+(.*?)\s+&lt;\//g, '&gt;$1&lt;/');
428
+ }
429
+
430
+ function htmlEscapeString(string) {
431
+ if (string.includes('&lt;/script>')) {
432
+ string = string.replaceAll('&lt;/script>', '<\/script>')
433
+ }
434
+ return string
435
+ .replace(/&/g, '&amp;')
436
+ .replace(/</g, '&lt;')
437
+ .replace(/>/g, '&gt;')
438
+ .replace(/"/g, '&quot;')
439
+ .replace(/'/g, '&#039;');
440
+ }
441
+ </script>