UNPKG

n8n-nodes-pdf-accessibility

Version:

AI-powered PDF accessibility automation for N8N - comprehensive WCAG compliance analysis, intelligent remediation, and professional audit reporting with 5 integrated accessibility tools

311 lines (310 loc) 12.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.HeadingStructureTool = void 0; class HeadingStructureTool { getName() { return 'heading_structure'; } getDescription() { return 'Analyzes and optimizes document heading hierarchy for logical structure and navigation'; } getSupportedWCAGCriteria() { return [ '1.3.1', // Info and Relationships (Level A) '2.4.1', // Bypass Blocks (Level A) '2.4.6', // Headings and Labels (Level AA) '2.4.10', // Section Headings (Level AAA) ]; } canProcess(context) { // Can process any document with substantial text content return context.textContent.length > 500; } async execute(context, llmProvider) { const startTime = Date.now(); const issues = []; const fixes = []; try { // Analyze heading structure const structureAnalysis = await this.analyzeHeadingStructure(context); // Identify issues const structureIssues = this.identifyStructureIssues(structureAnalysis, context); issues.push(...structureIssues); // Generate fixes if LLM provider is available if (llmProvider && issues.length > 0) { const structureFixes = await this.generateStructureFixes(structureAnalysis, context, llmProvider); fixes.push(...structureFixes); } return { toolName: this.getName(), success: true, issuesFound: issues, fixesApplied: fixes, processing_time_ms: Date.now() - startTime, }; } catch (error) { return { toolName: this.getName(), success: false, issuesFound: issues, fixesApplied: fixes, processing_time_ms: Date.now() - startTime, error: error instanceof Error ? error.message : String(error), }; } } async analyzeHeadingStructure(context) { const headings = this.extractHeadings(context.textContent); // Analyze structure const hasLogicalStructure = this.hasLogicalHeadingStructure(headings); const skippedLevels = this.findSkippedLevels(headings); const duplicateHeadings = this.findDuplicateHeadings(headings); const emptyHeadings = headings.filter(h => h.text.trim().length === 0); const h1Count = headings.filter(h => h.level === 1).length; const tooManyH1s = h1Count > 1; return { headings, hasLogicalStructure, skippedLevels, duplicateHeadings, emptyHeadings, tooManyH1s, }; } extractHeadings(textContent) { const headings = []; const lines = textContent.split('\n'); let position = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (line.length === 0) continue; // Detect potential headings using various heuristics const headingLevel = this.detectHeadingLevel(line, lines, i); if (headingLevel > 0) { headings.push({ text: line, level: headingLevel, position, page: Math.ceil(position / 3000), // Approximate page based on character position isProperlyTagged: false, // PDF structure analysis would be needed contextText: this.getContextText(lines, i), }); } position += line.length; } return headings; } detectHeadingLevel(line, lines, index) { // Heuristics for detecting heading levels from text // Look for numbered headings (1., 1.1, Chapter 1, etc.) if (/^\d+\.?\s/.test(line) && line.length < 100) { const dots = (line.match(/\./g) || []).length; return Math.min(dots + 1, 6); } // Look for chapter/section indicators if (/^(chapter|section|part)\s+\d+/i.test(line)) { return 1; } // Look for all caps (potential headings) if (line === line.toUpperCase() && line.length > 3 && line.length < 80) { return this.estimateHeadingLevelFromContext(line, lines, index); } // Look for title case with specific patterns if (this.isTitleCase(line) && line.length < 100 && !line.endsWith('.')) { return this.estimateHeadingLevelFromContext(line, lines, index); } // Look for lines followed by empty lines (potential headings) if (index < lines.length - 1 && lines[index + 1].trim() === '' && line.length < 80) { return this.estimateHeadingLevelFromContext(line, lines, index); } return 0; // Not a heading } estimateHeadingLevelFromContext(_line, lines, index) { // Estimate heading level based on position and content const linesFromStart = index; const totalLines = lines.length; const positionRatio = linesFromStart / totalLines; // Earlier in document = likely higher level if (positionRatio < 0.1) return 1; if (positionRatio < 0.3) return 2; if (positionRatio < 0.6) return 3; return 4; } isTitleCase(text) { const words = text.split(/\s+/); if (words.length === 0) return false; // Check if most words are capitalized const capitalizedWords = words.filter(word => word.length > 0 && word[0] === word[0].toUpperCase()); return capitalizedWords.length / words.length > 0.7; } getContextText(lines, index) { const start = Math.max(0, index - 2); const end = Math.min(lines.length, index + 3); return lines.slice(start, end).join(' ').trim(); } hasLogicalHeadingStructure(headings) { if (headings.length === 0) return true; let currentLevel = 0; for (const heading of headings) { if (heading.level > currentLevel + 1) { return false; // Skipped a level } currentLevel = heading.level; } return true; } findSkippedLevels(headings) { const skipped = []; let maxLevel = 0; for (const heading of headings) { if (heading.level > maxLevel + 1) { // Found skipped levels for (let level = maxLevel + 1; level < heading.level; level++) { if (!skipped.includes(level)) { skipped.push(level); } } } maxLevel = Math.max(maxLevel, heading.level); } return skipped.sort(); } findDuplicateHeadings(headings) { const seen = new Set(); const duplicates = new Set(); for (const heading of headings) { const normalizedText = heading.text.toLowerCase().trim(); if (seen.has(normalizedText)) { duplicates.add(heading.text); } else { seen.add(normalizedText); } } return Array.from(duplicates); } identifyStructureIssues(analysis, context) { const issues = []; // No headings found if (analysis.headings.length === 0 && context.textContent.length > 2000) { issues.push({ type: 'heading_structure', severity: 'high', description: 'Document lacks heading structure for navigation', wcagCriteria: ['1.3.1', '2.4.1'], suggestion: 'Add headings to create a logical document structure that aids navigation', }); } // Multiple H1s if (analysis.tooManyH1s) { issues.push({ type: 'heading_structure', severity: 'medium', description: 'Document has multiple H1 headings', wcagCriteria: ['1.3.1'], suggestion: 'Use only one H1 heading per document, typically for the main title', }); } // Skipped heading levels if (analysis.skippedLevels.length > 0) { issues.push({ type: 'heading_structure', severity: 'medium', description: `Heading structure skips levels: ${analysis.skippedLevels.join(', ')}`, wcagCriteria: ['1.3.1'], suggestion: 'Use heading levels sequentially (H1, H2, H3...) without skipping levels', }); } // Duplicate headings if (analysis.duplicateHeadings.length > 0) { issues.push({ type: 'heading_structure', severity: 'low', description: `Found ${analysis.duplicateHeadings.length} duplicate headings`, wcagCriteria: ['2.4.6'], suggestion: 'Make headings unique and descriptive to improve navigation clarity', }); } // Empty headings if (analysis.emptyHeadings.length > 0) { issues.push({ type: 'heading_structure', severity: 'high', description: `Found ${analysis.emptyHeadings.length} empty headings`, wcagCriteria: ['1.3.1', '2.4.6'], suggestion: 'Remove empty headings or add meaningful content', }); } // Poor heading structure if (!analysis.hasLogicalStructure) { issues.push({ type: 'heading_structure', severity: 'medium', description: 'Document heading structure is not logically organized', wcagCriteria: ['1.3.1', '2.4.10'], suggestion: 'Reorganize headings to create a logical hierarchy that reflects content structure', }); } return issues; } async generateStructureFixes(analysis, context, llmProvider) { const fixes = []; // Generate optimized heading structure if (!analysis.hasLogicalStructure || analysis.skippedLevels.length > 0) { try { const optimizedStructure = await this.generateOptimizedStructure(analysis, context, llmProvider); fixes.push({ type: 'heading_structure_optimization', description: 'Generated optimized heading structure', applied: false, wcagImprovement: ['1.3.1', '2.4.1', '2.4.6'], beforeValue: `${analysis.headings.length} headings with structural issues`, afterValue: optimizedStructure, }); } catch (error) { console.warn('Failed to generate optimized heading structure:', error); } } // Generate suggestions for duplicate headings if (analysis.duplicateHeadings.length > 0) { try { const uniqueHeadings = await this.generateUniqueHeadings(analysis.duplicateHeadings, context, llmProvider); fixes.push({ type: 'heading_uniqueness', description: `Generated unique alternatives for ${analysis.duplicateHeadings.length} duplicate headings`, applied: false, wcagImprovement: ['2.4.6'], beforeValue: analysis.duplicateHeadings.join(', '), afterValue: uniqueHeadings, }); } catch (error) { console.warn('Failed to generate unique headings:', error); } } return fixes; } async generateOptimizedStructure(analysis, context, _llmProvider) { // Mock implementation - would use LLM in real scenario const headingTexts = analysis.headings.map(h => h.text).slice(0, 10); return `Optimized heading structure: H1: ${context.fileName.replace(/\.[^/.]+$/, "")} (Main Document Title) ${headingTexts.map((text, index) => { const level = Math.min(index + 2, 6); return `H${level}: ${text}`; }).join('\n')}`; } async generateUniqueHeadings(duplicates, _context, _llmProvider) { // Mock implementation - would use LLM in real scenario return duplicates.map((heading, index) => `"${heading}" → "${heading} (Section ${index + 1})"`).join('\n'); } } exports.HeadingStructureTool = HeadingStructureTool;