@govtechsg/oobee 0.10.70 → 0.10.74
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.
- package/DETAILS.md +1 -1
- package/README.md +12 -0
- package/S3_UPLOAD_README.md +172 -0
- package/dev/runGenerateJustHtmlReport.ts +25 -0
- package/package.json +7 -4
- package/src/combine.ts +72 -15
- package/src/constants/common.ts +91 -93
- package/src/constants/constants.ts +536 -59
- package/src/crawlers/crawlDomain.ts +313 -305
- package/src/crawlers/crawlIntelligentSitemap.ts +24 -18
- package/src/crawlers/crawlLocalFile.ts +29 -27
- package/src/crawlers/crawlSitemap.ts +265 -254
- package/src/crawlers/custom/utils.ts +809 -119
- package/src/crawlers/runCustom.ts +29 -4
- package/src/generateHtmlReport.ts +224 -0
- package/src/mergeAxeResults.ts +133 -46
- package/src/runGenerateJustHtmlReport.ts +20 -0
- package/src/services/s3Uploader.ts +184 -0
- package/src/static/ejs/partials/components/allIssues/AllIssues.ejs +9 -0
- package/src/static/ejs/partials/components/allIssues/CategoryBadges.ejs +82 -0
- package/src/static/ejs/partials/components/allIssues/FilterBar.ejs +33 -0
- package/src/static/ejs/partials/components/allIssues/IssuesTable.ejs +41 -0
- package/src/static/ejs/partials/components/header/SiteInfo.ejs +119 -0
- package/src/static/ejs/partials/components/header/aboutScanModal/AboutScanModal.ejs +15 -0
- package/src/static/ejs/partials/components/header/aboutScanModal/ScanConfiguration.ejs +44 -0
- package/src/static/ejs/partials/components/header/aboutScanModal/ScanDetails.ejs +142 -0
- package/src/static/ejs/partials/components/prioritiseIssues/IssueDetailCard.ejs +36 -0
- package/src/static/ejs/partials/components/prioritiseIssues/PrioritiseIssues.ejs +47 -0
- package/src/static/ejs/partials/components/ruleModal/ruleOffcanvas.ejs +196 -0
- package/src/static/ejs/partials/components/scannedPagesSegmentedTabs.ejs +48 -0
- package/src/static/ejs/partials/components/shared/InfoAlert.ejs +3 -0
- package/src/static/ejs/partials/components/{topFive.ejs → topTen.ejs} +2 -2
- package/src/static/ejs/partials/components/wcagCompliance/FailedCriteria.ejs +47 -0
- package/src/static/ejs/partials/components/wcagCompliance/WcagCompliance.ejs +16 -0
- package/src/static/ejs/partials/components/wcagCompliance/WcagGaugeBar.ejs +16 -0
- package/src/static/ejs/partials/components/wcagCoverageDetails.ejs +18 -0
- package/src/static/ejs/partials/footer.ejs +1 -1
- package/src/static/ejs/partials/header.ejs +7 -223
- package/src/static/ejs/partials/main.ejs +12 -23
- package/src/static/ejs/partials/scripts/allIssues/AllIssues.ejs +376 -0
- package/src/static/ejs/partials/scripts/categorySummary.ejs +1 -1
- package/src/static/ejs/partials/scripts/header/SiteInfo.ejs +44 -0
- package/src/static/ejs/partials/scripts/header/aboutScanModal/AboutScanModal.ejs +51 -0
- package/src/static/ejs/partials/scripts/header/aboutScanModal/ScanConfiguration.ejs +127 -0
- package/src/static/ejs/partials/scripts/header/aboutScanModal/ScanDetails.ejs +60 -0
- package/src/static/ejs/partials/scripts/prioritiseIssues/IssueDetailCard.ejs +137 -0
- package/src/static/ejs/partials/scripts/prioritiseIssues/PrioritiseIssues.ejs +214 -0
- package/src/static/ejs/partials/scripts/prioritiseIssues/wcagSvgMap.ejs +861 -0
- package/src/static/ejs/partials/scripts/ruleModal/constants.ejs +957 -0
- package/src/static/ejs/partials/scripts/ruleModal/itemCardRenderer.ejs +353 -0
- package/src/static/ejs/partials/scripts/ruleModal/pageAccordionBuilder.ejs +468 -0
- package/src/static/ejs/partials/scripts/ruleModal/ruleOffcanvas.ejs +306 -0
- package/src/static/ejs/partials/scripts/ruleModal/utilities.ejs +483 -0
- package/src/static/ejs/partials/scripts/scannedPagesSegmentedTabs.ejs +35 -0
- package/src/static/ejs/partials/scripts/screenshotLightbox.ejs +61 -57
- package/src/static/ejs/partials/scripts/topTen.ejs +61 -0
- package/src/static/ejs/partials/scripts/utils.ejs +15 -0
- package/src/static/ejs/partials/scripts/wcagCompliance/FailedCriteria.ejs +103 -0
- package/src/static/ejs/partials/scripts/wcagCompliance/WcagGaugeBar.ejs +47 -0
- package/src/static/ejs/partials/scripts/wcagCompliance.ejs +15 -0
- package/src/static/ejs/partials/scripts/wcagCoverageDetails.ejs +75 -0
- package/src/static/ejs/partials/styles/allIssues/AllIssues.ejs +384 -0
- package/src/static/ejs/partials/styles/bootstrap.ejs +17 -1
- package/src/static/ejs/partials/styles/header/SiteInfo.ejs +121 -0
- package/src/static/ejs/partials/styles/header/aboutScanModal/AboutScanModal.ejs +82 -0
- package/src/static/ejs/partials/styles/header/aboutScanModal/ScanConfiguration.ejs +50 -0
- package/src/static/ejs/partials/styles/header/aboutScanModal/ScanDetails.ejs +149 -0
- package/src/static/ejs/partials/styles/header.ejs +7 -0
- package/src/static/ejs/partials/styles/prioritiseIssues/IssueDetailCard.ejs +141 -0
- package/src/static/ejs/partials/styles/prioritiseIssues/PrioritiseIssues.ejs +204 -0
- package/src/static/ejs/partials/styles/ruleModal/ruleOffcanvas.ejs +456 -0
- package/src/static/ejs/partials/styles/scannedPagesSegmentedTabs.ejs +46 -0
- package/src/static/ejs/partials/styles/shared/InfoAlert.ejs +12 -0
- package/src/static/ejs/partials/styles/styles.ejs +198 -470
- package/src/static/ejs/partials/styles/topTenCard.ejs +44 -0
- package/src/static/ejs/partials/styles/wcagCompliance/FailedCriteria.ejs +59 -0
- package/src/static/ejs/partials/styles/wcagCompliance/WcagGaugeBar.ejs +62 -0
- package/src/static/ejs/partials/styles/wcagCompliance.ejs +36 -0
- package/src/static/ejs/partials/styles/wcagCoverageDetails.ejs +33 -0
- package/src/static/ejs/report.ejs +42 -259
- package/src/static/ejs/summary.ejs +1 -1
- package/src/utils.ts +30 -0
- package/src/static/ejs/partials/components/categorySelector.ejs +0 -4
- package/src/static/ejs/partials/components/categorySelectorDropdown.ejs +0 -57
- package/src/static/ejs/partials/components/pagesScannedModal.ejs +0 -70
- package/src/static/ejs/partials/components/reportSearch.ejs +0 -47
- package/src/static/ejs/partials/components/ruleOffcanvas.ejs +0 -105
- package/src/static/ejs/partials/components/scanAbout.ejs +0 -328
- package/src/static/ejs/partials/components/wcagCompliance.ejs +0 -52
- package/src/static/ejs/partials/scripts/categorySelectorDropdownScript.ejs +0 -190
- package/src/static/ejs/partials/scripts/reportSearch.ejs +0 -287
- package/src/static/ejs/partials/scripts/ruleOffcanvas.ejs +0 -804
- package/src/static/ejs/partials/scripts/scanAboutScript.ejs +0 -38
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
function generateWcagConformanceLinks(conformanceList) {
|
|
3
|
+
const wcagConformanceUrls = {
|
|
4
|
+
wcag111: 'https://www.w3.org/TR/WCAG22/#non-text-content',
|
|
5
|
+
wcag122: 'https://www.w3.org/TR/WCAG22/#captions-prerecorded',
|
|
6
|
+
wcag131: 'https://www.w3.org/TR/WCAG22/#info-and-relationships',
|
|
7
|
+
wcag135: 'https://www.w3.org/TR/WCAG22/#identify-input-purpose',
|
|
8
|
+
wcag141: 'https://www.w3.org/TR/WCAG22/#use-of-color',
|
|
9
|
+
wcag142: 'https://www.w3.org/TR/WCAG22/#audio-control',
|
|
10
|
+
wcag143: 'https://www.w3.org/TR/WCAG22/#contrast-minimum',
|
|
11
|
+
wcag144: 'https://www.w3.org/TR/WCAG22/#resize-text',
|
|
12
|
+
wcag146: 'https://www.w3.org/TR/WCAG22/#contrast-enhanced',
|
|
13
|
+
wcag1412: 'https://www.w3.org/TR/WCAG22/#text-spacing',
|
|
14
|
+
wcag211: 'https://www.w3.org/TR/WCAG22/#keyboard',
|
|
15
|
+
wcag213: 'https://www.w3.org/WAI/WCAG22/Understanding/keyboard-no-exception.html',
|
|
16
|
+
wcag221: 'https://www.w3.org/TR/WCAG22/#timing-adjustable',
|
|
17
|
+
wcag222: 'https://www.w3.org/TR/WCAG22/#pause-stop-hide',
|
|
18
|
+
wcag224: 'https://www.w3.org/TR/WCAG22/#interruptions',
|
|
19
|
+
wcag241: 'https://www.w3.org/TR/WCAG22/#bypass-blocks',
|
|
20
|
+
wcag242: 'https://www.w3.org/TR/WCAG22/#page-titled',
|
|
21
|
+
wcag244: 'https://www.w3.org/TR/WCAG22/#link-purpose-in-context',
|
|
22
|
+
wcag249: 'https://www.w3.org/TR/WCAG22/#link-purpose-link-only',
|
|
23
|
+
wcag258: 'https://www.w3.org/TR/WCAG22/#target-size-minimum',
|
|
24
|
+
wcag311: 'https://www.w3.org/TR/WCAG22/#language-of-page',
|
|
25
|
+
wcag312: 'https://www.w3.org/TR/WCAG22/#language-of-parts',
|
|
26
|
+
wcag315: 'https://www.w3.org/TR/WCAG22/#reading-level',
|
|
27
|
+
wcag325: 'https://www.w3.org/TR/WCAG22/#change-on-request',
|
|
28
|
+
wcag332: 'https://www.w3.org/TR/WCAG22/#labels-or-instructions',
|
|
29
|
+
wcag412: 'https://www.w3.org/TR/WCAG22/#name-role-value',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const links = [];
|
|
33
|
+
for (let i = 1; i < conformanceList.length; i++) {
|
|
34
|
+
const [wcagSection, subSection, ...sectionItem] = conformanceList[i].slice(4);
|
|
35
|
+
const formattedConformanceNumber = `${wcagSection}.${subSection}.${sectionItem.join('')}`;
|
|
36
|
+
links.push(
|
|
37
|
+
`<a href="${wcagConformanceUrls[conformanceList[i]]}" target="_blank">WCAG ${formattedConformanceNumber}</a>`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
return links.join('  ,   ');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function generateItemMessageElement(displayNeedsReview, rawMessage) {
|
|
44
|
+
if (rawMessage.includes('\n\nFix')) {
|
|
45
|
+
rawMessage = rawMessage.replace('\n\nFix', '\n Fix');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const htmlEscapedMessageArray = rawMessage.split('\n ').map(m => htmlEscapeString(m));
|
|
49
|
+
|
|
50
|
+
if (displayNeedsReview) {
|
|
51
|
+
if (htmlEscapedMessageArray.length === 1) {
|
|
52
|
+
return `<p class="mb-0">${htmlEscapedMessageArray[0]}</p>`;
|
|
53
|
+
} else {
|
|
54
|
+
return `<ul>${htmlEscapedMessageArray.map(m => `<li>${m}</li>`).join('')}</ul>`;
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
let i = 0;
|
|
58
|
+
const elements = [];
|
|
59
|
+
while (i < htmlEscapedMessageArray.length) {
|
|
60
|
+
if (htmlEscapedMessageArray[i].startsWith('Fix ')) {
|
|
61
|
+
elements.push(`<p class="mb-0">${htmlEscapedMessageArray[i]}</p>`);
|
|
62
|
+
i++;
|
|
63
|
+
} else {
|
|
64
|
+
const fixesList = [];
|
|
65
|
+
while (
|
|
66
|
+
i < htmlEscapedMessageArray.length &&
|
|
67
|
+
!htmlEscapedMessageArray[i].startsWith('Fix a')
|
|
68
|
+
) {
|
|
69
|
+
fixesList.push(`<li>${htmlEscapedMessageArray[i]}</li>`);
|
|
70
|
+
i++;
|
|
71
|
+
}
|
|
72
|
+
elements.push(`<ul>${fixesList.join('')}</ul>`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return elements.join('');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Generate AI-powered fix suggestions using wcag-eval API
|
|
81
|
+
const generateGenAiSuggestFix = async (ruleId, accordionDiv, html, buttonDiv, errorDiv) => {
|
|
82
|
+
console.log('Gen AI Suggest Fix called:', { ruleId, accordionDiv, html, buttonDiv, errorDiv });
|
|
83
|
+
|
|
84
|
+
const SUPPORTED_RULES = ['color-contrast', 'oobee-accessible-label', 'image-alt', 'listitem', 'link-in-text-block', 'target-size'];
|
|
85
|
+
if (!SUPPORTED_RULES.includes(ruleId)) {
|
|
86
|
+
console.warn(`Gen AI Suggest Fix not yet available for ${ruleId}`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Update button state
|
|
91
|
+
const button = document.getElementById(buttonDiv);
|
|
92
|
+
if (!button) {
|
|
93
|
+
console.error('Gen AI button not found:', buttonDiv);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
button.disabled = true;
|
|
98
|
+
button.textContent = 'Generating Fix...';
|
|
99
|
+
|
|
100
|
+
// Clear previous errors
|
|
101
|
+
const errorContainer = document.getElementById(errorDiv);
|
|
102
|
+
if (errorContainer) {
|
|
103
|
+
errorContainer.innerHTML = '';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
let violationContext = null;
|
|
108
|
+
let elementContext = null;
|
|
109
|
+
|
|
110
|
+
// Attempt to get violation context from embedded JSON data
|
|
111
|
+
if (window.oobeeJsonContextParser && window.oobeeJsonContextParser.jsonData) {
|
|
112
|
+
violationContext = window.oobeeJsonContextParser.getViolationContextByHtml(html, ruleId);
|
|
113
|
+
|
|
114
|
+
if (violationContext) {
|
|
115
|
+
console.log('Found violation context from embedded JSON:', violationContext);
|
|
116
|
+
} else {
|
|
117
|
+
console.warn('No matching violation found in embedded JSON data, will extract context from DOM');
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
console.warn('Embedded JSON context parser not available, will extract context from DOM');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Extract element context from DOM if embedded JSON data is not available
|
|
124
|
+
if (!violationContext || !violationContext.colorData) {
|
|
125
|
+
console.log('Extracting element context from DOM as fallback');
|
|
126
|
+
elementContext = extractElementContext(html);
|
|
127
|
+
} else {
|
|
128
|
+
console.log('Using embedded JSON violation context, skipping DOM extraction');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Build scanner findings from embedded JSON data or fallback to extracted context
|
|
132
|
+
let scannerFindings;
|
|
133
|
+
if (violationContext && violationContext.colorData) {
|
|
134
|
+
const colorData = violationContext.colorData;
|
|
135
|
+
console.log('Using color data from embedded JSON:', {
|
|
136
|
+
foreground: colorData.foreground,
|
|
137
|
+
background: colorData.background,
|
|
138
|
+
currentRatio: colorData.currentRatio,
|
|
139
|
+
requiredRatio: colorData.requiredRatio,
|
|
140
|
+
fontSize: colorData.fontSize,
|
|
141
|
+
fontWeight: colorData.fontWeight
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
scannerFindings = {
|
|
145
|
+
foreground: colorData.foreground || '#000000',
|
|
146
|
+
background: colorData.background || '#ffffff',
|
|
147
|
+
font_size_px: colorData.fontSize || 16,
|
|
148
|
+
font_weight: colorData.fontWeight || '400',
|
|
149
|
+
contrast_ratio: colorData.currentRatio || 1.0,
|
|
150
|
+
required_ratio: colorData.requiredRatio || 4.5,
|
|
151
|
+
actual_html: violationContext.html,
|
|
152
|
+
xpath_selector: violationContext.xpath
|
|
153
|
+
};
|
|
154
|
+
console.log('Using embedded JSON-based scanner findings with contrast ratio:', scannerFindings.contrast_ratio);
|
|
155
|
+
} else {
|
|
156
|
+
scannerFindings = {
|
|
157
|
+
foreground: elementContext?.foreground || '#000000',
|
|
158
|
+
background: elementContext?.background || '#ffffff',
|
|
159
|
+
font_size_px: elementContext?.fontSize || 16,
|
|
160
|
+
font_weight: elementContext?.fontWeight || '400',
|
|
161
|
+
contrast_ratio: elementContext?.contrastRatio || 1.0,
|
|
162
|
+
required_ratio: 4.5
|
|
163
|
+
};
|
|
164
|
+
console.log('Using fallback scanner findings:', scannerFindings);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const violationTypeMap = {
|
|
168
|
+
'color-contrast': 'color-contrast',
|
|
169
|
+
'oobee-accessible-label': 'accessible-label',
|
|
170
|
+
'image-alt': 'image-alt',
|
|
171
|
+
'listitem': 'listitem',
|
|
172
|
+
'target-size': 'target-size'
|
|
173
|
+
};
|
|
174
|
+
let violationType = violationTypeMap[ruleId] || ruleId;
|
|
175
|
+
|
|
176
|
+
// Prepare payload for wcag-eval API
|
|
177
|
+
const apiPayload = {
|
|
178
|
+
violationType: violationType,
|
|
179
|
+
html: html,
|
|
180
|
+
css: elementContext?.css || '',
|
|
181
|
+
violationContext: violationContext ? {
|
|
182
|
+
severity: violationContext.severity,
|
|
183
|
+
issue_description: violationContext.issueDescription,
|
|
184
|
+
wcag_conformance: violationContext.wcagConformance,
|
|
185
|
+
page_title: violationContext.pageTitle,
|
|
186
|
+
page_url: violationContext.url,
|
|
187
|
+
how_to_fix: violationContext.howToFix,
|
|
188
|
+
axe_impact: violationContext.axeImpact,
|
|
189
|
+
learn_more: violationContext.learnMore
|
|
190
|
+
} : {},
|
|
191
|
+
context: {
|
|
192
|
+
rule_id: ruleId,
|
|
193
|
+
timestamp: new Date().toISOString(),
|
|
194
|
+
data_source: violationContext ? 'json_scan_results' : 'dom_extraction'
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// Add scannerFindings for color-contrast violations
|
|
199
|
+
if (violationType === 'color-contrast') {
|
|
200
|
+
apiPayload.scannerFindings = scannerFindings;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
console.log('Sending request to wcag-eval API:', apiPayload);
|
|
204
|
+
|
|
205
|
+
// Call wcag-eval API (now in oobee-web-proxy)
|
|
206
|
+
const wcagEvalApiUrl = `${proxyUrl}/api/ai/generate-fix` || 'http://localhost:3002/api/ai/generate-fix';
|
|
207
|
+
const response = await fetch(wcagEvalApiUrl, {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: {
|
|
210
|
+
'Content-Type': 'application/json',
|
|
211
|
+
'Accept': 'application/json'
|
|
212
|
+
},
|
|
213
|
+
body: JSON.stringify(apiPayload)
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
if (!response.ok) {
|
|
217
|
+
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const apiResponse = await response.json();
|
|
221
|
+
console.log('Received response from wcag-eval API:', apiResponse);
|
|
222
|
+
|
|
223
|
+
if (!apiResponse.success) {
|
|
224
|
+
throw new Error(apiResponse.error || 'API returned unsuccessful response');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const fixSuggestion = apiResponse.fixSuggestion;
|
|
228
|
+
console.log('Fix suggestion data:', fixSuggestion);
|
|
229
|
+
console.log('Code fixes array:', fixSuggestion?.codeFixes);
|
|
230
|
+
|
|
231
|
+
// Format the AI response for display
|
|
232
|
+
const formattedResponse = formatFixSuggestionResponse(fixSuggestion);
|
|
233
|
+
|
|
234
|
+
// Display the generated fix
|
|
235
|
+
const responseContainer = document.getElementById(accordionDiv);
|
|
236
|
+
if (responseContainer) {
|
|
237
|
+
responseContainer.innerHTML = formattedResponse;
|
|
238
|
+
|
|
239
|
+
// Highlight code blocks
|
|
240
|
+
responseContainer.querySelectorAll('.codeForAiResponse').forEach(el => {
|
|
241
|
+
if (typeof hljs !== 'undefined') {
|
|
242
|
+
hljs.highlightElement(el);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Reset button to regenerate state
|
|
248
|
+
button.disabled = false;
|
|
249
|
+
button.style = 'border: 1px solid var(--oobee-blue-100); color: var(--oobee-blue-100);';
|
|
250
|
+
button.textContent = 'Regenerate Fix';
|
|
251
|
+
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.error('Error in Gen AI fix generation:', error);
|
|
254
|
+
|
|
255
|
+
// Reset button to initial state
|
|
256
|
+
button.disabled = false;
|
|
257
|
+
button.style = 'border: 1px solid var(--oobee-blue-100); color: var(--oobee-blue-100);';
|
|
258
|
+
button.textContent = 'Gen AI Suggest Fix';
|
|
259
|
+
|
|
260
|
+
if (errorContainer) {
|
|
261
|
+
const isConnectionError = error.message.includes('fetch') || error.message.includes('Failed to fetch');
|
|
262
|
+
const errorMessage = isConnectionError
|
|
263
|
+
? 'Unable to connect to AI service. Please ensure the wcag-eval API is running on port 5000.'
|
|
264
|
+
: `Failed to generate fix suggestion: ${error.message}`;
|
|
265
|
+
|
|
266
|
+
errorContainer.innerHTML = `<div class="generateAiError">${errorMessage}</div>`;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// Helper function to extract element context from HTML/CSS
|
|
272
|
+
function extractElementContext(html) {
|
|
273
|
+
const context = {
|
|
274
|
+
css: '',
|
|
275
|
+
foreground: '#000000',
|
|
276
|
+
background: '#ffffff',
|
|
277
|
+
fontSize: 16,
|
|
278
|
+
fontWeight: '400',
|
|
279
|
+
contrastRatio: 1.0
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
const tempDiv = document.createElement('div');
|
|
284
|
+
tempDiv.innerHTML = html;
|
|
285
|
+
const element = tempDiv.querySelector('*') || tempDiv;
|
|
286
|
+
|
|
287
|
+
if (element && element.parentNode) {
|
|
288
|
+
document.body.appendChild(tempDiv);
|
|
289
|
+
const computedStyle = window.getComputedStyle(element);
|
|
290
|
+
|
|
291
|
+
context.foreground = rgbToHex(computedStyle.color) || context.foreground;
|
|
292
|
+
context.background = rgbToHex(computedStyle.backgroundColor) || context.background;
|
|
293
|
+
context.fontSize = parseFloat(computedStyle.fontSize) || context.fontSize;
|
|
294
|
+
context.fontWeight = computedStyle.fontWeight || context.fontWeight;
|
|
295
|
+
|
|
296
|
+
if (element.style && element.style.cssText) {
|
|
297
|
+
context.css = element.style.cssText;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
document.body.removeChild(tempDiv);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (context.foreground && context.background) {
|
|
304
|
+
context.contrastRatio = calculateContrastRatio(context.foreground, context.background);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
} catch (error) {
|
|
308
|
+
console.warn('Could not extract element context:', error);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return context;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Format fix suggestion response for display
|
|
315
|
+
function formatFixSuggestionResponse(fixSuggestion) {
|
|
316
|
+
// Support both snake_case (Python) and camelCase (TypeScript) responses
|
|
317
|
+
const contrastAnalysis = fixSuggestion.contrast_analysis || fixSuggestion.contrastAnalysis || {};
|
|
318
|
+
const targetSizeAnalysis = fixSuggestion.target_size_analysis || fixSuggestion.targetSizeAnalysis || {};
|
|
319
|
+
const plainExplanation = fixSuggestion.why_it_matters || fixSuggestion.explanation || 'This fix improves accessibility for users with visual impairments.';
|
|
320
|
+
const diagnosis = fixSuggestion.diagnosis || fixSuggestion.explanation || plainExplanation;
|
|
321
|
+
|
|
322
|
+
// Escape HTML for safe rendering
|
|
323
|
+
const escapeHtml = (text) => String(text)
|
|
324
|
+
.replace(/&/g, '&')
|
|
325
|
+
.replace(/</g, '<')
|
|
326
|
+
.replace(/>/g, '>')
|
|
327
|
+
.replace(/"/g, '"')
|
|
328
|
+
.replace(/'/g, ''');
|
|
329
|
+
|
|
330
|
+
// Build code fixes section
|
|
331
|
+
let codeFixesHtml = '';
|
|
332
|
+
|
|
333
|
+
// Handle TypeScript format (css/html directly in response)
|
|
334
|
+
if (fixSuggestion.css || fixSuggestion.html) {
|
|
335
|
+
const mainFix = [];
|
|
336
|
+
if (fixSuggestion.css) {
|
|
337
|
+
mainFix.push(`
|
|
338
|
+
<div style="margin-bottom: 16px;">
|
|
339
|
+
<p style="font-size: 13px; margin-bottom: 8px; color: #666; font-weight: 500;">Recommended CSS Fix:</p>
|
|
340
|
+
<code class="codeForAiResponse language-css hljs" style="display: block; padding: 16px; background: #f8f9fa; border-radius: 6px; font-size: 13px; border: 1px solid #e0e0e0; white-space: pre-wrap; word-break: break-all;">${escapeHtml(fixSuggestion.css)}</code>
|
|
341
|
+
</div>
|
|
342
|
+
`);
|
|
343
|
+
}
|
|
344
|
+
if (fixSuggestion.html) {
|
|
345
|
+
mainFix.push(`
|
|
346
|
+
<div style="margin-bottom: 16px;">
|
|
347
|
+
<p style="font-size: 13px; margin-bottom: 8px; color: #666; font-weight: 500;">Recommended HTML Fix:</p>
|
|
348
|
+
<code class="codeForAiResponse language-html hljs" style="display: block; padding: 16px; background: #f8f9fa; border-radius: 6px; font-size: 13px; border: 1px solid #e0e0e0; white-space: pre-wrap; word-break: break-all;">${escapeHtml(fixSuggestion.html)}</code>
|
|
349
|
+
</div>
|
|
350
|
+
`);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Handle alternatives
|
|
354
|
+
if (fixSuggestion.alternatives && Array.isArray(fixSuggestion.alternatives) && fixSuggestion.alternatives.length > 0) {
|
|
355
|
+
const alternatives = fixSuggestion.alternatives.map((alt, index) => {
|
|
356
|
+
const altCode = alt.css || alt.html || '';
|
|
357
|
+
return `
|
|
358
|
+
<div style="margin-top: 16px; padding-top: 16px; border-top: 1px solid #e0e0e0;">
|
|
359
|
+
<p style="font-size: 13px; margin-bottom: 8px; color: #666; font-weight: 500;">Alternative ${index + 1}: ${escapeHtml(alt.explanation || '')}</p>
|
|
360
|
+
<code class="codeForAiResponse language-${alt.css ? 'css' : 'html'} hljs" style="display: block; padding: 16px; background: #f8f9fa; border-radius: 6px; font-size: 13px; border: 1px solid #e0e0e0; white-space: pre-wrap; word-break: break-all;">${escapeHtml(altCode)}</code>
|
|
361
|
+
</div>
|
|
362
|
+
`;
|
|
363
|
+
}).join('');
|
|
364
|
+
mainFix.push(alternatives);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
codeFixesHtml = `
|
|
368
|
+
<div style="margin-bottom: 16px;">
|
|
369
|
+
<h4 style="font-size: 16px; font-weight: 600; margin-bottom: 12px; color: #1a1a1a;">Code Fix:</h4>
|
|
370
|
+
${mainFix.join('')}
|
|
371
|
+
</div>
|
|
372
|
+
`;
|
|
373
|
+
}
|
|
374
|
+
// Handle Python format (code_fixes array)
|
|
375
|
+
else if (fixSuggestion.code_fixes && Array.isArray(fixSuggestion.code_fixes) && fixSuggestion.code_fixes.length > 0) {
|
|
376
|
+
const codeFixes = fixSuggestion.code_fixes.map((fix, index) => {
|
|
377
|
+
const language = fix.language || 'html';
|
|
378
|
+
const description = fix.description || `Option ${index + 1}`;
|
|
379
|
+
const code = fix.after || fix.before || '';
|
|
380
|
+
const marginBottom = index === fixSuggestion.code_fixes.length - 1 ? '0' : '20px';
|
|
381
|
+
|
|
382
|
+
return `
|
|
383
|
+
<div style="margin-bottom: ${marginBottom};">
|
|
384
|
+
<p style="font-size: 13px; margin-bottom: 8px; color: #666; font-weight: 500;">${escapeHtml(description)}</p>
|
|
385
|
+
<code class="codeForAiResponse language-${escapeHtml(language)} hljs" style="display: block; padding: 16px; background: #f8f9fa; border-radius: 6px; font-size: 13px; border: 1px solid #e0e0e0; white-space: pre-wrap; word-break: break-all;">${escapeHtml(code)}</code>
|
|
386
|
+
</div>`;
|
|
387
|
+
}).join('');
|
|
388
|
+
|
|
389
|
+
codeFixesHtml = `
|
|
390
|
+
<div style="margin-bottom: 16px;">
|
|
391
|
+
<h4 style="font-size: 16px; font-weight: 600; margin-bottom: 12px; color: #1a1a1a;">Code Fix${fixSuggestion.code_fixes.length > 1 ? 'es' : ''}:</h4>
|
|
392
|
+
${codeFixes}
|
|
393
|
+
</div>
|
|
394
|
+
`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return `
|
|
398
|
+
<div class="genai-response-card">
|
|
399
|
+
<div style="margin-bottom: 24px;">
|
|
400
|
+
<h4 style="font-size: 16px; font-weight: 600; margin-bottom: 12px; color: #1a1a1a;">What's the problem?</h4>
|
|
401
|
+
<p style="font-size: 14px; line-height: 1.6; margin-bottom: 16px; color: #333;">
|
|
402
|
+
${escapeHtml(diagnosis)}
|
|
403
|
+
</p>
|
|
404
|
+
|
|
405
|
+
${contrastAnalysis.current_ratio ? `
|
|
406
|
+
<div style="background: #FAF8FD; padding: 12px; border-radius: 6px; margin-bottom: 16px;">
|
|
407
|
+
<p style="margin: 0; font-size: 14px; line-height: 1.6; color: #333;">
|
|
408
|
+
The current text has a contrast ratio of <strong>${contrastAnalysis.current_ratio}:1</strong>,
|
|
409
|
+
but it needs at least <strong>${contrastAnalysis.required_ratio}:1</strong> to meet accessibility standards.
|
|
410
|
+
${contrastAnalysis.passes_aa
|
|
411
|
+
? `The suggested fix achieves <strong>${contrastAnalysis.proposed_ratio}:1</strong>, which passes WCAG AA requirements.`
|
|
412
|
+
: `The suggested fix improves it to <strong>${contrastAnalysis.proposed_ratio}:1</strong>.`}
|
|
413
|
+
</p>
|
|
414
|
+
</div>
|
|
415
|
+
` : ''}
|
|
416
|
+
|
|
417
|
+
${targetSizeAnalysis && (targetSizeAnalysis.estimated_width !== undefined || targetSizeAnalysis.estimated_height !== undefined || targetSizeAnalysis.notes) ? `
|
|
418
|
+
<div style="background: #F5FAFF; padding: 12px; border-radius: 6px; margin-bottom: 16px; border: 1px solid #e0efff;">
|
|
419
|
+
<p style="margin: 0; font-size: 14px; line-height: 1.6; color: #333;">
|
|
420
|
+
${targetSizeAnalysis.meets_minimum === false
|
|
421
|
+
? 'Estimated hit area is below the WCAG 2.5.8 minimum of 24x24 CSS pixels.'
|
|
422
|
+
: 'Target size analysis for this control:'}
|
|
423
|
+
${targetSizeAnalysis.estimated_width !== undefined && targetSizeAnalysis.estimated_width !== null
|
|
424
|
+
? ` Estimated width: <strong>${targetSizeAnalysis.estimated_width}${typeof targetSizeAnalysis.estimated_width === 'number' ? 'px' : ''}</strong>.`
|
|
425
|
+
: ''}
|
|
426
|
+
${targetSizeAnalysis.estimated_height !== undefined && targetSizeAnalysis.estimated_height !== null
|
|
427
|
+
? ` Estimated height: <strong>${targetSizeAnalysis.estimated_height}${typeof targetSizeAnalysis.estimated_height === 'number' ? 'px' : ''}</strong>.`
|
|
428
|
+
: ''}
|
|
429
|
+
${targetSizeAnalysis.notes ? ` ${escapeHtml(targetSizeAnalysis.notes)}` : ''}
|
|
430
|
+
</p>
|
|
431
|
+
</div>
|
|
432
|
+
` : ''}
|
|
433
|
+
|
|
434
|
+
${fixSuggestion.fix_steps && fixSuggestion.fix_steps.length > 0 ? `
|
|
435
|
+
<div style="margin-top: 16px;">
|
|
436
|
+
<h5 style="font-size: 14px; font-weight: 600; margin-bottom: 12px; color: #1a1a1a;">How to fix it:</h5>
|
|
437
|
+
<ol style="margin: 0; padding-left: 20px; font-size: 14px; line-height: 1.8; color: #333;">
|
|
438
|
+
${fixSuggestion.fix_steps.map(step => `<li style="word-wrap: break-word; overflow-wrap: break-word;">${escapeHtml(step)}</li>`).join('')}
|
|
439
|
+
</ol>
|
|
440
|
+
</div>
|
|
441
|
+
` : ''}
|
|
442
|
+
</div>
|
|
443
|
+
|
|
444
|
+
${codeFixesHtml}
|
|
445
|
+
</div>
|
|
446
|
+
`;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Convert RGB color to hex format
|
|
450
|
+
function rgbToHex(rgb) {
|
|
451
|
+
if (!rgb || rgb === 'transparent' || rgb === 'inherit') return null;
|
|
452
|
+
|
|
453
|
+
const result = rgb.match(/\d+/g);
|
|
454
|
+
if (!result || result.length < 3) return null;
|
|
455
|
+
|
|
456
|
+
const r = parseInt(result[0]);
|
|
457
|
+
const g = parseInt(result[1]);
|
|
458
|
+
const b = parseInt(result[2]);
|
|
459
|
+
|
|
460
|
+
return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Calculate WCAG contrast ratio between two colors
|
|
464
|
+
function calculateContrastRatio(color1, color2) {
|
|
465
|
+
try {
|
|
466
|
+
const getLuminance = (hex) => {
|
|
467
|
+
const rgb = hex.match(/\w\w/g).map(h => parseInt(h, 16) / 255);
|
|
468
|
+
const [r, g, b] = rgb.map(c => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
|
|
469
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const l1 = getLuminance(color1.replace('#', ''));
|
|
473
|
+
const l2 = getLuminance(color2.replace('#', ''));
|
|
474
|
+
const lightest = Math.max(l1, l2);
|
|
475
|
+
const darkest = Math.min(l1, l2);
|
|
476
|
+
|
|
477
|
+
return Math.round(((lightest + 0.05) / (darkest + 0.05)) * 100) / 100;
|
|
478
|
+
} catch (error) {
|
|
479
|
+
console.warn('Could not calculate contrast ratio:', error);
|
|
480
|
+
return 1.0;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
</script>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
document.addEventListener('click', function (e) {
|
|
3
|
+
const btn = e.target.closest('.seg-pill');
|
|
4
|
+
if (!btn) return;
|
|
5
|
+
|
|
6
|
+
const container = btn.closest('.segmented-tabs');
|
|
7
|
+
const targetSel = btn.getAttribute('data-tab-target');
|
|
8
|
+
const panel = document.querySelector(targetSel);
|
|
9
|
+
if (!panel) return;
|
|
10
|
+
|
|
11
|
+
container.querySelectorAll('.seg-pill').forEach((p) =>
|
|
12
|
+
p.setAttribute('aria-selected', 'false')
|
|
13
|
+
);
|
|
14
|
+
btn.setAttribute('aria-selected', 'true');
|
|
15
|
+
|
|
16
|
+
document
|
|
17
|
+
.querySelectorAll('.seg-panels > [role="tabpanel"]')
|
|
18
|
+
.forEach((p) => (p.hidden = true));
|
|
19
|
+
panel.hidden = false;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
window.addEventListener('DOMContentLoaded', () => {
|
|
23
|
+
const pairs = [
|
|
24
|
+
['#seg-not-scanned', '#totalPagesNotScannedLabel'],
|
|
25
|
+
['#seg-unsupported', '#totalUnsupportedDocsLabel']
|
|
26
|
+
];
|
|
27
|
+
pairs.forEach(([btnSel, countSel]) => {
|
|
28
|
+
const btn = document.querySelector(btnSel);
|
|
29
|
+
const countEl = document.querySelector(countSel);
|
|
30
|
+
if (btn && countEl && countEl.textContent.trim() === '0') {
|
|
31
|
+
btn.style.display = 'none';
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
</script>
|
|
@@ -1,71 +1,75 @@
|
|
|
1
1
|
<%# functions used to show lightbox of screenshot when thumbnail is clicked on %>
|
|
2
2
|
<script>
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
lightbox.addEventListener('click', event => {
|
|
24
|
+
if (
|
|
25
|
+
event.target === lightbox ||
|
|
26
|
+
event.target === lightboxHeader ||
|
|
27
|
+
event.target === lightboxTitle
|
|
28
|
+
) {
|
|
29
|
+
closeLightbox();
|
|
30
|
+
}
|
|
31
|
+
});
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
const offcanvasElem = document.getElementsByClassName('offcanvas')[0];
|
|
34
|
+
const offcanvasItem = new bootstrap.Offcanvas(offcanvasElem);
|
|
35
|
+
offcanvasItem._config.keyboard = false; // Disable default keyboard handling
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
const pagesScannedModalElem = document.getElementById('pagesScannedModal');
|
|
38
|
+
const pagesScannedModalItem = new bootstrap.Modal(pagesScannedModalElem);
|
|
39
|
+
pagesScannedModalItem._config.keyboard = false;
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
41
|
+
document.addEventListener('keydown', event => {
|
|
42
|
+
if (event.key === 'Escape') {
|
|
43
|
+
if (offcanvasItem._isShown) {
|
|
44
|
+
if (lightbox.style.display === 'block') {
|
|
45
|
+
event.preventDefault(); // Prevent default bootstrap behaviour
|
|
46
|
+
closeLightbox();
|
|
47
|
+
} else {
|
|
48
|
+
offcanvasItem.hide();
|
|
46
49
|
}
|
|
50
|
+
}
|
|
47
51
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
52
|
+
if (pagesScannedModalItem._isShown) {
|
|
53
|
+
if (lightbox.style.display === 'block') {
|
|
54
|
+
event.preventDefault(); // Prevent default bootstrap behaviour
|
|
55
|
+
closeLightbox();
|
|
56
|
+
} else {
|
|
57
|
+
pagesScannedModalItem.hide();
|
|
55
58
|
}
|
|
56
59
|
}
|
|
57
|
-
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
58
62
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
lightboxImg.src = imgSrc;
|
|
63
|
-
lightboxImg.alt = `Screenshot of ${pageUrl}`;
|
|
63
|
+
function openLightbox(imgSrc, pageTitle, pageUrl) {
|
|
64
|
+
lightbox.style.display = 'block';
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
}
|
|
66
|
+
lightboxImg.src = imgSrc;
|
|
67
|
+
lightboxImg.alt = `Screenshot of ${pageUrl}`;
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
lightboxTitle.textContent = pageTitle;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function closeLightbox() {
|
|
73
|
+
lightbox.style.display = 'none';
|
|
74
|
+
};
|
|
75
|
+
</script>
|