@govtechsg/oobee 0.10.76 → 0.10.78-alpha1

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 (137) hide show
  1. package/.github/workflows/publish.yml +8 -1
  2. package/INTEGRATION.md +50 -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 +1588 -0
  37. package/dist/npmIndex.js +640 -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/examples/oobee-scan-html-demo.js +51 -0
  130. package/examples/oobee-scan-page-demo.js +40 -0
  131. package/package.json +9 -3
  132. package/src/constants/common.ts +2 -2
  133. package/src/constants/constants.ts +3 -1
  134. package/src/crawlers/crawlDomain.ts +1 -0
  135. package/src/crawlers/runCustom.ts +0 -1
  136. package/src/mergeAxeResults.ts +43 -22
  137. package/src/npmIndex.ts +500 -131
@@ -0,0 +1,353 @@
1
+ <script>
2
+ function buildItemCardsWithPagination(accordionId, category, ruleInCategory, page, index) {
3
+ const accordionAIId = `${ruleInCategory.rule}-${category}-accordion-AI-${index}`;
4
+ const buttonAIId = `${ruleInCategory.rule}-${category}-button-AI-${index}`;
5
+ const errorAIId = `${ruleInCategory.rule}-${category}-error-AI-${index}`;
6
+ const genAiAccordionId = `${ruleInCategory.rule}-${category}-genai-accordion-${index}`;
7
+ const genAiButtonId = `${ruleInCategory.rule}-${category}-genai-button-${index}`;
8
+ const genAiErrorId = `${ruleInCategory.rule}-${category}-genai-error-${index}`;
9
+ const accordion = document.getElementById(`${accordionId}-title`).parentElement.parentElement
10
+ .parentElement;
11
+
12
+ const accordionBody = accordion.getElementsByClassName('accordion-body')[0];
13
+ const totalItems = page.items.length;
14
+ let currentItemIndex = 0;
15
+
16
+ // Create pagination controls
17
+ const paginationControls = createPaginationControls(totalItems);
18
+
19
+ // Create container for the single item card
20
+ const itemCardContainer = createElementFromString('<div></div>');
21
+
22
+ // Function to render a specific item
23
+ async function renderItem(itemIndex) {
24
+ const item = page.items[itemIndex];
25
+ const accordionDivToAppendAI = `${accordionAIId}-${itemIndex}`;
26
+ const buttonDivForAiFeedback = `${buttonAIId}-${itemIndex}`;
27
+ const aiErrorDiv = `${errorAIId}-${itemIndex}`;
28
+ const genAiAccordionDiv = `${genAiAccordionId}-${itemIndex}`;
29
+ const genAiButtonDiv = `${genAiButtonId}-${itemIndex}`;
30
+ const genAiErrorDiv = `${genAiErrorId}-${itemIndex}`;
31
+
32
+ const isPurpleAiRule = oobeeAiRules.includes(ruleInCategory.rule);
33
+ let oobeeAiQueryLabel;
34
+ if (isPurpleAiRule) {
35
+ oobeeAiQueryLabel = await checkPurpleAiQueryLabel(ruleInCategory.rule, item.html);
36
+ }
37
+
38
+ const itemCard = createItemCard(item, isPurpleAiRule, oobeeAiQueryLabel, {
39
+ accordionDivToAppendAI,
40
+ buttonDivForAiFeedback,
41
+ aiErrorDiv,
42
+ genAiAccordionDiv,
43
+ genAiButtonDiv,
44
+ genAiErrorDiv,
45
+ ruleInCategory,
46
+ });
47
+
48
+ // Clear and update the container
49
+ itemCardContainer.replaceChildren(itemCard);
50
+
51
+ // Setup copy button functionality
52
+ setupCopyButton(itemCard, item.xpath);
53
+
54
+ // Highlight code
55
+ hljs.configure({ ignoreUnescapedHTML: true });
56
+ hljs.highlightAll();
57
+ }
58
+
59
+ // Function to update pagination state
60
+ function updatePaginationState() {
61
+ const prevBtn = paginationControls.querySelector('.pagination-prev');
62
+ const nextBtn = paginationControls.querySelector('.pagination-next');
63
+ const pageInput = paginationControls.querySelector('.pagination-page-input');
64
+
65
+ prevBtn.disabled = currentItemIndex === 0;
66
+ nextBtn.disabled = currentItemIndex === totalItems - 1;
67
+ pageInput.value = currentItemIndex + 1;
68
+ }
69
+
70
+ // Setup pagination event handlers
71
+ setupPaginationHandlers(paginationControls, totalItems, {
72
+ onPageChange: newIndex => {
73
+ currentItemIndex = newIndex;
74
+ renderItem(currentItemIndex);
75
+ updatePaginationState();
76
+ },
77
+ });
78
+
79
+ // Append pagination and container to accordion body
80
+ accordionBody.appendChild(paginationControls);
81
+ accordionBody.appendChild(itemCardContainer);
82
+
83
+ // Render the first item
84
+ renderItem(0);
85
+ }
86
+
87
+ function createPaginationControls(totalItems) {
88
+ return createElementFromString(`
89
+ <div class="item-pagination-controls">
90
+ <div class="mr-1">Occurrence no.</div>
91
+ <button class="pagination-arrow pagination-prev" aria-label="Previous item" disabled>
92
+ <svg width="8" height="12" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg">
93
+ <path d="M7.41 1.41L6 0L0 6L6 12L7.41 10.59L2.83 6L7.41 1.41Z" fill="#4D34BF"/>
94
+ </svg>
95
+ </button>
96
+ <div class="pagination-page-info">
97
+ <input type="number" class="pagination-page-input" value="1" min="1" max="${totalItems}" aria-label="Current page number" />
98
+ </div>
99
+ <button class="pagination-arrow pagination-next" aria-label="Next item" ${totalItems <= 1 ? 'disabled' : ''}>
100
+ <svg width="8" height="12" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg">
101
+ <path d="M1.41 0L0 1.41L4.58 6L0 10.59L1.41 12L7.41 6L1.41 0Z" fill="#4D34BF"/>
102
+ </svg>
103
+ </button>
104
+ <div class="mr-1">of ${totalItems}</div>
105
+ </div>
106
+ `);
107
+ }
108
+
109
+ function setupPaginationHandlers(paginationControls, totalItems, callbacks) {
110
+ const prevBtn = paginationControls.querySelector('.pagination-prev');
111
+ const nextBtn = paginationControls.querySelector('.pagination-next');
112
+ const pageInput = paginationControls.querySelector('.pagination-page-input');
113
+
114
+ let currentIndex = 0;
115
+
116
+ prevBtn.addEventListener('click', () => {
117
+ if (currentIndex > 0) {
118
+ currentIndex--;
119
+ callbacks.onPageChange(currentIndex);
120
+ }
121
+ });
122
+
123
+ nextBtn.addEventListener('click', () => {
124
+ if (currentIndex < totalItems - 1) {
125
+ currentIndex++;
126
+ callbacks.onPageChange(currentIndex);
127
+ }
128
+ });
129
+
130
+ pageInput.addEventListener('change', e => {
131
+ let pageNum = parseInt(e.target.value);
132
+ if (isNaN(pageNum) || pageNum < 1) {
133
+ pageNum = 1;
134
+ } else if (pageNum > totalItems) {
135
+ pageNum = totalItems;
136
+ }
137
+ currentIndex = pageNum - 1;
138
+ callbacks.onPageChange(currentIndex);
139
+ });
140
+
141
+ pageInput.addEventListener('keydown', e => {
142
+ if (e.key === 'Enter') {
143
+ e.target.blur();
144
+ }
145
+ });
146
+ }
147
+
148
+ function createItemCard(item, isPurpleAiRule, oobeeAiQueryLabel, aiConfig) {
149
+ const isGenAiSupportedRule = ['color-contrast', 'oobee-accessible-label', 'image-alt', 'listitem', 'link-in-text-block', 'target-size'].includes(aiConfig.ruleInCategory.rule);
150
+ const showGenAiUI = window.oobeeGenAiFeatureEnabled && isGenAiSupportedRule;
151
+
152
+ return createElementFromString(`
153
+ <div>
154
+ <div class="d-flex gap-3 flex-column">
155
+ <h3>Where to find this issue</h3>
156
+ ${item.screenshotPath ? createScreenshotSection(item.screenshotPath) : ''}
157
+ ${item.xpath ? createXpathSection(item.xpath) : ''}
158
+ ${createElementSection(item)}
159
+ ${
160
+ item.displayNeedsReview
161
+ ? `<div class="d-flex justify-content-between g-one">
162
+ <div class="fw-semibold page-item-card-section-title">Details</div>
163
+ <div class="page-item-card-section-content">
164
+ ${generateItemMessageElement(item.displayNeedsReview, item.message)}
165
+ </div>
166
+ </div>`
167
+ : ''
168
+ }
169
+ ${isPurpleAiRule ? createAiSuggestionSection(item, oobeeAiQueryLabel, aiConfig) : ''}
170
+ ${showGenAiUI ? createGenAiSuggestFixSection(item, aiConfig) : ''}
171
+ </div>
172
+ </div>
173
+ `);
174
+ }
175
+
176
+ function createScreenshotSection(screenshotPath) {
177
+ return `
178
+ <div class="hide-on-img-error">
179
+ <div class="d-flex justify-content-between g-one modal-view">
180
+ <div class="fw-semibold">Screenshot</div>
181
+ <div class="page-item-card-section-content bg-grey-w-border">
182
+ <img
183
+ src="${screenshotPath}"
184
+ onerror="this.onerror = null; this.closest('div.hide-on-img-error').remove();"
185
+ alt="Screenshot of affected element" />
186
+ </div>
187
+ </div>
188
+ </div>
189
+ `;
190
+ }
191
+
192
+ function createXpathSection(xpath) {
193
+ return `
194
+ <div class="d-flex justify-content-between g-one modal-view">
195
+ <div class="fw-semibold">Path</div>
196
+ <div class="page-item-card-section-content">
197
+ <div class="g-one path-container">
198
+ ${xpath}
199
+ <button
200
+ aria-label="Copy path to clipboard"
201
+ class="copy-button"
202
+ data-bs-toggle="tooltip"
203
+ data-bs-placement="top"
204
+ data-bs-original-title="Copy"
205
+ >
206
+ <svg width="13" height="15" viewBox="0 0 13 15" fill="none" xmlns="http://www.w3.org/2000/svg">
207
+ <path d="M3.33333 0H11.3333C12.0667 0 12.6667 0.6 12.6667 1.33333V10.6667H11.3333V1.33333H3.33333V0ZM1.33333 2.66667H8.66667C9.4 2.66667 10 3.26667 10 4V13.3333C10 14.0667 9.4 14.6667 8.66667 14.6667H1.33333C0.600001 14.6667 9.53674e-07 14.0667 9.53674e-07 13.3333V4C9.53674e-07 3.26667 0.600001 2.66667 1.33333 2.66667ZM1.33333 13.3333H8.66667V4H1.33333V13.3333Z" fill="#5735DF"/>
208
+ </svg>
209
+ </button>
210
+ </div>
211
+ </div>
212
+ </div>
213
+ `;
214
+ }
215
+
216
+ function createElementSection(item) {
217
+ return `
218
+ <div class="d-flex justify-content-between g-one modal-view">
219
+ ${
220
+ item.html
221
+ ? `
222
+ <div class="fw-semibold">HTML element</div>
223
+ <pre class="page-item-card-section-content">
224
+ <code class="language-html">
225
+ ${htmlEscapeString(item.html)}
226
+ </code>
227
+ </pre>`
228
+ : `
229
+ <div class="fw-semibold">Location</div>
230
+ <div class="page-item-card-section-content">
231
+ ${item.page > 0 ? `Page ${item.page}` : 'Document'}
232
+ </div>`
233
+ }
234
+ </div>
235
+ `;
236
+ }
237
+
238
+ function createAiSuggestionSection(item, oobeeAiQueryLabel, aiConfig) {
239
+ const { accordionDivToAppendAI, buttonDivForAiFeedback, aiErrorDiv, ruleInCategory } = aiConfig;
240
+
241
+ let aiContent = '';
242
+ if (oobeeAiQueryLabel.hasNetworkError) {
243
+ aiContent = `
244
+ <button
245
+ id="${buttonDivForAiFeedback}"
246
+ class="aiGenerateResponseButton"
247
+ onClick="handleOfflinePurpleAi(
248
+ '${ruleInCategory.rule}',
249
+ '${accordionDivToAppendAI}',
250
+ '${escapeHtmlStringForArg(item.html)}',
251
+ '${buttonDivForAiFeedback}',
252
+ '${aiErrorDiv}')"
253
+ >
254
+ Generate response
255
+ </button>
256
+ <div id="${aiErrorDiv}"></div>
257
+ `;
258
+ } else if (oobeeAiQueryLabel.label) {
259
+ aiContent = `
260
+ <button
261
+ id="${buttonDivForAiFeedback}"
262
+ class="aiGenerateResponseButton"
263
+ onClick="getPurpleAiAnswer(
264
+ '${ruleInCategory.rule}',
265
+ '${accordionDivToAppendAI}',
266
+ '${oobeeAiQueryLabel.label}',
267
+ '${buttonDivForAiFeedback}',
268
+ '${aiErrorDiv}')"
269
+ >
270
+ Generate response
271
+ </button>
272
+ <div id="${aiErrorDiv}"></div>
273
+ `;
274
+ } else {
275
+ aiContent = `
276
+ <span class="processAI">
277
+ Processing AI suggestions, please check back later.
278
+ </span>
279
+ `;
280
+ }
281
+
282
+ return `
283
+ <div class="d-flex g-one">
284
+ <div class="fw-semibold page-item-card-section-title">AI suggestion</div>
285
+ <div id="${accordionDivToAppendAI}" class="page-item-card-section-content">
286
+ ${aiContent}
287
+ </div>
288
+ </div>
289
+ `;
290
+ }
291
+
292
+ function createGenAiSuggestFixSection(item, aiConfig) {
293
+ const { genAiAccordionDiv, genAiButtonDiv, genAiErrorDiv, ruleInCategory } = aiConfig;
294
+
295
+ return `
296
+ <div class="d-flex g-one genai-fix-section">
297
+ <div class="fw-semibold page-item-card-section-title">Generative AI Fix</div>
298
+ <div id="${genAiAccordionDiv}" class="page-item-card-section-content">
299
+ <button
300
+ id="${genAiButtonDiv}"
301
+ class="genAiSuggestFixButton"
302
+ onClick="generateGenAiSuggestFix(
303
+ '${ruleInCategory.rule}',
304
+ '${genAiAccordionDiv}',
305
+ '${escapeHtmlStringForArg(item.html)}',
306
+ '${genAiButtonDiv}',
307
+ '${genAiErrorDiv}')"
308
+ >
309
+ Gen AI Suggest Fix
310
+ </button>
311
+ <div id="${genAiErrorDiv}"></div>
312
+ </div>
313
+ </div>
314
+ `;
315
+ }
316
+
317
+ function setupCopyButton(itemCard, xpath) {
318
+ const copyButtonElem = itemCard.getElementsByClassName('copy-button')[0];
319
+ if (!copyButtonElem || !xpath) return;
320
+
321
+ const copyTooltipItem = new bootstrap.Tooltip(copyButtonElem);
322
+ const textToCopy = createElementFromString(`<textarea>${xpath}</textarea>`);
323
+
324
+ copyButtonElem.onclick = event => {
325
+ textToCopy.select();
326
+ navigator.clipboard.writeText(textToCopy.value);
327
+
328
+ copyButtonElem.setAttribute('data-bs-original-title', 'Copied');
329
+ copyTooltipItem.update();
330
+ copyTooltipItem.show();
331
+
332
+ setTimeout(() => {
333
+ copyButtonElem.setAttribute('data-bs-original-title', 'Copy');
334
+ copyTooltipItem.update();
335
+ }, 1500);
336
+ };
337
+
338
+ copyButtonElem.onmousedown = event => {
339
+ event.preventDefault();
340
+ };
341
+ }
342
+
343
+ // Legacy function name for backwards compatibility
344
+ function buildExpandedRuleCategoryContentAccordian(
345
+ accordionId,
346
+ category,
347
+ ruleInCategory,
348
+ page,
349
+ index,
350
+ ) {
351
+ buildItemCardsWithPagination(accordionId, category, ruleInCategory, page, index);
352
+ }
353
+ </script>