gbu-accessibility-package 3.3.0 โ 3.5.0
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/CHANGELOG.md +19 -0
- package/README-vi.md +12 -3
- package/README.md +12 -3
- package/cli.js +78 -11
- package/demo/heading-structure-test.html +60 -0
- package/demo/nested-controls-test.html +92 -0
- package/lib/fixer.js +995 -0
- package/package.json +1 -1
package/lib/fixer.js
CHANGED
|
@@ -1236,6 +1236,9 @@ class AccessibilityFixer {
|
|
|
1236
1236
|
altCreativity: config.altCreativity || 'balanced', // conservative, balanced, creative
|
|
1237
1237
|
includeEmotions: config.includeEmotions || false,
|
|
1238
1238
|
strictAltChecking: config.strictAltChecking || false,
|
|
1239
|
+
// New options for advanced features
|
|
1240
|
+
autoFixHeadings: config.autoFixHeadings || false, // Enable automatic heading fixes
|
|
1241
|
+
fixDescriptionLists: config.fixDescriptionLists || true, // Enable DL structure fixes
|
|
1239
1242
|
...config
|
|
1240
1243
|
};
|
|
1241
1244
|
|
|
@@ -4093,6 +4096,998 @@ class AccessibilityFixer {
|
|
|
4093
4096
|
return fixed;
|
|
4094
4097
|
}
|
|
4095
4098
|
|
|
4099
|
+
async fixNestedInteractiveControls(directory = '.') {
|
|
4100
|
+
console.log(chalk.blue('๐ฏ Fixing nested interactive controls...'));
|
|
4101
|
+
|
|
4102
|
+
const htmlFiles = await this.findHtmlFiles(directory);
|
|
4103
|
+
const results = [];
|
|
4104
|
+
let totalIssuesFound = 0;
|
|
4105
|
+
|
|
4106
|
+
for (const file of htmlFiles) {
|
|
4107
|
+
try {
|
|
4108
|
+
const content = await fs.readFile(file, 'utf8');
|
|
4109
|
+
const issues = this.analyzeNestedInteractiveControls(content);
|
|
4110
|
+
|
|
4111
|
+
if (issues.length > 0) {
|
|
4112
|
+
console.log(chalk.cyan(`\n๐ ${file}:`));
|
|
4113
|
+
issues.forEach(issue => {
|
|
4114
|
+
console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
|
|
4115
|
+
if (issue.suggestion) {
|
|
4116
|
+
console.log(chalk.gray(` ๐ก ${issue.suggestion}`));
|
|
4117
|
+
}
|
|
4118
|
+
totalIssuesFound++;
|
|
4119
|
+
});
|
|
4120
|
+
}
|
|
4121
|
+
|
|
4122
|
+
const fixed = this.fixNestedInteractiveControlsInContent(content);
|
|
4123
|
+
|
|
4124
|
+
if (fixed !== content) {
|
|
4125
|
+
if (this.config.backupFiles) {
|
|
4126
|
+
await fs.writeFile(`${file}.backup`, content);
|
|
4127
|
+
}
|
|
4128
|
+
|
|
4129
|
+
if (!this.config.dryRun) {
|
|
4130
|
+
await fs.writeFile(file, fixed);
|
|
4131
|
+
}
|
|
4132
|
+
|
|
4133
|
+
console.log(chalk.green(`โ
Fixed nested interactive controls in: ${file}`));
|
|
4134
|
+
results.push({ file, status: 'fixed', issues: issues.length });
|
|
4135
|
+
} else {
|
|
4136
|
+
results.push({ file, status: 'no-change', issues: issues.length });
|
|
4137
|
+
}
|
|
4138
|
+
} catch (error) {
|
|
4139
|
+
console.error(chalk.red(`โ Error processing ${file}: ${error.message}`));
|
|
4140
|
+
results.push({ file, status: 'error', error: error.message });
|
|
4141
|
+
}
|
|
4142
|
+
}
|
|
4143
|
+
|
|
4144
|
+
console.log(chalk.blue(`\n๐ Summary: Found ${totalIssuesFound} nested interactive control issues across ${results.length} files`));
|
|
4145
|
+
return results;
|
|
4146
|
+
}
|
|
4147
|
+
|
|
4148
|
+
analyzeNestedInteractiveControls(content) {
|
|
4149
|
+
const issues = [];
|
|
4150
|
+
|
|
4151
|
+
// Define interactive elements and their roles
|
|
4152
|
+
const interactiveElements = [
|
|
4153
|
+
{ tag: 'button', role: 'button' },
|
|
4154
|
+
{ tag: 'a', role: 'link', requiresHref: true },
|
|
4155
|
+
{ tag: 'input', role: 'textbox|button|checkbox|radio|slider|spinbutton' },
|
|
4156
|
+
{ tag: 'textarea', role: 'textbox' },
|
|
4157
|
+
{ tag: 'select', role: 'combobox|listbox' },
|
|
4158
|
+
{ tag: 'details', role: 'group' },
|
|
4159
|
+
{ tag: 'summary', role: 'button' }
|
|
4160
|
+
];
|
|
4161
|
+
|
|
4162
|
+
// Also check for elements with interactive roles
|
|
4163
|
+
const interactiveRoles = [
|
|
4164
|
+
'button', 'link', 'textbox', 'checkbox', 'radio', 'slider',
|
|
4165
|
+
'spinbutton', 'combobox', 'listbox', 'menuitem', 'tab',
|
|
4166
|
+
'treeitem', 'gridcell', 'option'
|
|
4167
|
+
];
|
|
4168
|
+
|
|
4169
|
+
// Find all interactive elements
|
|
4170
|
+
const interactiveSelectors = [];
|
|
4171
|
+
|
|
4172
|
+
// Add tag-based selectors
|
|
4173
|
+
interactiveElements.forEach(element => {
|
|
4174
|
+
if (element.requiresHref) {
|
|
4175
|
+
interactiveSelectors.push(`<${element.tag}[^>]*href[^>]*>`);
|
|
4176
|
+
} else {
|
|
4177
|
+
interactiveSelectors.push(`<${element.tag}[^>]*>`);
|
|
4178
|
+
}
|
|
4179
|
+
});
|
|
4180
|
+
|
|
4181
|
+
// Add role-based selectors
|
|
4182
|
+
interactiveRoles.forEach(role => {
|
|
4183
|
+
interactiveSelectors.push(`<[^>]*role\\s*=\\s*["']${role}["'][^>]*>`);
|
|
4184
|
+
});
|
|
4185
|
+
|
|
4186
|
+
// Create combined regex pattern
|
|
4187
|
+
const interactivePattern = new RegExp(interactiveSelectors.join('|'), 'gi');
|
|
4188
|
+
|
|
4189
|
+
// Find all interactive elements with their positions
|
|
4190
|
+
const interactiveMatches = [];
|
|
4191
|
+
let match;
|
|
4192
|
+
|
|
4193
|
+
while ((match = interactivePattern.exec(content)) !== null) {
|
|
4194
|
+
const element = match[0];
|
|
4195
|
+
const startPos = match.index;
|
|
4196
|
+
const endPos = this.findElementEndPosition(content, element, startPos);
|
|
4197
|
+
|
|
4198
|
+
if (endPos > startPos) {
|
|
4199
|
+
interactiveMatches.push({
|
|
4200
|
+
element: element,
|
|
4201
|
+
startPos: startPos,
|
|
4202
|
+
endPos: endPos,
|
|
4203
|
+
fullElement: content.substring(startPos, endPos)
|
|
4204
|
+
});
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4207
|
+
|
|
4208
|
+
// Check for nesting
|
|
4209
|
+
for (let i = 0; i < interactiveMatches.length; i++) {
|
|
4210
|
+
const parent = interactiveMatches[i];
|
|
4211
|
+
|
|
4212
|
+
for (let j = 0; j < interactiveMatches.length; j++) {
|
|
4213
|
+
if (i === j) continue;
|
|
4214
|
+
|
|
4215
|
+
const child = interactiveMatches[j];
|
|
4216
|
+
|
|
4217
|
+
// Check if child is nested inside parent
|
|
4218
|
+
if (child.startPos > parent.startPos && child.endPos < parent.endPos) {
|
|
4219
|
+
const parentType = this.getInteractiveElementType(parent.element);
|
|
4220
|
+
const childType = this.getInteractiveElementType(child.element);
|
|
4221
|
+
|
|
4222
|
+
issues.push({
|
|
4223
|
+
type: '๐ฏ Nested interactive controls',
|
|
4224
|
+
description: `${childType} is nested inside ${parentType}`,
|
|
4225
|
+
parentElement: parent.element.substring(0, 100) + '...',
|
|
4226
|
+
childElement: child.element.substring(0, 100) + '...',
|
|
4227
|
+
suggestion: `Remove interactive role from parent or child, or restructure HTML to avoid nesting`
|
|
4228
|
+
});
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
4231
|
+
}
|
|
4232
|
+
|
|
4233
|
+
return issues;
|
|
4234
|
+
}
|
|
4235
|
+
|
|
4236
|
+
findElementEndPosition(content, startTag, startPos) {
|
|
4237
|
+
// Extract tag name from start tag
|
|
4238
|
+
const tagMatch = startTag.match(/<(\w+)/);
|
|
4239
|
+
if (!tagMatch) return startPos + startTag.length;
|
|
4240
|
+
|
|
4241
|
+
const tagName = tagMatch[1].toLowerCase();
|
|
4242
|
+
|
|
4243
|
+
// Self-closing tags
|
|
4244
|
+
if (startTag.endsWith('/>') || ['input', 'img', 'br', 'hr', 'meta', 'link'].includes(tagName)) {
|
|
4245
|
+
return startPos + startTag.length;
|
|
4246
|
+
}
|
|
4247
|
+
|
|
4248
|
+
// Find matching closing tag
|
|
4249
|
+
const closeTagPattern = new RegExp(`</${tagName}>`, 'i');
|
|
4250
|
+
const remainingContent = content.substring(startPos + startTag.length);
|
|
4251
|
+
const closeMatch = remainingContent.match(closeTagPattern);
|
|
4252
|
+
|
|
4253
|
+
if (closeMatch) {
|
|
4254
|
+
return startPos + startTag.length + closeMatch.index + closeMatch[0].length;
|
|
4255
|
+
}
|
|
4256
|
+
|
|
4257
|
+
// If no closing tag found, assume it ends at the start tag
|
|
4258
|
+
return startPos + startTag.length;
|
|
4259
|
+
}
|
|
4260
|
+
|
|
4261
|
+
getInteractiveElementType(element) {
|
|
4262
|
+
// Extract tag name
|
|
4263
|
+
const tagMatch = element.match(/<(\w+)/);
|
|
4264
|
+
const tagName = tagMatch ? tagMatch[1].toLowerCase() : 'element';
|
|
4265
|
+
|
|
4266
|
+
// Extract role if present
|
|
4267
|
+
const roleMatch = element.match(/role\s*=\s*["']([^"']+)["']/i);
|
|
4268
|
+
const role = roleMatch ? roleMatch[1] : null;
|
|
4269
|
+
|
|
4270
|
+
if (role) {
|
|
4271
|
+
return `${tagName}[role="${role}"]`;
|
|
4272
|
+
}
|
|
4273
|
+
|
|
4274
|
+
// Special cases
|
|
4275
|
+
if (tagName === 'a' && /href\s*=/i.test(element)) {
|
|
4276
|
+
return 'link';
|
|
4277
|
+
}
|
|
4278
|
+
|
|
4279
|
+
if (tagName === 'input') {
|
|
4280
|
+
const typeMatch = element.match(/type\s*=\s*["']([^"']+)["']/i);
|
|
4281
|
+
const inputType = typeMatch ? typeMatch[1] : 'text';
|
|
4282
|
+
return `input[type="${inputType}"]`;
|
|
4283
|
+
}
|
|
4284
|
+
|
|
4285
|
+
return tagName;
|
|
4286
|
+
}
|
|
4287
|
+
|
|
4288
|
+
fixNestedInteractiveControlsInContent(content) {
|
|
4289
|
+
let fixed = content;
|
|
4290
|
+
|
|
4291
|
+
// Strategy 1: Remove role attributes from parent containers that have interactive children
|
|
4292
|
+
const issues = this.analyzeNestedInteractiveControls(content);
|
|
4293
|
+
|
|
4294
|
+
issues.forEach(issue => {
|
|
4295
|
+
// Try to fix by removing role from parent element
|
|
4296
|
+
const parentRoleMatch = issue.parentElement.match(/role\s*=\s*["'][^"']*["']/i);
|
|
4297
|
+
if (parentRoleMatch) {
|
|
4298
|
+
const parentWithoutRole = issue.parentElement.replace(/\s*role\s*=\s*["'][^"']*["']/i, '');
|
|
4299
|
+
fixed = fixed.replace(issue.parentElement, parentWithoutRole);
|
|
4300
|
+
console.log(chalk.yellow(` ๐ฏ Removed role attribute from parent element to fix nesting`));
|
|
4301
|
+
}
|
|
4302
|
+
});
|
|
4303
|
+
|
|
4304
|
+
// Strategy 2: Convert div[role="button"] containing links to regular div
|
|
4305
|
+
fixed = fixed.replace(/<div([^>]*role\s*=\s*["']button["'][^>]*)>([\s\S]*?)<\/div>/gi, (match, attributes, content) => {
|
|
4306
|
+
// Check if content contains interactive elements
|
|
4307
|
+
const hasInteractiveChildren = /<(?:a\s[^>]*href|button|input|select|textarea)[^>]*>/i.test(content);
|
|
4308
|
+
|
|
4309
|
+
if (hasInteractiveChildren) {
|
|
4310
|
+
// Remove role="button" and any button-related attributes
|
|
4311
|
+
const cleanAttributes = attributes
|
|
4312
|
+
.replace(/\s*role\s*=\s*["']button["']/i, '')
|
|
4313
|
+
.replace(/\s*tabindex\s*=\s*["'][^"']*["']/i, '')
|
|
4314
|
+
.replace(/\s*onclick\s*=\s*["'][^"']*["']/i, '');
|
|
4315
|
+
|
|
4316
|
+
console.log(chalk.yellow(` ๐ฏ Converted div[role="button"] to regular div due to interactive children`));
|
|
4317
|
+
return `<div${cleanAttributes}>${content}</div>`;
|
|
4318
|
+
}
|
|
4319
|
+
|
|
4320
|
+
return match;
|
|
4321
|
+
});
|
|
4322
|
+
|
|
4323
|
+
// Strategy 3: Remove tabindex from parent containers with interactive children
|
|
4324
|
+
fixed = fixed.replace(/(<[^>]+)(\s+tabindex\s*=\s*["'][^"']*["'])([^>]*>[\s\S]*?<(?:a\s[^>]*href|button|input|select|textarea)[^>]*>[\s\S]*?<\/[^>]+>)/gi, (match, beforeTabindex, tabindexAttr, afterTabindex) => {
|
|
4325
|
+
console.log(chalk.yellow(` ๐ฏ Removed tabindex from parent element with interactive children`));
|
|
4326
|
+
return beforeTabindex + afterTabindex;
|
|
4327
|
+
});
|
|
4328
|
+
|
|
4329
|
+
return fixed;
|
|
4330
|
+
}
|
|
4331
|
+
|
|
4332
|
+
async fixAllAccessibilityIssues(directory = '.') {
|
|
4333
|
+
console.log(chalk.blue('๐ Starting comprehensive accessibility fixes...'));
|
|
4334
|
+
console.log('');
|
|
4335
|
+
|
|
4336
|
+
const results = {
|
|
4337
|
+
totalFiles: 0,
|
|
4338
|
+
fixedFiles: 0,
|
|
4339
|
+
totalIssues: 0,
|
|
4340
|
+
steps: []
|
|
4341
|
+
};
|
|
4342
|
+
|
|
4343
|
+
try {
|
|
4344
|
+
// Step 1: HTML lang attributes
|
|
4345
|
+
console.log(chalk.blue('๐ Step 1: HTML lang attributes...'));
|
|
4346
|
+
const langResults = await this.fixHtmlLang(directory);
|
|
4347
|
+
const langFixed = langResults.filter(r => r.status === 'fixed').length;
|
|
4348
|
+
results.steps.push({ step: 1, name: 'HTML lang attributes', fixed: langFixed });
|
|
4349
|
+
|
|
4350
|
+
// Step 2: Alt attributes
|
|
4351
|
+
console.log(chalk.blue('๐ผ๏ธ Step 2: Alt attributes...'));
|
|
4352
|
+
const altResults = await this.fixEmptyAltAttributes(directory);
|
|
4353
|
+
const altFixed = altResults.filter(r => r.status === 'fixed').length;
|
|
4354
|
+
const totalAltIssues = altResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4355
|
+
results.steps.push({ step: 2, name: 'Alt attributes', fixed: altFixed, issues: totalAltIssues });
|
|
4356
|
+
|
|
4357
|
+
// Step 3: Role attributes
|
|
4358
|
+
console.log(chalk.blue('๐ญ Step 3: Role attributes...'));
|
|
4359
|
+
const roleResults = await this.fixRoleAttributes(directory);
|
|
4360
|
+
const roleFixed = roleResults.filter(r => r.status === 'fixed').length;
|
|
4361
|
+
const totalRoleIssues = roleResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4362
|
+
results.steps.push({ step: 3, name: 'Role attributes', fixed: roleFixed, issues: totalRoleIssues });
|
|
4363
|
+
|
|
4364
|
+
// Step 4: Form labels
|
|
4365
|
+
console.log(chalk.blue('๐ Step 4: Form labels...'));
|
|
4366
|
+
const formResults = await this.fixFormLabels(directory);
|
|
4367
|
+
const formFixed = formResults.filter(r => r.status === 'fixed').length;
|
|
4368
|
+
const totalFormIssues = formResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4369
|
+
results.steps.push({ step: 4, name: 'Form labels', fixed: formFixed, issues: totalFormIssues });
|
|
4370
|
+
|
|
4371
|
+
// Step 5: Nested interactive controls (NEW!)
|
|
4372
|
+
console.log(chalk.blue('๐ฏ Step 5: Nested interactive controls...'));
|
|
4373
|
+
const nestedResults = await this.fixNestedInteractiveControls(directory);
|
|
4374
|
+
const nestedFixed = nestedResults.filter(r => r.status === 'fixed').length;
|
|
4375
|
+
const totalNestedIssues = nestedResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4376
|
+
results.steps.push({ step: 5, name: 'Nested interactive controls', fixed: nestedFixed, issues: totalNestedIssues });
|
|
4377
|
+
|
|
4378
|
+
// Step 6: Button names
|
|
4379
|
+
console.log(chalk.blue('๐ Step 6: Button names...'));
|
|
4380
|
+
const buttonResults = await this.fixButtonNames(directory);
|
|
4381
|
+
const buttonFixed = buttonResults.filter(r => r.status === 'fixed').length;
|
|
4382
|
+
const totalButtonIssues = buttonResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4383
|
+
results.steps.push({ step: 6, name: 'Button names', fixed: buttonFixed, issues: totalButtonIssues });
|
|
4384
|
+
|
|
4385
|
+
// Step 7: Link names
|
|
4386
|
+
console.log(chalk.blue('๐ Step 7: Link names...'));
|
|
4387
|
+
const linkResults = await this.fixLinkNames(directory);
|
|
4388
|
+
const linkFixed = linkResults.filter(r => r.status === 'fixed').length;
|
|
4389
|
+
const totalLinkIssues = linkResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4390
|
+
results.steps.push({ step: 7, name: 'Link names', fixed: linkFixed, issues: totalLinkIssues });
|
|
4391
|
+
|
|
4392
|
+
// Step 8: Landmarks
|
|
4393
|
+
console.log(chalk.blue('๐๏ธ Step 8: Landmarks...'));
|
|
4394
|
+
const landmarkResults = await this.fixLandmarks(directory);
|
|
4395
|
+
const landmarkFixed = landmarkResults.filter(r => r.status === 'fixed').length;
|
|
4396
|
+
const totalLandmarkIssues = landmarkResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4397
|
+
results.steps.push({ step: 8, name: 'Landmarks', fixed: landmarkFixed, issues: totalLandmarkIssues });
|
|
4398
|
+
|
|
4399
|
+
// Step 9: Heading analysis
|
|
4400
|
+
console.log(chalk.blue('๐ Step 9: Heading analysis...'));
|
|
4401
|
+
const headingResults = await this.analyzeHeadings(directory);
|
|
4402
|
+
const totalHeadingSuggestions = headingResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4403
|
+
results.steps.push({ step: 9, name: 'Heading analysis', suggestions: totalHeadingSuggestions });
|
|
4404
|
+
console.log(chalk.gray('๐ก Heading issues require manual review and cannot be auto-fixed'));
|
|
4405
|
+
|
|
4406
|
+
// Step 10: Broken links check
|
|
4407
|
+
console.log(chalk.blue('๐ Step 10: Broken links check...'));
|
|
4408
|
+
const brokenLinksResults = await this.checkBrokenLinks(directory);
|
|
4409
|
+
const totalBrokenLinks = brokenLinksResults.reduce((sum, r) => sum + (r.issues || 0), 0);
|
|
4410
|
+
results.steps.push({ step: 10, name: 'Broken links check', issues: totalBrokenLinks });
|
|
4411
|
+
console.log(chalk.gray('๐ก Broken link issues require manual review and cannot be auto-fixed'));
|
|
4412
|
+
|
|
4413
|
+
// Step 11: Cleanup duplicate roles
|
|
4414
|
+
console.log(chalk.blue('๐งน Step 11: Cleanup duplicate roles...'));
|
|
4415
|
+
const cleanupResults = await this.cleanupDuplicateRoles(directory);
|
|
4416
|
+
const cleanupFixed = cleanupResults.filter(r => r.status === 'fixed').length;
|
|
4417
|
+
results.steps.push({ step: 11, name: 'Cleanup duplicate roles', fixed: cleanupFixed });
|
|
4418
|
+
|
|
4419
|
+
// Calculate totals
|
|
4420
|
+
results.totalFiles = Math.max(
|
|
4421
|
+
langResults.length, altResults.length, roleResults.length, formResults.length,
|
|
4422
|
+
nestedResults.length, buttonResults.length, linkResults.length, landmarkResults.length,
|
|
4423
|
+
headingResults.length, brokenLinksResults.length, cleanupResults.length
|
|
4424
|
+
);
|
|
4425
|
+
|
|
4426
|
+
results.fixedFiles = new Set([
|
|
4427
|
+
...langResults.filter(r => r.status === 'fixed').map(r => r.file),
|
|
4428
|
+
...altResults.filter(r => r.status === 'fixed').map(r => r.file),
|
|
4429
|
+
...roleResults.filter(r => r.status === 'fixed').map(r => r.file),
|
|
4430
|
+
...formResults.filter(r => r.status === 'fixed').map(r => r.file),
|
|
4431
|
+
...nestedResults.filter(r => r.status === 'fixed').map(r => r.file),
|
|
4432
|
+
...buttonResults.filter(r => r.status === 'fixed').map(r => r.file),
|
|
4433
|
+
...linkResults.filter(r => r.status === 'fixed').map(r => r.file),
|
|
4434
|
+
...landmarkResults.filter(r => r.status === 'fixed').map(r => r.file),
|
|
4435
|
+
...cleanupResults.filter(r => r.status === 'fixed').map(r => r.file)
|
|
4436
|
+
]).size;
|
|
4437
|
+
|
|
4438
|
+
results.totalIssues = totalAltIssues + totalRoleIssues + totalFormIssues + totalNestedIssues +
|
|
4439
|
+
totalButtonIssues + totalLinkIssues + totalLandmarkIssues;
|
|
4440
|
+
|
|
4441
|
+
// Final summary
|
|
4442
|
+
console.log(chalk.green('\n๐ All accessibility fixes completed!'));
|
|
4443
|
+
console.log(chalk.blue('๐ Final Summary:'));
|
|
4444
|
+
console.log(chalk.blue(` Total files scanned: ${results.totalFiles}`));
|
|
4445
|
+
console.log(chalk.blue(` Files fixed: ${results.fixedFiles}`));
|
|
4446
|
+
console.log(chalk.blue(` Total issues resolved: ${results.totalIssues}`));
|
|
4447
|
+
|
|
4448
|
+
if (this.config.dryRun) {
|
|
4449
|
+
console.log(chalk.yellow('\n๐ก This was a dry run. Use without --dry-run to apply changes.'));
|
|
4450
|
+
}
|
|
4451
|
+
|
|
4452
|
+
return results;
|
|
4453
|
+
|
|
4454
|
+
} catch (error) {
|
|
4455
|
+
console.error(chalk.red(`โ Error during comprehensive fixes: ${error.message}`));
|
|
4456
|
+
throw error;
|
|
4457
|
+
}
|
|
4458
|
+
}
|
|
4459
|
+
|
|
4460
|
+
async fixHeadingStructure(directory = '.') {
|
|
4461
|
+
console.log(chalk.blue('๐ Fixing heading structure...'));
|
|
4462
|
+
|
|
4463
|
+
const htmlFiles = await this.findHtmlFiles(directory);
|
|
4464
|
+
const results = [];
|
|
4465
|
+
let totalIssuesFound = 0;
|
|
4466
|
+
let totalIssuesFixed = 0;
|
|
4467
|
+
|
|
4468
|
+
for (const file of htmlFiles) {
|
|
4469
|
+
try {
|
|
4470
|
+
const content = await fs.readFile(file, 'utf8');
|
|
4471
|
+
const analysis = this.analyzeHeadingStructure(content);
|
|
4472
|
+
|
|
4473
|
+
if (analysis.issues.length > 0) {
|
|
4474
|
+
console.log(chalk.cyan(`\n๐ ${file}:`));
|
|
4475
|
+
analysis.issues.forEach(issue => {
|
|
4476
|
+
console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
|
|
4477
|
+
if (issue.suggestion) {
|
|
4478
|
+
console.log(chalk.gray(` ๐ก ${issue.suggestion}`));
|
|
4479
|
+
}
|
|
4480
|
+
totalIssuesFound++;
|
|
4481
|
+
});
|
|
4482
|
+
}
|
|
4483
|
+
|
|
4484
|
+
let fixed = content;
|
|
4485
|
+
let changesMade = false;
|
|
4486
|
+
|
|
4487
|
+
if (this.config.autoFixHeadings) {
|
|
4488
|
+
fixed = this.fixHeadingStructureInContent(content, analysis);
|
|
4489
|
+
changesMade = fixed !== content;
|
|
4490
|
+
|
|
4491
|
+
if (changesMade) {
|
|
4492
|
+
const fixedCount = this.countHeadingFixes(content, fixed);
|
|
4493
|
+
totalIssuesFixed += fixedCount;
|
|
4494
|
+
|
|
4495
|
+
if (this.config.backupFiles) {
|
|
4496
|
+
await fs.writeFile(`${file}.backup`, content);
|
|
4497
|
+
}
|
|
4498
|
+
|
|
4499
|
+
if (!this.config.dryRun) {
|
|
4500
|
+
await fs.writeFile(file, fixed);
|
|
4501
|
+
}
|
|
4502
|
+
|
|
4503
|
+
console.log(chalk.green(`โ
Fixed heading structure in: ${file} (${fixedCount} fixes)`));
|
|
4504
|
+
results.push({ file, status: 'fixed', issues: analysis.issues.length, fixes: fixedCount });
|
|
4505
|
+
} else {
|
|
4506
|
+
results.push({ file, status: 'no-change', issues: analysis.issues.length });
|
|
4507
|
+
}
|
|
4508
|
+
} else {
|
|
4509
|
+
results.push({ file, status: 'analyzed', issues: analysis.issues.length });
|
|
4510
|
+
}
|
|
4511
|
+
} catch (error) {
|
|
4512
|
+
console.error(chalk.red(`โ Error processing ${file}: ${error.message}`));
|
|
4513
|
+
results.push({ file, status: 'error', error: error.message });
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
|
|
4517
|
+
console.log(chalk.blue(`\n๐ Summary: Found ${totalIssuesFound} heading issues across ${results.length} files`));
|
|
4518
|
+
if (this.config.autoFixHeadings) {
|
|
4519
|
+
console.log(chalk.green(` Fixed ${totalIssuesFixed} heading issues automatically`));
|
|
4520
|
+
} else {
|
|
4521
|
+
console.log(chalk.gray('๐ก Use --auto-fix-headings option to enable automatic fixes'));
|
|
4522
|
+
}
|
|
4523
|
+
|
|
4524
|
+
return results;
|
|
4525
|
+
}
|
|
4526
|
+
|
|
4527
|
+
analyzeHeadingStructure(content) {
|
|
4528
|
+
const issues = [];
|
|
4529
|
+
const suggestions = [];
|
|
4530
|
+
|
|
4531
|
+
// Extract all headings with their levels, text, and positions
|
|
4532
|
+
const headingPattern = /<h([1-6])[^>]*>([\s\S]*?)<\/h[1-6]>/gi;
|
|
4533
|
+
const headings = [];
|
|
4534
|
+
let match;
|
|
4535
|
+
|
|
4536
|
+
while ((match = headingPattern.exec(content)) !== null) {
|
|
4537
|
+
const level = parseInt(match[1]);
|
|
4538
|
+
const rawText = match[2];
|
|
4539
|
+
const text = rawText.replace(/<[^>]*>/g, '').trim();
|
|
4540
|
+
const fullTag = match[0];
|
|
4541
|
+
|
|
4542
|
+
headings.push({
|
|
4543
|
+
level,
|
|
4544
|
+
text,
|
|
4545
|
+
rawText,
|
|
4546
|
+
fullTag,
|
|
4547
|
+
position: match.index,
|
|
4548
|
+
originalMatch: match[0]
|
|
4549
|
+
});
|
|
4550
|
+
}
|
|
4551
|
+
|
|
4552
|
+
if (headings.length === 0) {
|
|
4553
|
+
issues.push({
|
|
4554
|
+
type: '๐ No headings found',
|
|
4555
|
+
description: 'Page has no heading elements',
|
|
4556
|
+
suggestion: 'Add heading elements (h1-h6) to structure content',
|
|
4557
|
+
severity: 'error',
|
|
4558
|
+
fixable: false
|
|
4559
|
+
});
|
|
4560
|
+
return { issues, suggestions, headings };
|
|
4561
|
+
}
|
|
4562
|
+
|
|
4563
|
+
// Check for h1
|
|
4564
|
+
const h1Count = headings.filter(h => h.level === 1).length;
|
|
4565
|
+
if (h1Count === 0) {
|
|
4566
|
+
issues.push({
|
|
4567
|
+
type: '๐ Missing h1',
|
|
4568
|
+
description: 'Page should have exactly one h1 element',
|
|
4569
|
+
suggestion: 'Add an h1 element as the main page heading',
|
|
4570
|
+
severity: 'error',
|
|
4571
|
+
fixable: true,
|
|
4572
|
+
fix: 'add-h1'
|
|
4573
|
+
});
|
|
4574
|
+
} else if (h1Count > 1) {
|
|
4575
|
+
issues.push({
|
|
4576
|
+
type: '๐ Multiple h1 elements',
|
|
4577
|
+
description: `Found ${h1Count} h1 elements, should have only one`,
|
|
4578
|
+
suggestion: 'Convert extra h1 elements to h2-h6 as appropriate',
|
|
4579
|
+
severity: 'error',
|
|
4580
|
+
fixable: true,
|
|
4581
|
+
fix: 'fix-multiple-h1'
|
|
4582
|
+
});
|
|
4583
|
+
}
|
|
4584
|
+
|
|
4585
|
+
// Check heading order and hierarchy
|
|
4586
|
+
for (let i = 1; i < headings.length; i++) {
|
|
4587
|
+
const current = headings[i];
|
|
4588
|
+
const previous = headings[i - 1];
|
|
4589
|
+
|
|
4590
|
+
// Check for level skipping
|
|
4591
|
+
if (current.level > previous.level + 1) {
|
|
4592
|
+
issues.push({
|
|
4593
|
+
type: '๐ Heading level skip',
|
|
4594
|
+
description: `Heading level jumps from h${previous.level} to h${current.level}`,
|
|
4595
|
+
suggestion: `Use h${previous.level + 1} instead of h${current.level}`,
|
|
4596
|
+
severity: 'warning',
|
|
4597
|
+
fixable: true,
|
|
4598
|
+
fix: 'fix-level-skip',
|
|
4599
|
+
targetIndex: i,
|
|
4600
|
+
correctLevel: previous.level + 1
|
|
4601
|
+
});
|
|
4602
|
+
}
|
|
4603
|
+
}
|
|
4604
|
+
|
|
4605
|
+
// Check for empty headings
|
|
4606
|
+
headings.forEach((heading, index) => {
|
|
4607
|
+
if (!heading.text) {
|
|
4608
|
+
issues.push({
|
|
4609
|
+
type: '๐ Empty heading',
|
|
4610
|
+
description: `Heading ${index + 1} (h${heading.level}) is empty`,
|
|
4611
|
+
suggestion: 'Add descriptive text to the heading or remove it',
|
|
4612
|
+
severity: 'error',
|
|
4613
|
+
fixable: true,
|
|
4614
|
+
fix: 'fix-empty-heading',
|
|
4615
|
+
targetIndex: index
|
|
4616
|
+
});
|
|
4617
|
+
}
|
|
4618
|
+
});
|
|
4619
|
+
|
|
4620
|
+
// Check for consecutive headings with same level and similar content
|
|
4621
|
+
for (let i = 1; i < headings.length; i++) {
|
|
4622
|
+
const current = headings[i];
|
|
4623
|
+
const previous = headings[i - 1];
|
|
4624
|
+
|
|
4625
|
+
if (current.level === previous.level &&
|
|
4626
|
+
current.text.toLowerCase() === previous.text.toLowerCase() &&
|
|
4627
|
+
current.text.length > 0) {
|
|
4628
|
+
issues.push({
|
|
4629
|
+
type: '๐ Duplicate heading',
|
|
4630
|
+
description: `Duplicate h${current.level} heading: "${current.text}"`,
|
|
4631
|
+
suggestion: 'Make heading text unique or merge content',
|
|
4632
|
+
severity: 'warning',
|
|
4633
|
+
fixable: false
|
|
4634
|
+
});
|
|
4635
|
+
}
|
|
4636
|
+
}
|
|
4637
|
+
|
|
4638
|
+
return { issues, suggestions, headings };
|
|
4639
|
+
}
|
|
4640
|
+
|
|
4641
|
+
fixHeadingStructureInContent(content, analysis) {
|
|
4642
|
+
let fixed = content;
|
|
4643
|
+
const { issues, headings } = analysis;
|
|
4644
|
+
|
|
4645
|
+
// Sort issues by position (descending) to avoid position shifts
|
|
4646
|
+
const fixableIssues = issues
|
|
4647
|
+
.filter(issue => issue.fixable)
|
|
4648
|
+
.sort((a, b) => (b.targetIndex || 0) - (a.targetIndex || 0));
|
|
4649
|
+
|
|
4650
|
+
fixableIssues.forEach(issue => {
|
|
4651
|
+
switch (issue.fix) {
|
|
4652
|
+
case 'add-h1':
|
|
4653
|
+
fixed = this.addMissingH1(fixed);
|
|
4654
|
+
break;
|
|
4655
|
+
|
|
4656
|
+
case 'fix-multiple-h1':
|
|
4657
|
+
fixed = this.fixMultipleH1(fixed, headings);
|
|
4658
|
+
break;
|
|
4659
|
+
|
|
4660
|
+
case 'fix-level-skip':
|
|
4661
|
+
if (issue.targetIndex !== undefined && issue.correctLevel) {
|
|
4662
|
+
fixed = this.fixHeadingLevelSkip(fixed, headings[issue.targetIndex], issue.correctLevel);
|
|
4663
|
+
}
|
|
4664
|
+
break;
|
|
4665
|
+
|
|
4666
|
+
case 'fix-empty-heading':
|
|
4667
|
+
if (issue.targetIndex !== undefined) {
|
|
4668
|
+
fixed = this.fixEmptyHeading(fixed, headings[issue.targetIndex]);
|
|
4669
|
+
}
|
|
4670
|
+
break;
|
|
4671
|
+
}
|
|
4672
|
+
});
|
|
4673
|
+
|
|
4674
|
+
return fixed;
|
|
4675
|
+
}
|
|
4676
|
+
|
|
4677
|
+
addMissingH1(content) {
|
|
4678
|
+
// Try to find the first heading and convert it to h1
|
|
4679
|
+
const firstHeadingMatch = content.match(/<h([2-6])[^>]*>([\s\S]*?)<\/h[2-6]>/i);
|
|
4680
|
+
|
|
4681
|
+
if (firstHeadingMatch) {
|
|
4682
|
+
const level = firstHeadingMatch[1];
|
|
4683
|
+
const replacement = firstHeadingMatch[0].replace(
|
|
4684
|
+
new RegExp(`<h${level}([^>]*)>`, 'i'),
|
|
4685
|
+
'<h1$1>'
|
|
4686
|
+
).replace(
|
|
4687
|
+
new RegExp(`</h${level}>`, 'i'),
|
|
4688
|
+
'</h1>'
|
|
4689
|
+
);
|
|
4690
|
+
|
|
4691
|
+
const result = content.replace(firstHeadingMatch[0], replacement);
|
|
4692
|
+
console.log(chalk.yellow(` ๐ Converted first h${level} to h1`));
|
|
4693
|
+
return result;
|
|
4694
|
+
}
|
|
4695
|
+
|
|
4696
|
+
// If no headings found, try to add h1 based on title or first significant text
|
|
4697
|
+
const titleMatch = content.match(/<title[^>]*>([^<]+)<\/title>/i);
|
|
4698
|
+
if (titleMatch) {
|
|
4699
|
+
const title = titleMatch[1].trim();
|
|
4700
|
+
// Insert h1 after opening body tag
|
|
4701
|
+
const bodyMatch = content.match(/(<body[^>]*>)/i);
|
|
4702
|
+
if (bodyMatch) {
|
|
4703
|
+
const h1Element = `\n <h1>${title}</h1>\n`;
|
|
4704
|
+
const result = content.replace(bodyMatch[1], bodyMatch[1] + h1Element);
|
|
4705
|
+
console.log(chalk.yellow(` ๐ Added h1 element with title: "${title}"`));
|
|
4706
|
+
return result;
|
|
4707
|
+
}
|
|
4708
|
+
}
|
|
4709
|
+
|
|
4710
|
+
return content;
|
|
4711
|
+
}
|
|
4712
|
+
|
|
4713
|
+
fixMultipleH1(content, headings) {
|
|
4714
|
+
const h1Elements = headings.filter(h => h.level === 1);
|
|
4715
|
+
|
|
4716
|
+
// Keep the first h1, convert others to h2
|
|
4717
|
+
for (let i = 1; i < h1Elements.length; i++) {
|
|
4718
|
+
const h1 = h1Elements[i];
|
|
4719
|
+
const replacement = h1.fullTag.replace(/<h1([^>]*)>/i, '<h2$1>').replace(/<\/h1>/i, '</h2>');
|
|
4720
|
+
content = content.replace(h1.fullTag, replacement);
|
|
4721
|
+
console.log(chalk.yellow(` ๐ Converted extra h1 to h2: "${h1.text}"`));
|
|
4722
|
+
}
|
|
4723
|
+
|
|
4724
|
+
return content;
|
|
4725
|
+
}
|
|
4726
|
+
|
|
4727
|
+
fixHeadingLevelSkip(content, heading, correctLevel) {
|
|
4728
|
+
const replacement = heading.fullTag
|
|
4729
|
+
.replace(new RegExp(`<h${heading.level}([^>]*)>`, 'i'), `<h${correctLevel}$1>`)
|
|
4730
|
+
.replace(new RegExp(`</h${heading.level}>`, 'i'), `</h${correctLevel}>`);
|
|
4731
|
+
|
|
4732
|
+
const result = content.replace(heading.fullTag, replacement);
|
|
4733
|
+
console.log(chalk.yellow(` ๐ Fixed level skip: h${heading.level} โ h${correctLevel} for "${heading.text}"`));
|
|
4734
|
+
return result;
|
|
4735
|
+
}
|
|
4736
|
+
|
|
4737
|
+
fixEmptyHeading(content, heading) {
|
|
4738
|
+
// Generate meaningful text based on context
|
|
4739
|
+
const contextText = this.generateHeadingText(content, heading);
|
|
4740
|
+
|
|
4741
|
+
if (contextText) {
|
|
4742
|
+
const replacement = heading.fullTag.replace(
|
|
4743
|
+
/<h([1-6])([^>]*)>[\s\S]*?<\/h[1-6]>/i,
|
|
4744
|
+
`<h$1$2>${contextText}</h$1>`
|
|
4745
|
+
);
|
|
4746
|
+
|
|
4747
|
+
const result = content.replace(heading.fullTag, replacement);
|
|
4748
|
+
console.log(chalk.yellow(` ๐ Added text to empty heading: "${contextText}"`));
|
|
4749
|
+
return result;
|
|
4750
|
+
}
|
|
4751
|
+
|
|
4752
|
+
// If can't generate text, remove the empty heading
|
|
4753
|
+
const result = content.replace(heading.fullTag, '');
|
|
4754
|
+
console.log(chalk.yellow(` ๐ Removed empty h${heading.level} heading`));
|
|
4755
|
+
return result;
|
|
4756
|
+
}
|
|
4757
|
+
|
|
4758
|
+
generateHeadingText(content, heading) {
|
|
4759
|
+
const lang = this.config.language;
|
|
4760
|
+
|
|
4761
|
+
// Try to find nearby text content
|
|
4762
|
+
const position = heading.position;
|
|
4763
|
+
const contextRange = 500;
|
|
4764
|
+
const beforeContext = content.substring(Math.max(0, position - contextRange), position);
|
|
4765
|
+
const afterContext = content.substring(position + heading.fullTag.length, position + heading.fullTag.length + contextRange);
|
|
4766
|
+
|
|
4767
|
+
// Look for meaningful text in nearby paragraphs
|
|
4768
|
+
const nearbyText = (beforeContext + afterContext).replace(/<[^>]*>/g, ' ').trim();
|
|
4769
|
+
const words = nearbyText.split(/\s+/).filter(word => word.length > 2);
|
|
4770
|
+
|
|
4771
|
+
if (words.length > 0) {
|
|
4772
|
+
const meaningfulWords = words.slice(0, 3);
|
|
4773
|
+
return meaningfulWords.join(' ');
|
|
4774
|
+
}
|
|
4775
|
+
|
|
4776
|
+
// Fallback to generic text based on language
|
|
4777
|
+
const genericTexts = {
|
|
4778
|
+
ja: ['่ฆๅบใ', 'ใปใฏใทใงใณ', 'ใณใณใใณใ'],
|
|
4779
|
+
en: ['Heading', 'Section', 'Content'],
|
|
4780
|
+
vi: ['Tiรชu ฤแป', 'Phแบงn', 'Nแปi dung']
|
|
4781
|
+
};
|
|
4782
|
+
|
|
4783
|
+
const texts = genericTexts[lang] || genericTexts.en;
|
|
4784
|
+
return texts[0];
|
|
4785
|
+
}
|
|
4786
|
+
|
|
4787
|
+
countHeadingFixes(originalContent, fixedContent) {
|
|
4788
|
+
// Count the number of heading-related changes
|
|
4789
|
+
const originalHeadings = (originalContent.match(/<h[1-6][^>]*>[\s\S]*?<\/h[1-6]>/gi) || []).length;
|
|
4790
|
+
const fixedHeadings = (fixedContent.match(/<h[1-6][^>]*>[\s\S]*?<\/h[1-6]>/gi) || []).length;
|
|
4791
|
+
|
|
4792
|
+
// Simple heuristic: count tag changes
|
|
4793
|
+
let changes = 0;
|
|
4794
|
+
|
|
4795
|
+
// Count h1 additions
|
|
4796
|
+
const originalH1 = (originalContent.match(/<h1[^>]*>/gi) || []).length;
|
|
4797
|
+
const fixedH1 = (fixedContent.match(/<h1[^>]*>/gi) || []).length;
|
|
4798
|
+
changes += Math.abs(fixedH1 - originalH1);
|
|
4799
|
+
|
|
4800
|
+
// Count level changes (rough estimate)
|
|
4801
|
+
for (let level = 1; level <= 6; level++) {
|
|
4802
|
+
const originalCount = (originalContent.match(new RegExp(`<h${level}[^>]*>`, 'gi')) || []).length;
|
|
4803
|
+
const fixedCount = (fixedContent.match(new RegExp(`<h${level}[^>]*>`, 'gi')) || []).length;
|
|
4804
|
+
changes += Math.abs(fixedCount - originalCount);
|
|
4805
|
+
}
|
|
4806
|
+
|
|
4807
|
+
return Math.max(1, Math.floor(changes / 2)); // Rough estimate
|
|
4808
|
+
}
|
|
4809
|
+
|
|
4810
|
+
async fixDescriptionLists(directory = '.') {
|
|
4811
|
+
console.log(chalk.blue('๐ Fixing description list structure...'));
|
|
4812
|
+
|
|
4813
|
+
const htmlFiles = await this.findHtmlFiles(directory);
|
|
4814
|
+
const results = [];
|
|
4815
|
+
let totalIssuesFound = 0;
|
|
4816
|
+
|
|
4817
|
+
for (const file of htmlFiles) {
|
|
4818
|
+
try {
|
|
4819
|
+
const content = await fs.readFile(file, 'utf8');
|
|
4820
|
+
const issues = this.analyzeDescriptionListStructure(content);
|
|
4821
|
+
|
|
4822
|
+
if (issues.length > 0) {
|
|
4823
|
+
console.log(chalk.cyan(`\n๐ ${file}:`));
|
|
4824
|
+
issues.forEach(issue => {
|
|
4825
|
+
console.log(chalk.yellow(` ${issue.type}: ${issue.description}`));
|
|
4826
|
+
if (issue.suggestion) {
|
|
4827
|
+
console.log(chalk.gray(` ๐ก ${issue.suggestion}`));
|
|
4828
|
+
}
|
|
4829
|
+
totalIssuesFound++;
|
|
4830
|
+
});
|
|
4831
|
+
}
|
|
4832
|
+
|
|
4833
|
+
const fixed = this.fixDescriptionListStructureInContent(content);
|
|
4834
|
+
|
|
4835
|
+
if (fixed !== content) {
|
|
4836
|
+
if (this.config.backupFiles) {
|
|
4837
|
+
await fs.writeFile(`${file}.backup`, content);
|
|
4838
|
+
}
|
|
4839
|
+
|
|
4840
|
+
if (!this.config.dryRun) {
|
|
4841
|
+
await fs.writeFile(file, fixed);
|
|
4842
|
+
}
|
|
4843
|
+
|
|
4844
|
+
console.log(chalk.green(`โ
Fixed description list structure in: ${file}`));
|
|
4845
|
+
results.push({ file, status: 'fixed', issues: issues.length });
|
|
4846
|
+
} else {
|
|
4847
|
+
results.push({ file, status: 'no-change', issues: issues.length });
|
|
4848
|
+
}
|
|
4849
|
+
} catch (error) {
|
|
4850
|
+
console.error(chalk.red(`โ Error processing ${file}: ${error.message}`));
|
|
4851
|
+
results.push({ file, status: 'error', error: error.message });
|
|
4852
|
+
}
|
|
4853
|
+
}
|
|
4854
|
+
|
|
4855
|
+
console.log(chalk.blue(`\n๐ Summary: Found ${totalIssuesFound} description list issues across ${results.length} files`));
|
|
4856
|
+
return results;
|
|
4857
|
+
}
|
|
4858
|
+
|
|
4859
|
+
analyzeDescriptionListStructure(content) {
|
|
4860
|
+
const issues = [];
|
|
4861
|
+
|
|
4862
|
+
// Find all dl elements
|
|
4863
|
+
const dlPattern = /<dl[^>]*>([\s\S]*?)<\/dl>/gi;
|
|
4864
|
+
let dlMatch;
|
|
4865
|
+
let dlIndex = 0;
|
|
4866
|
+
|
|
4867
|
+
while ((dlMatch = dlPattern.exec(content)) !== null) {
|
|
4868
|
+
dlIndex++;
|
|
4869
|
+
const dlContent = dlMatch[1];
|
|
4870
|
+
const dlElement = dlMatch[0];
|
|
4871
|
+
|
|
4872
|
+
// Analyze the content inside dl
|
|
4873
|
+
const dtElements = (dlContent.match(/<dt[^>]*>[\s\S]*?<\/dt>/gi) || []);
|
|
4874
|
+
const ddElements = (dlContent.match(/<dd[^>]*>[\s\S]*?<\/dd>/gi) || []);
|
|
4875
|
+
|
|
4876
|
+
// Check for empty dl
|
|
4877
|
+
if (dtElements.length === 0 && ddElements.length === 0) {
|
|
4878
|
+
issues.push({
|
|
4879
|
+
type: '๐ Empty description list',
|
|
4880
|
+
description: `Description list ${dlIndex} is empty`,
|
|
4881
|
+
suggestion: 'Add dt/dd pairs or remove the empty dl element',
|
|
4882
|
+
severity: 'error',
|
|
4883
|
+
dlIndex,
|
|
4884
|
+
fix: 'remove-empty-dl'
|
|
4885
|
+
});
|
|
4886
|
+
continue;
|
|
4887
|
+
}
|
|
4888
|
+
|
|
4889
|
+
// Check for missing dt elements
|
|
4890
|
+
if (dtElements.length === 0 && ddElements.length > 0) {
|
|
4891
|
+
issues.push({
|
|
4892
|
+
type: '๐ Missing dt elements',
|
|
4893
|
+
description: `Description list ${dlIndex} has dd elements but no dt elements`,
|
|
4894
|
+
suggestion: 'Add dt elements to describe the dd content',
|
|
4895
|
+
severity: 'error',
|
|
4896
|
+
dlIndex,
|
|
4897
|
+
fix: 'add-missing-dt'
|
|
4898
|
+
});
|
|
4899
|
+
}
|
|
4900
|
+
|
|
4901
|
+
// Check for missing dd elements
|
|
4902
|
+
if (dtElements.length > 0 && ddElements.length === 0) {
|
|
4903
|
+
issues.push({
|
|
4904
|
+
type: '๐ Missing dd elements',
|
|
4905
|
+
description: `Description list ${dlIndex} has dt elements but no dd elements`,
|
|
4906
|
+
suggestion: 'Add dd elements to provide descriptions',
|
|
4907
|
+
severity: 'error',
|
|
4908
|
+
dlIndex,
|
|
4909
|
+
fix: 'add-missing-dd'
|
|
4910
|
+
});
|
|
4911
|
+
}
|
|
4912
|
+
|
|
4913
|
+
// Check for improper nesting (non-dt/dd elements directly in dl)
|
|
4914
|
+
const invalidChildren = dlContent.match(/<(?!dt|dd|\/dt|\/dd)[a-zA-Z][^>]*>/g);
|
|
4915
|
+
if (invalidChildren) {
|
|
4916
|
+
const invalidTags = [...new Set(invalidChildren.map(tag => tag.match(/<([a-zA-Z]+)/)[1]))];
|
|
4917
|
+
issues.push({
|
|
4918
|
+
type: '๐ Invalid dl children',
|
|
4919
|
+
description: `Description list ${dlIndex} contains invalid child elements: ${invalidTags.join(', ')}`,
|
|
4920
|
+
suggestion: 'Only dt and dd elements should be direct children of dl',
|
|
4921
|
+
severity: 'warning',
|
|
4922
|
+
dlIndex,
|
|
4923
|
+
fix: 'wrap-invalid-children'
|
|
4924
|
+
});
|
|
4925
|
+
}
|
|
4926
|
+
|
|
4927
|
+
// Check for empty dt/dd elements
|
|
4928
|
+
dtElements.forEach((dt, index) => {
|
|
4929
|
+
const dtText = dt.replace(/<[^>]*>/g, '').trim();
|
|
4930
|
+
if (!dtText) {
|
|
4931
|
+
issues.push({
|
|
4932
|
+
type: '๐ Empty dt element',
|
|
4933
|
+
description: `Empty dt element in description list ${dlIndex}`,
|
|
4934
|
+
suggestion: 'Add descriptive text to the dt element',
|
|
4935
|
+
severity: 'warning',
|
|
4936
|
+
dlIndex,
|
|
4937
|
+
dtIndex: index,
|
|
4938
|
+
fix: 'fix-empty-dt'
|
|
4939
|
+
});
|
|
4940
|
+
}
|
|
4941
|
+
});
|
|
4942
|
+
|
|
4943
|
+
ddElements.forEach((dd, index) => {
|
|
4944
|
+
const ddText = dd.replace(/<[^>]*>/g, '').trim();
|
|
4945
|
+
if (!ddText) {
|
|
4946
|
+
issues.push({
|
|
4947
|
+
type: '๐ Empty dd element',
|
|
4948
|
+
description: `Empty dd element in description list ${dlIndex}`,
|
|
4949
|
+
suggestion: 'Add descriptive content to the dd element',
|
|
4950
|
+
severity: 'warning',
|
|
4951
|
+
dlIndex,
|
|
4952
|
+
ddIndex: index,
|
|
4953
|
+
fix: 'fix-empty-dd'
|
|
4954
|
+
});
|
|
4955
|
+
}
|
|
4956
|
+
});
|
|
4957
|
+
|
|
4958
|
+
// Check for proper dt/dd pairing
|
|
4959
|
+
if (dtElements.length > 0 && ddElements.length > 0) {
|
|
4960
|
+
// Basic check: should have at least one dd for each dt
|
|
4961
|
+
if (ddElements.length < dtElements.length) {
|
|
4962
|
+
issues.push({
|
|
4963
|
+
type: '๐ Insufficient dd elements',
|
|
4964
|
+
description: `Description list ${dlIndex} has ${dtElements.length} dt elements but only ${ddElements.length} dd elements`,
|
|
4965
|
+
suggestion: 'Each dt should have at least one corresponding dd element',
|
|
4966
|
+
severity: 'warning',
|
|
4967
|
+
dlIndex
|
|
4968
|
+
});
|
|
4969
|
+
}
|
|
4970
|
+
}
|
|
4971
|
+
}
|
|
4972
|
+
|
|
4973
|
+
return issues;
|
|
4974
|
+
}
|
|
4975
|
+
|
|
4976
|
+
fixDescriptionListStructureInContent(content) {
|
|
4977
|
+
let fixed = content;
|
|
4978
|
+
|
|
4979
|
+
// Fix empty dl elements
|
|
4980
|
+
fixed = fixed.replace(/<dl[^>]*>\s*<\/dl>/gi, (match) => {
|
|
4981
|
+
console.log(chalk.yellow(` ๐ Removed empty description list`));
|
|
4982
|
+
return '';
|
|
4983
|
+
});
|
|
4984
|
+
|
|
4985
|
+
// Fix dl elements with only whitespace
|
|
4986
|
+
fixed = fixed.replace(/<dl[^>]*>[\s\n\r]*<\/dl>/gi, (match) => {
|
|
4987
|
+
console.log(chalk.yellow(` ๐ Removed empty description list`));
|
|
4988
|
+
return '';
|
|
4989
|
+
});
|
|
4990
|
+
|
|
4991
|
+
// Fix dl elements with invalid direct children
|
|
4992
|
+
fixed = fixed.replace(/<dl([^>]*)>([\s\S]*?)<\/dl>/gi, (match, attributes, content) => {
|
|
4993
|
+
// Extract dt and dd elements
|
|
4994
|
+
const dtElements = content.match(/<dt[^>]*>[\s\S]*?<\/dt>/gi) || [];
|
|
4995
|
+
const ddElements = content.match(/<dd[^>]*>[\s\S]*?<\/dd>/gi) || [];
|
|
4996
|
+
|
|
4997
|
+
// Find invalid children (not dt or dd)
|
|
4998
|
+
let cleanContent = content;
|
|
4999
|
+
|
|
5000
|
+
// Remove invalid direct children by wrapping them in dd
|
|
5001
|
+
cleanContent = cleanContent.replace(/<(?!dt|dd|\/dt|\/dd)([a-zA-Z][^>]*)>([\s\S]*?)<\/[a-zA-Z]+>/gi, (invalidMatch, tag, innerContent) => {
|
|
5002
|
+
console.log(chalk.yellow(` ๐ Wrapped invalid child element in dd`));
|
|
5003
|
+
return `<dd>${invalidMatch}</dd>`;
|
|
5004
|
+
});
|
|
5005
|
+
|
|
5006
|
+
// Handle text nodes that are not in dt/dd
|
|
5007
|
+
cleanContent = cleanContent.replace(/^([^<]+)(?=<(?:dt|dd))/gm, (textMatch) => {
|
|
5008
|
+
const trimmed = textMatch.trim();
|
|
5009
|
+
if (trimmed) {
|
|
5010
|
+
console.log(chalk.yellow(` ๐ Wrapped loose text in dd`));
|
|
5011
|
+
return `<dd>${trimmed}</dd>`;
|
|
5012
|
+
}
|
|
5013
|
+
return '';
|
|
5014
|
+
});
|
|
5015
|
+
|
|
5016
|
+
return `<dl${attributes}>${cleanContent}</dl>`;
|
|
5017
|
+
});
|
|
5018
|
+
|
|
5019
|
+
// Add missing dd elements for dt elements without corresponding dd
|
|
5020
|
+
fixed = fixed.replace(/<dl([^>]*)>([\s\S]*?)<\/dl>/gi, (match, attributes, content) => {
|
|
5021
|
+
const dtPattern = /<dt[^>]*>([\s\S]*?)<\/dt>/gi;
|
|
5022
|
+
const ddPattern = /<dd[^>]*>[\s\S]*?<\/dd>/gi;
|
|
5023
|
+
|
|
5024
|
+
const dtMatches = [...content.matchAll(dtPattern)];
|
|
5025
|
+
const ddMatches = [...content.matchAll(ddPattern)];
|
|
5026
|
+
|
|
5027
|
+
if (dtMatches.length > 0 && ddMatches.length === 0) {
|
|
5028
|
+
// Add dd elements after each dt
|
|
5029
|
+
let fixedContent = content;
|
|
5030
|
+
|
|
5031
|
+
// Process from end to beginning to maintain positions
|
|
5032
|
+
for (let i = dtMatches.length - 1; i >= 0; i--) {
|
|
5033
|
+
const dtMatch = dtMatches[i];
|
|
5034
|
+
const dtText = dtMatch[1].replace(/<[^>]*>/g, '').trim();
|
|
5035
|
+
const ddText = this.generateDescriptionForTerm(dtText);
|
|
5036
|
+
|
|
5037
|
+
const insertPosition = dtMatch.index + dtMatch[0].length;
|
|
5038
|
+
fixedContent = fixedContent.slice(0, insertPosition) +
|
|
5039
|
+
`\n <dd>${ddText}</dd>` +
|
|
5040
|
+
fixedContent.slice(insertPosition);
|
|
5041
|
+
}
|
|
5042
|
+
|
|
5043
|
+
console.log(chalk.yellow(` ๐ Added missing dd elements for ${dtMatches.length} dt elements`));
|
|
5044
|
+
return `<dl${attributes}>${fixedContent}</dl>`;
|
|
5045
|
+
}
|
|
5046
|
+
|
|
5047
|
+
return match;
|
|
5048
|
+
});
|
|
5049
|
+
|
|
5050
|
+
// Fix empty dt/dd elements
|
|
5051
|
+
fixed = fixed.replace(/<dt[^>]*>\s*<\/dt>/gi, (match) => {
|
|
5052
|
+
const lang = this.config.language;
|
|
5053
|
+
const defaultText = lang === 'ja' ? '้
็ฎ' : lang === 'vi' ? 'Mแปฅc' : 'Term';
|
|
5054
|
+
console.log(chalk.yellow(` ๐ Added text to empty dt element`));
|
|
5055
|
+
return match.replace(/>\s*</, `>${defaultText}<`);
|
|
5056
|
+
});
|
|
5057
|
+
|
|
5058
|
+
fixed = fixed.replace(/<dd[^>]*>\s*<\/dd>/gi, (match) => {
|
|
5059
|
+
const lang = this.config.language;
|
|
5060
|
+
const defaultText = lang === 'ja' ? '่ชฌๆ' : lang === 'vi' ? 'Mรด tแบฃ' : 'Description';
|
|
5061
|
+
console.log(chalk.yellow(` ๐ Added text to empty dd element`));
|
|
5062
|
+
return match.replace(/>\s*</, `>${defaultText}<`);
|
|
5063
|
+
});
|
|
5064
|
+
|
|
5065
|
+
return fixed;
|
|
5066
|
+
}
|
|
5067
|
+
|
|
5068
|
+
generateDescriptionForTerm(termText) {
|
|
5069
|
+
const lang = this.config.language;
|
|
5070
|
+
|
|
5071
|
+
// Try to generate meaningful description based on term
|
|
5072
|
+
if (termText) {
|
|
5073
|
+
const descriptions = {
|
|
5074
|
+
ja: `${termText}ใฎ่ชฌๆ`,
|
|
5075
|
+
en: `Description of ${termText}`,
|
|
5076
|
+
vi: `Mรด tแบฃ vแป ${termText}`
|
|
5077
|
+
};
|
|
5078
|
+
return descriptions[lang] || descriptions.en;
|
|
5079
|
+
}
|
|
5080
|
+
|
|
5081
|
+
// Fallback to generic description
|
|
5082
|
+
const fallbacks = {
|
|
5083
|
+
ja: '่ชฌๆ',
|
|
5084
|
+
en: 'Description',
|
|
5085
|
+
vi: 'Mรด tแบฃ'
|
|
5086
|
+
};
|
|
5087
|
+
|
|
5088
|
+
return fallbacks[lang] || fallbacks.en;
|
|
5089
|
+
}
|
|
5090
|
+
|
|
4096
5091
|
async findHtmlFiles(directory) {
|
|
4097
5092
|
const files = [];
|
|
4098
5093
|
|