@govtechsg/oobee 0.10.76 → 0.10.77

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 (134) hide show
  1. package/.github/workflows/publish.yml +8 -1
  2. package/INTEGRATION.md +7 -3
  3. package/dist/cli.js +252 -0
  4. package/dist/combine.js +221 -0
  5. package/dist/constants/cliFunctions.js +306 -0
  6. package/dist/constants/common.js +1669 -0
  7. package/dist/constants/constants.js +913 -0
  8. package/dist/constants/errorMeta.json +319 -0
  9. package/dist/constants/itemTypeDescription.js +7 -0
  10. package/dist/constants/oobeeAi.js +121 -0
  11. package/dist/constants/questions.js +151 -0
  12. package/dist/constants/sampleData.js +176 -0
  13. package/dist/crawlers/commonCrawlerFunc.js +428 -0
  14. package/dist/crawlers/crawlDomain.js +613 -0
  15. package/dist/crawlers/crawlIntelligentSitemap.js +135 -0
  16. package/dist/crawlers/crawlLocalFile.js +151 -0
  17. package/dist/crawlers/crawlSitemap.js +303 -0
  18. package/dist/crawlers/custom/escapeCssSelector.js +10 -0
  19. package/dist/crawlers/custom/evaluateAltText.js +11 -0
  20. package/dist/crawlers/custom/extractAndGradeText.js +44 -0
  21. package/dist/crawlers/custom/extractText.js +27 -0
  22. package/dist/crawlers/custom/findElementByCssSelector.js +36 -0
  23. package/dist/crawlers/custom/flagUnlabelledClickableElements.js +963 -0
  24. package/dist/crawlers/custom/framesCheck.js +37 -0
  25. package/dist/crawlers/custom/getAxeConfiguration.js +111 -0
  26. package/dist/crawlers/custom/gradeReadability.js +23 -0
  27. package/dist/crawlers/custom/utils.js +1024 -0
  28. package/dist/crawlers/custom/xPathToCss.js +147 -0
  29. package/dist/crawlers/guards/urlGuard.js +71 -0
  30. package/dist/crawlers/pdfScanFunc.js +276 -0
  31. package/dist/crawlers/runCustom.js +89 -0
  32. package/dist/exclusions.txt +7 -0
  33. package/dist/generateHtmlReport.js +144 -0
  34. package/dist/index.js +62 -0
  35. package/dist/logs.js +84 -0
  36. package/dist/mergeAxeResults.js +1571 -0
  37. package/dist/npmIndex.js +429 -0
  38. package/dist/proxyService.js +360 -0
  39. package/dist/runGenerateJustHtmlReport.js +16 -0
  40. package/dist/screenshotFunc/htmlScreenshotFunc.js +355 -0
  41. package/dist/screenshotFunc/pdfScreenshotFunc.js +645 -0
  42. package/dist/services/s3Uploader.js +127 -0
  43. package/dist/static/ejs/partials/components/allIssues/AllIssues.ejs +9 -0
  44. package/dist/static/ejs/partials/components/allIssues/CategoryBadges.ejs +82 -0
  45. package/dist/static/ejs/partials/components/allIssues/FilterBar.ejs +33 -0
  46. package/dist/static/ejs/partials/components/allIssues/IssuesTable.ejs +41 -0
  47. package/dist/static/ejs/partials/components/header/SiteInfo.ejs +119 -0
  48. package/dist/static/ejs/partials/components/header/aboutScanModal/AboutScanModal.ejs +15 -0
  49. package/dist/static/ejs/partials/components/header/aboutScanModal/ScanConfiguration.ejs +44 -0
  50. package/dist/static/ejs/partials/components/header/aboutScanModal/ScanDetails.ejs +142 -0
  51. package/dist/static/ejs/partials/components/prioritiseIssues/IssueDetailCard.ejs +36 -0
  52. package/dist/static/ejs/partials/components/prioritiseIssues/PrioritiseIssues.ejs +47 -0
  53. package/dist/static/ejs/partials/components/ruleModal/ruleOffcanvas.ejs +196 -0
  54. package/dist/static/ejs/partials/components/scannedPagesSegmentedTabs.ejs +48 -0
  55. package/dist/static/ejs/partials/components/screenshotLightbox.ejs +13 -0
  56. package/dist/static/ejs/partials/components/shared/InfoAlert.ejs +3 -0
  57. package/dist/static/ejs/partials/components/summaryScanAbout.ejs +141 -0
  58. package/dist/static/ejs/partials/components/summaryScanResults.ejs +16 -0
  59. package/dist/static/ejs/partials/components/summaryTable.ejs +20 -0
  60. package/dist/static/ejs/partials/components/summaryWcagCompliance.ejs +94 -0
  61. package/dist/static/ejs/partials/components/topTen.ejs +6 -0
  62. package/dist/static/ejs/partials/components/wcagCompliance/FailedCriteria.ejs +47 -0
  63. package/dist/static/ejs/partials/components/wcagCompliance/WcagCompliance.ejs +16 -0
  64. package/dist/static/ejs/partials/components/wcagCompliance/WcagGaugeBar.ejs +16 -0
  65. package/dist/static/ejs/partials/components/wcagCoverageDetails.ejs +18 -0
  66. package/dist/static/ejs/partials/footer.ejs +24 -0
  67. package/dist/static/ejs/partials/header.ejs +14 -0
  68. package/dist/static/ejs/partials/main.ejs +29 -0
  69. package/dist/static/ejs/partials/scripts/allIssues/AllIssues.ejs +376 -0
  70. package/dist/static/ejs/partials/scripts/bootstrap.ejs +8 -0
  71. package/dist/static/ejs/partials/scripts/categorySummary.ejs +141 -0
  72. package/dist/static/ejs/partials/scripts/decodeUnzipParse.ejs +3 -0
  73. package/dist/static/ejs/partials/scripts/header/SiteInfo.ejs +44 -0
  74. package/dist/static/ejs/partials/scripts/header/aboutScanModal/AboutScanModal.ejs +51 -0
  75. package/dist/static/ejs/partials/scripts/header/aboutScanModal/ScanConfiguration.ejs +127 -0
  76. package/dist/static/ejs/partials/scripts/header/aboutScanModal/ScanDetails.ejs +60 -0
  77. package/dist/static/ejs/partials/scripts/highlightjs.ejs +335 -0
  78. package/dist/static/ejs/partials/scripts/popper.ejs +7 -0
  79. package/dist/static/ejs/partials/scripts/prioritiseIssues/IssueDetailCard.ejs +137 -0
  80. package/dist/static/ejs/partials/scripts/prioritiseIssues/PrioritiseIssues.ejs +214 -0
  81. package/dist/static/ejs/partials/scripts/prioritiseIssues/wcagSvgMap.ejs +861 -0
  82. package/dist/static/ejs/partials/scripts/ruleModal/constants.ejs +957 -0
  83. package/dist/static/ejs/partials/scripts/ruleModal/itemCardRenderer.ejs +353 -0
  84. package/dist/static/ejs/partials/scripts/ruleModal/pageAccordionBuilder.ejs +468 -0
  85. package/dist/static/ejs/partials/scripts/ruleModal/ruleOffcanvas.ejs +306 -0
  86. package/dist/static/ejs/partials/scripts/ruleModal/utilities.ejs +483 -0
  87. package/dist/static/ejs/partials/scripts/scannedPagesSegmentedTabs.ejs +35 -0
  88. package/dist/static/ejs/partials/scripts/screenshotLightbox.ejs +75 -0
  89. package/dist/static/ejs/partials/scripts/summaryScanResults.ejs +14 -0
  90. package/dist/static/ejs/partials/scripts/summaryTable.ejs +78 -0
  91. package/dist/static/ejs/partials/scripts/topTen.ejs +61 -0
  92. package/dist/static/ejs/partials/scripts/utils.ejs +453 -0
  93. package/dist/static/ejs/partials/scripts/wcagCompliance/FailedCriteria.ejs +103 -0
  94. package/dist/static/ejs/partials/scripts/wcagCompliance/WcagGaugeBar.ejs +47 -0
  95. package/dist/static/ejs/partials/scripts/wcagCompliance.ejs +15 -0
  96. package/dist/static/ejs/partials/scripts/wcagCoverageDetails.ejs +75 -0
  97. package/dist/static/ejs/partials/styles/allIssues/AllIssues.ejs +384 -0
  98. package/dist/static/ejs/partials/styles/bootstrap.ejs +12391 -0
  99. package/dist/static/ejs/partials/styles/header/SiteInfo.ejs +121 -0
  100. package/dist/static/ejs/partials/styles/header/aboutScanModal/AboutScanModal.ejs +82 -0
  101. package/dist/static/ejs/partials/styles/header/aboutScanModal/ScanConfiguration.ejs +50 -0
  102. package/dist/static/ejs/partials/styles/header/aboutScanModal/ScanDetails.ejs +149 -0
  103. package/dist/static/ejs/partials/styles/header.ejs +7 -0
  104. package/dist/static/ejs/partials/styles/highlightjs.ejs +54 -0
  105. package/dist/static/ejs/partials/styles/prioritiseIssues/IssueDetailCard.ejs +141 -0
  106. package/dist/static/ejs/partials/styles/prioritiseIssues/PrioritiseIssues.ejs +204 -0
  107. package/dist/static/ejs/partials/styles/ruleModal/ruleOffcanvas.ejs +456 -0
  108. package/dist/static/ejs/partials/styles/scannedPagesSegmentedTabs.ejs +46 -0
  109. package/dist/static/ejs/partials/styles/shared/InfoAlert.ejs +12 -0
  110. package/dist/static/ejs/partials/styles/styles.ejs +1607 -0
  111. package/dist/static/ejs/partials/styles/summaryBootstrap.ejs +12458 -0
  112. package/dist/static/ejs/partials/styles/topTenCard.ejs +44 -0
  113. package/dist/static/ejs/partials/styles/wcagCompliance/FailedCriteria.ejs +59 -0
  114. package/dist/static/ejs/partials/styles/wcagCompliance/WcagGaugeBar.ejs +62 -0
  115. package/dist/static/ejs/partials/styles/wcagCompliance.ejs +36 -0
  116. package/dist/static/ejs/partials/styles/wcagCoverageDetails.ejs +33 -0
  117. package/dist/static/ejs/partials/summaryHeader.ejs +70 -0
  118. package/dist/static/ejs/partials/summaryMain.ejs +49 -0
  119. package/dist/static/ejs/report.ejs +226 -0
  120. package/dist/static/ejs/summary.ejs +47 -0
  121. package/dist/types/types.js +1 -0
  122. package/dist/utils.js +1070 -0
  123. package/examples/oobee-cypress-integration-js/cypress/support/e2e.js +36 -6
  124. package/examples/oobee-cypress-integration-js/cypress.config.js +45 -1
  125. package/examples/oobee-cypress-integration-ts/cypress.config.ts +47 -1
  126. package/examples/oobee-cypress-integration-ts/src/cypress/support/e2e.ts +36 -6
  127. package/examples/oobee-playwright-integration-js/oobee-playwright-demo.js +2 -1
  128. package/examples/oobee-playwright-integration-ts/src/oobee-playwright-demo.ts +2 -1
  129. package/package.json +9 -3
  130. package/src/constants/common.ts +2 -2
  131. package/src/constants/constants.ts +3 -1
  132. package/src/crawlers/crawlDomain.ts +1 -0
  133. package/src/crawlers/runCustom.ts +0 -1
  134. package/src/npmIndex.ts +42 -24
@@ -0,0 +1,468 @@
1
+ <script>
2
+ function buildExpandedRuleCategoryContent(category, ruleInCategory) {
3
+ const contentContainer = document.getElementById('expandedRuleCategoryContent');
4
+ const isCustomFlow = <%- isCustomFlow -%>;
5
+
6
+ if (category === 'passed') {
7
+ contentContainer.innerHTML = `You may find the list of passed HTML elements in <a href='./passed_items.json' target='_blank'>passed_items.json.txt</a>.`;
8
+ return;
9
+ }
10
+
11
+ setupGroupByToggle(category, ruleInCategory, isCustomFlow);
12
+
13
+ renderGroupedByPage(category, ruleInCategory, isCustomFlow);
14
+ }
15
+
16
+ function setupGroupByToggle(category, ruleInCategory, isCustomFlow) {
17
+ const groupByPage = document.getElementById('groupByPage');
18
+ const groupByHtmlElement = document.getElementById('groupByHtmlElement');
19
+
20
+ const newGroupByPage = groupByPage.cloneNode(true);
21
+ const newGroupByHtmlElement = groupByHtmlElement.cloneNode(true);
22
+ groupByPage.parentNode.replaceChild(newGroupByPage, groupByPage);
23
+ groupByHtmlElement.parentNode.replaceChild(newGroupByHtmlElement, groupByHtmlElement);
24
+
25
+ const updateDropdownTitle = (isHtmlGrouping) => {
26
+ const itemsToUse = typeof filteredItems !== 'undefined' ? filteredItems : scanItems;
27
+ const dropdownTitle = document.getElementById('expandedRuleDropdownTitle');
28
+ if (dropdownTitle) {
29
+ const selectedCategory = itemsToUse[category]?.rules?.find(r => r.rule === ruleInCategory.rule);
30
+ if (selectedCategory) {
31
+ const count = selectedCategory.pagesAffected.length;
32
+ if (isHtmlGrouping) {
33
+ dropdownTitle.innerText = `HTML elements affected by this issue (${count})`;
34
+ } else {
35
+ dropdownTitle.innerText = `Pages affected by this issue (${count})`;
36
+ }
37
+ }
38
+ }
39
+ };
40
+
41
+
42
+ newGroupByPage.addEventListener('change', () => {
43
+ if (newGroupByPage.checked) {
44
+ renderGroupedByPage(category, ruleInCategory, isCustomFlow);
45
+ updateDropdownTitle(false);
46
+ }
47
+ });
48
+
49
+ newGroupByHtmlElement.addEventListener('change', async () => {
50
+ if (newGroupByHtmlElement.checked) {
51
+ updateDropdownTitle(true);
52
+ const contentContainer = document.getElementById('expandedRuleCategoryContent');
53
+ contentContainer.innerHTML = `
54
+ <div class="p-4 text-center">
55
+ <div class="spinner-border text-primary" role="status">
56
+ <span class="visually-hidden">Loading...</span>
57
+ </div>
58
+ <p class="mt-2">Loading all items for grouping...</p>
59
+ </div>
60
+ `;
61
+
62
+ await loadAllPageItems(ruleInCategory);
63
+
64
+ renderGroupedByHtmlElement(category, ruleInCategory, isCustomFlow);
65
+ }
66
+ });
67
+ }
68
+
69
+ async function loadAllPageItems(ruleInCategory) {
70
+ const loadPromises = ruleInCategory.pagesAffected.map(async (page) => {
71
+ if (!page.items && page.itemsCount > 0) {
72
+ page.items = [];
73
+ }
74
+ });
75
+
76
+ await Promise.all(loadPromises);
77
+ }
78
+
79
+ function renderGroupedByPage(category, ruleInCategory, isCustomFlow) {
80
+ const contentContainer = document.getElementById('expandedRuleCategoryContent');
81
+ const accordionsList = createElementFromString(`<ul class="unbulleted-list"></ul>`);
82
+
83
+ ruleInCategory.pagesAffected.forEach((page, index) => {
84
+ const accordion = createPageAccordion(page, index, category, ruleInCategory, isCustomFlow);
85
+ accordionsList.appendChild(accordion);
86
+ });
87
+
88
+ contentContainer.replaceChildren(accordionsList);
89
+ hljs.highlightAll();
90
+ }
91
+
92
+ function renderGroupedByHtmlElement(category, ruleInCategory, isCustomFlow) {
93
+ const contentContainer = document.getElementById('expandedRuleCategoryContent');
94
+
95
+ // Group items by HTML element
96
+ const groupedByHtml = {};
97
+ ruleInCategory.pagesAffected.forEach((page) => {
98
+ if (!page.items || page.items.length === 0) {
99
+ return;
100
+ }
101
+
102
+ page.items.forEach((item) => {
103
+ const htmlKey = item.html || 'No HTML element';
104
+ if (!groupedByHtml[htmlKey]) {
105
+ groupedByHtml[htmlKey] = [];
106
+ }
107
+ groupedByHtml[htmlKey].push({
108
+ ...item,
109
+ pageUrl: page.url,
110
+ pageTitle: page.pageTitle || page.metadata,
111
+ });
112
+ });
113
+ });
114
+
115
+ if (Object.keys(groupedByHtml).length === 0) {
116
+ contentContainer.innerHTML = `
117
+ <div class="p-4 text-center">
118
+ <p class="mb-0">No items available for grouping by HTML element.</p>
119
+ <p class="text-muted small">Items may need to be loaded first by expanding pages.</p>
120
+ </div>
121
+ `;
122
+ return;
123
+ }
124
+
125
+ const accordionsList = createElementFromString(`<ul class="unbulleted-list"></ul>`);
126
+ let htmlIndex = 0;
127
+
128
+ const sortedGroups = Object.entries(groupedByHtml).sort(([, itemsA], [, itemsB]) => {
129
+ return itemsB.length - itemsA.length; // DESC
130
+ });
131
+
132
+ sortedGroups.forEach(([htmlElement, items]) => {
133
+ const accordion = createHtmlElementAccordion(
134
+ htmlElement,
135
+ items,
136
+ htmlIndex,
137
+ category,
138
+ ruleInCategory,
139
+ isCustomFlow
140
+ );
141
+ accordionsList.appendChild(accordion);
142
+ htmlIndex++;
143
+ });
144
+
145
+ contentContainer.replaceChildren(accordionsList);
146
+ hljs.highlightAll();
147
+ }
148
+
149
+ function createHtmlElementAccordion(htmlElement, items, index, category, ruleInCategory, isCustomFlow) {
150
+ const accordionId = `${ruleInCategory.rule}-${category}-html-${index}`;
151
+ const itemsCount = items.length;
152
+
153
+ const htmlPreview = htmlElement === 'No HTML element' ? htmlElement : htmlElement;
154
+ const escapedPreview = htmlEscapeString(htmlPreview);
155
+
156
+ const accordion = createElementFromString(`
157
+ <li>
158
+ <div class="accordion mt-2 ${category}">
159
+ <div class="accordion-item rule-modal-item">
160
+ <div class="accordion-header" id="${accordionId}-title">
161
+ <button
162
+ aria-label="HTML element ${index + 1}, ${itemsCount} ${itemsCount === 1 ? 'occurrence' : 'occurrences'}"
163
+ class="accordion-button collapsed"
164
+ type="button"
165
+ >
166
+ <span class="sr-only visually-hidden">${itemsCount} ${itemsCount === 1 ? 'occurrence' : 'occurrences'}</span>
167
+ <svg
168
+ class="accordion-arrow"
169
+ width="24"
170
+ height="24"
171
+ viewBox="0 0 24 24"
172
+ fill="none"
173
+ xmlns="http://www.w3.org/2000/svg"
174
+ >
175
+ <path
176
+ d="M7.41 8.58984L12 13.1698L16.59 8.58984L18 9.99984L12 15.9998L6 9.99984L7.41 8.58984Z"
177
+ fill="#5735DF"
178
+ />
179
+ </svg>
180
+ <div class="me-3 html-element-preview flex-1">${escapedPreview}</div>
181
+ <div class="ms-auto rule-occurrence-count counter">${itemsCount} occ.</div>
182
+ </button>
183
+ </div>
184
+ <div id="${accordionId}-content" class="accordion-collapse collapse" aria-labelledby="${accordionId}-title">
185
+ <div class="accordion-body p-3">
186
+ </div>
187
+ </div>
188
+ </div>
189
+ </div>
190
+ </li>
191
+ `);
192
+
193
+ setupHtmlElementAccordionHandlers(accordion, accordionId, category, ruleInCategory, items, index);
194
+
195
+ return accordion;
196
+ }
197
+
198
+ function setupHtmlElementAccordionHandlers(accordion, accordionId, category, ruleInCategory, items, index) {
199
+ const button = accordion.querySelector('button');
200
+
201
+ button.addEventListener('click', function loadContent(event) {
202
+ const accordionBody = accordion.querySelector(".accordion-body");
203
+
204
+ if (!accordionBody.querySelector(".item-pagination-controls")) {
205
+ buildHtmlElementItemCards(accordionId, category, ruleInCategory, items, index);
206
+ this.setAttribute('data-bs-target', '#' + accordionId + "-content");
207
+ this.removeEventListener('click', loadContent);
208
+ this.click();
209
+ }
210
+ });
211
+
212
+ button.addEventListener('click', function initCollapse(event) {
213
+ this.setAttribute('data-bs-toggle', 'collapse');
214
+ this.setAttribute('data-bs-target', '#' + accordionId + "-content");
215
+ this.setAttribute('aria-expanded', 'false');
216
+ this.setAttribute('aria-controls', accordionId + "-content");
217
+
218
+ new bootstrap.Collapse(this, { toggle: false });
219
+
220
+ this.removeEventListener('click', initCollapse);
221
+ setTimeout(() => {
222
+ this.click();
223
+ }, 0);
224
+ });
225
+ }
226
+
227
+ function buildHtmlElementItemCards(accordionId, category, ruleInCategory, items, index) {
228
+ const accordion = document.getElementById(`${accordionId}-title`).parentElement.parentElement.parentElement;
229
+ const accordionBody = accordion.getElementsByClassName('accordion-body')[0];
230
+ const totalItems = items.length;
231
+ let currentItemIndex = 0;
232
+
233
+ const paginationControls = createPaginationControls(totalItems);
234
+
235
+ const itemCardContainer = createElementFromString('<div></div>');
236
+
237
+ async function renderItem(itemIndex) {
238
+ const item = items[itemIndex];
239
+ const accordionAIId = `${ruleInCategory.rule}-${category}-accordion-AI-html-${index}-${itemIndex}`;
240
+ const buttonAIId = `${ruleInCategory.rule}-${category}-button-AI-html-${index}-${itemIndex}`;
241
+ const errorAIId = `${ruleInCategory.rule}-${category}-error-AI-html-${index}-${itemIndex}`;
242
+
243
+ const isPurpleAiRule = oobeeAiRules.includes(ruleInCategory.rule);
244
+ let oobeeAiQueryLabel;
245
+ if (isPurpleAiRule) {
246
+ oobeeAiQueryLabel = await checkPurpleAiQueryLabel(ruleInCategory.rule, item.html);
247
+ }
248
+
249
+ const itemCard = createHtmlGroupedItemCard(item, isPurpleAiRule, oobeeAiQueryLabel, {
250
+ accordionDivToAppendAI: accordionAIId,
251
+ buttonDivForAiFeedback: buttonAIId,
252
+ aiErrorDiv: errorAIId,
253
+ ruleInCategory,
254
+ }, item.html);
255
+
256
+ itemCardContainer.replaceChildren(itemCard);
257
+
258
+ setupCopyButton(itemCard, item.xpath);
259
+
260
+ hljs.configure({ ignoreUnescapedHTML: true });
261
+ hljs.highlightAll();
262
+ }
263
+
264
+ function updatePaginationState() {
265
+ const prevBtn = paginationControls.querySelector('.pagination-prev');
266
+ const nextBtn = paginationControls.querySelector('.pagination-next');
267
+ const pageInput = paginationControls.querySelector('.pagination-page-input');
268
+
269
+ prevBtn.disabled = currentItemIndex === 0;
270
+ nextBtn.disabled = currentItemIndex === totalItems - 1;
271
+ pageInput.value = currentItemIndex + 1;
272
+ }
273
+
274
+ setupPaginationHandlers(paginationControls, totalItems, {
275
+ onPageChange: newIndex => {
276
+ currentItemIndex = newIndex;
277
+ renderItem(currentItemIndex);
278
+ updatePaginationState();
279
+ },
280
+ });
281
+
282
+ accordionBody.appendChild(paginationControls);
283
+ accordionBody.appendChild(itemCardContainer);
284
+
285
+ renderItem(0);
286
+ }
287
+
288
+ function createHtmlGroupedItemCard(item, isPurpleAiRule, oobeeAiQueryLabel, aiConfig, htmlElement) {
289
+ return createElementFromString(`
290
+ <div>
291
+ <div class="d-flex gap-3 flex-column">
292
+ <h3>Where to find this issue</h3>
293
+ ${item.screenshotPath ? createScreenshotSection(item.screenshotPath) : ''}
294
+ ${item.xpath ? createXpathSection(item.xpath) : ''}
295
+
296
+ <div class="d-flex justify-content-between g-one modal-view">
297
+ <div class="fw-semibold">URL</div>
298
+ <div class="page-item-card-section-content">
299
+ <a href="${item.pageUrl}" target="_blank">${item.pageUrl}</a>
300
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
301
+ <g clip-path="url(#clip0_174_1993)">
302
+ <path d="M12.6667 12.6667H3.33333V3.33333H8V2H3.33333C2.59333 2 2 2.6 2 3.33333V12.6667C2 13.4 2.59333 14 3.33333 14H12.6667C13.4 14 14 13.4 14 12.6667V8H12.6667V12.6667ZM9.33333 2V3.33333H11.7267L5.17333 9.88667L6.11333 10.8267L12.6667 4.27333V6.66667H14V2H9.33333Z" fill="#5735DF"/>
303
+ </g>
304
+ <defs>
305
+ <clipPath id="clip0_174_1993">
306
+ <rect width="16" height="16" fill="white"/>
307
+ </clipPath>
308
+ </defs>
309
+ </svg>
310
+ </div>
311
+ </div>
312
+
313
+ <div class="d-flex justify-content-between g-one modal-view">
314
+ <div class="fw-semibold mb-2">HTML element</div>
315
+ <pre class="page-item-card-section-content">
316
+ <code class="language-html">${htmlEscapeString(htmlElement)}</code>
317
+ </pre>
318
+ </div>
319
+
320
+ ${
321
+ item.displayNeedsReview
322
+ ? `<div class="d-flex justify-content-between g-one modal-view">
323
+ <div class="fw-semibold page-item-card-section-title">Details</div>
324
+ <div class="page-item-card-section-content">
325
+ ${generateItemMessageElement(item.displayNeedsReview, item.message)}
326
+ </div>
327
+ </div>`
328
+ : ''
329
+ }
330
+ ${isPurpleAiRule ? createAiSuggestionSection(item, oobeeAiQueryLabel, aiConfig) : ''}
331
+ </div>
332
+ </div>
333
+ `);
334
+ }
335
+
336
+ function createPageAccordion(page, index, category, ruleInCategory, isCustomFlow) {
337
+ const accordionId = `${ruleInCategory.rule}-${category}-page-${index}`;
338
+ const pageItemsCount = page.items.length || page.itemsCount || 0;
339
+ const normalMode = page.itemsCount === undefined;
340
+
341
+ const accordion = createElementFromString(`
342
+ <li class="${normalMode ? '' : 'no-chevron'}">
343
+ <div class="accordion mt-2 ${category}">
344
+ <div class="accordion-item rule-modal-item">
345
+ <div class="accordion-header" id="${accordionId}-title">
346
+ ${normalMode ? '<button' : '<div'}
347
+ aria-label="Page ${index + 1}: ${page.pageTitle}, ${pageItemsCount} ${pageItemsCount === 1 ? 'occurrence' : 'occurrences'}"
348
+ class="accordion-button collapsed"
349
+ ${normalMode ? 'type="button"' : ''}
350
+ >
351
+ <span class="sr-only visually-hidden">${pageItemsCount} ${pageItemsCount === 1 ? 'occurrence' : 'occurrences'}</span>
352
+ <svg
353
+ class="accordion-arrow"
354
+ width="24"
355
+ height="24"
356
+ viewBox="0 0 24 24"
357
+ fill="none"
358
+ xmlns="http://www.w3.org/2000/svg"
359
+ >
360
+ <path
361
+ d="M7.41 8.58984L12 13.1698L16.59 8.58984L18 9.99984L12 15.9998L6 9.99984L7.41 8.58984Z"
362
+ fill="#5735DF"
363
+ />
364
+ </svg>
365
+ <div class="me-3 flex-1">${page.metadata ? page.metadata : page.pageTitle}</div>
366
+ <div class="ms-auto rule-occurrence-count counter">${pageItemsCount} occ.</div>
367
+ ${normalMode ? '</button>' : '</div>'}
368
+ </div>
369
+ <div id="${accordionId}-content" class="accordion-collapse collapse" aria-labelledby="${accordionId}-title">
370
+ <div class="accordion-body p-3">
371
+ ${isCustomFlow
372
+ ? createCustomFlowContent(page)
373
+ :
374
+ `<div class="d-flex align-items-center gap-1 accordion-link" style=" word-wrap: break-word; word-break: break-word;">
375
+ <a href="${page.url}" target="_blank">${page.url}
376
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-left: 0.25rem;">
377
+ <g clip-path="url(#clip0_174_1993)">
378
+ <path d="M12.6667 12.6667H3.33333V3.33333H8V2H3.33333C2.59333 2 2 2.6 2 3.33333V12.6667C2 13.4 2.59333 14 3.33333 14H12.6667C13.4 14 14 13.4 14 12.6667V8H12.6667V12.6667ZM9.33333 2V3.33333H11.7267L5.17333 9.88667L6.11333 10.8267L12.6667 4.27333V6.66667H14V2H9.33333Z" fill="#5735DF"/>
379
+ </g>
380
+ <defs>
381
+ <clipPath id="clip0_174_1993">
382
+ <rect width="16" height="16" fill="white"/>
383
+ </clipPath>
384
+ </defs>
385
+ </svg>
386
+ </a>
387
+ </div>`
388
+ }
389
+ </div>
390
+ </div>
391
+ </div>
392
+ </div>
393
+ </li>
394
+ `);
395
+
396
+ if (normalMode) {
397
+ setupPageAccordionHandlers(accordion, accordionId, category, ruleInCategory, page, index);
398
+ }
399
+
400
+ if (isCustomFlow) {
401
+ setupCustomFlowScreenshot(accordion, page);
402
+ }
403
+
404
+ return accordion;
405
+ }
406
+
407
+ function createCustomFlowContent(page) {
408
+ return `
409
+ <div class="custom-flow-screenshot-container">
410
+ <div class="custom-flow-thumb">
411
+ <img
412
+ src="${page.pageImagePath}"
413
+ alt="Screenshot of ${page.url}"
414
+ class="custom-flow-screenshot"
415
+ onerror="this.onerror = null; this.remove();"
416
+ >
417
+ </div>
418
+ <div class="display-url-container">
419
+ <div><a href="${page.url}" target="_blank">${page.url}</a></div>
420
+ </div>
421
+ </div>
422
+ `;
423
+ }
424
+
425
+ function setupPageAccordionHandlers(accordion, accordionId, category, ruleInCategory, page, index) {
426
+ const button = accordion.querySelector('button');
427
+
428
+ button.addEventListener('click', function loadContent(event) {
429
+ const accordionBody = accordion.querySelector(".accordion-body");
430
+
431
+ if (!accordionBody.querySelector(".item-pagination-controls") &&
432
+ !accordionBody.querySelector(".unbulleted-list")) {
433
+ buildItemCardsWithPagination(accordionId, category, ruleInCategory, page, index);
434
+ this.setAttribute('data-bs-target', '#' + accordionId + "-content");
435
+ this.removeEventListener('click', loadContent);
436
+ this.click();
437
+ }
438
+ });
439
+
440
+ button.addEventListener('click', function initCollapse(event) {
441
+ this.setAttribute('data-bs-toggle', 'collapse');
442
+ this.setAttribute('data-bs-target', '#' + accordionId + "-content");
443
+ this.setAttribute('aria-expanded', 'false');
444
+ this.setAttribute('aria-controls', accordionId + "-content");
445
+
446
+ new bootstrap.Collapse(this, { toggle: false });
447
+
448
+ this.removeEventListener('click', initCollapse);
449
+ setTimeout(() => {
450
+ this.click();
451
+ }, 0);
452
+ });
453
+ }
454
+
455
+ function setupCustomFlowScreenshot(accordion, page) {
456
+ const customScreenshotElem = accordion.getElementsByClassName('custom-flow-screenshot')[0];
457
+
458
+ customScreenshotElem.onerror = function(event) {
459
+ this.onerror = null;
460
+ this.remove();
461
+ };
462
+
463
+ customScreenshotElem.onclick = function(event) {
464
+ event.preventDefault();
465
+ openLightbox(this.src, page.pageTitle, page.url);
466
+ };
467
+ }
468
+ </script>