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
351 lines (350 loc) • 14.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MetadataEnhancerTool = void 0;
class MetadataEnhancerTool {
getName() {
return 'metadata_enhancer';
}
getDescription() {
return 'Analyzes and enhances PDF metadata for improved accessibility and discoverability';
}
getSupportedWCAGCriteria() {
return [
'1.3.1', // Info and Relationships (Level A)
'3.1.1', // Language of Page (Level A)
'3.1.2', // Language of Parts (Level AA)
];
}
canProcess(_context) {
// Can process any PDF document
return true;
}
async execute(context, llmProvider) {
const startTime = Date.now();
const issues = [];
const fixes = [];
try {
// Analyze current metadata
const metadataAnalysis = await this.analyzeMetadata(context);
// Identify metadata issues
const metadataIssues = this.identifyMetadataIssues(metadataAnalysis, context);
issues.push(...metadataIssues);
// Generate enhanced metadata if LLM provider is available
if (llmProvider && issues.length > 0) {
const metadataFixes = await this.generateMetadataEnhancements(metadataAnalysis, context, llmProvider);
fixes.push(...metadataFixes);
}
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 analyzeMetadata(context) {
// Extract current metadata (mock implementation - would use pdf-lib in real scenario)
const current = this.extractCurrentMetadata(context);
// Determine missing and inadequate metadata
const missing = this.findMissingMetadata(current);
const inadequate = this.findInadequateMetadata(current);
// Generate recommendations based on content analysis
const recommendations = this.generateMetadataRecommendations(context);
return {
current,
missing,
inadequate,
recommendations,
};
}
extractCurrentMetadata(context) {
// Mock implementation - in real scenario, would extract from PDF buffer
// For now, assume minimal metadata based on filename
const fileName = context.fileName.replace(/\.[^/.]+$/, ""); // Remove extension
return {
title: fileName.length > 5 ? fileName : undefined,
language: context.language,
tagged: false, // Would need PDF structure analysis
hasDocumentStructure: false,
hasBookmarks: false,
hasAccessibilityAPI: false,
creator: 'Unknown',
producer: 'Unknown',
};
}
findMissingMetadata(metadata) {
const missing = [];
const requiredFields = ['title', 'author', 'subject', 'language'];
requiredFields.forEach(field => {
if (!metadata[field]) {
missing.push(field);
}
});
if (!metadata.keywords)
missing.push('keywords');
if (!metadata.tagged)
missing.push('accessibility_tags');
return missing;
}
findInadequateMetadata(metadata) {
const inadequate = [];
// Check title quality
if (metadata.title) {
if (metadata.title.length < 5 ||
metadata.title.toLowerCase().includes('untitled') ||
metadata.title.toLowerCase().includes('document')) {
inadequate.push('title');
}
}
// Check subject quality
if (metadata.subject && metadata.subject.length < 10) {
inadequate.push('subject');
}
// Check keywords quality
if (metadata.keywords) {
const keywordCount = metadata.keywords.split(/[,;]/).filter(k => k.trim().length > 0).length;
if (keywordCount < 3) {
inadequate.push('keywords');
}
}
return inadequate;
}
generateMetadataRecommendations(context) {
const textSample = context.textContent.substring(0, 2000);
const words = textSample.toLowerCase().split(/\s+/);
// Extract potential title from first meaningful line
const lines = context.textContent.split('\n').filter(line => line.trim().length > 0);
const potentialTitle = this.extractPotentialTitle(lines);
// Generate keywords from content
const keywords = this.extractKeywords(words);
// Generate subject from content analysis
const subject = this.generateSubjectFromContent(textSample);
return {
title: potentialTitle,
subject,
keywords,
language: context.language,
};
}
extractPotentialTitle(lines) {
// Look for title in first few lines
for (let i = 0; i < Math.min(5, lines.length); i++) {
const line = lines[i].trim();
// Skip very short lines or lines with only special characters
if (line.length < 5 || /^[^a-zA-Z]*$/.test(line))
continue;
// Good title candidates
if (line.length >= 10 && line.length <= 100 &&
!line.toLowerCase().includes('page') &&
!line.includes('|') && !line.includes('\t')) {
return this.cleanTitle(line);
}
}
return 'Document Title'; // Fallback
}
cleanTitle(title) {
// Clean up potential title text
return title
.replace(/^\d+\.?\s*/, '') // Remove leading numbers
.replace(/\s+/g, ' ') // Normalize whitespace
.trim()
.substring(0, 100); // Limit length
}
extractKeywords(words) {
// Extract meaningful keywords from content
const stopWords = new Set([
'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by',
'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does',
'did', 'will', 'would', 'could', 'should', 'may', 'might', 'must', 'can', 'this', 'that',
'these', 'those', 'i', 'you', 'he', 'she', 'it', 'we', 'they', 'me', 'him', 'her', 'us', 'them'
]);
// Count word frequency
const wordCounts = new Map();
words.forEach(word => {
const cleanWord = word.replace(/[^\w]/g, '').toLowerCase();
if (cleanWord.length > 3 && !stopWords.has(cleanWord) && /^[a-z]+$/.test(cleanWord)) {
wordCounts.set(cleanWord, (wordCounts.get(cleanWord) || 0) + 1);
}
});
// Get top keywords
return Array.from(wordCounts.entries())
.filter(([_word, count]) => count >= 2) // Must appear at least twice
.sort((a, b) => b[1] - a[1])
.slice(0, 8)
.map(([word]) => word);
}
generateSubjectFromContent(text) {
const lowerText = text.toLowerCase();
// Detect content type and generate appropriate subject
if (lowerText.includes('report') || lowerText.includes('analysis')) {
return 'Analytical report containing data and findings';
}
else if (lowerText.includes('manual') || lowerText.includes('guide') || lowerText.includes('instruction')) {
return 'Instructional guide and documentation';
}
else if (lowerText.includes('policy') || lowerText.includes('procedure') || lowerText.includes('compliance')) {
return 'Policy and procedural documentation';
}
else if (lowerText.includes('financial') || lowerText.includes('budget') || lowerText.includes('accounting')) {
return 'Financial documentation and data';
}
else if (lowerText.includes('research') || lowerText.includes('study') || lowerText.includes('methodology')) {
return 'Research documentation and findings';
}
else if (lowerText.includes('technical') || lowerText.includes('specification') || lowerText.includes('engineering')) {
return 'Technical documentation and specifications';
}
else if (lowerText.includes('training') || lowerText.includes('education') || lowerText.includes('learning')) {
return 'Educational and training materials';
}
else {
return 'Document containing important information and content';
}
}
identifyMetadataIssues(analysis, _context) {
const issues = [];
// Missing title
if (analysis.missing.includes('title')) {
issues.push({
type: 'metadata',
severity: 'high',
description: 'PDF lacks a proper document title',
wcagCriteria: ['1.3.1'],
suggestion: 'Add a descriptive title that clearly identifies the document content',
});
}
// Inadequate title
if (analysis.inadequate.includes('title')) {
issues.push({
type: 'metadata',
severity: 'medium',
description: 'PDF title is inadequate or generic',
wcagCriteria: ['1.3.1'],
suggestion: 'Improve the title to be more descriptive and meaningful',
});
}
// Missing language
if (analysis.missing.includes('language')) {
issues.push({
type: 'metadata',
severity: 'high',
description: 'PDF lacks language specification',
wcagCriteria: ['3.1.1'],
suggestion: 'Specify the primary language of the document for accessibility tools',
});
}
// Missing author
if (analysis.missing.includes('author')) {
issues.push({
type: 'metadata',
severity: 'medium',
description: 'PDF lacks author information',
wcagCriteria: ['1.3.1'],
suggestion: 'Add author information for document identification and credibility',
});
}
// Missing subject/description
if (analysis.missing.includes('subject')) {
issues.push({
type: 'metadata',
severity: 'medium',
description: 'PDF lacks subject/description metadata',
wcagCriteria: ['1.3.1'],
suggestion: 'Add a subject description that summarizes the document content',
});
}
// Missing keywords
if (analysis.missing.includes('keywords')) {
issues.push({
type: 'metadata',
severity: 'low',
description: 'PDF lacks keyword metadata for discoverability',
wcagCriteria: ['1.3.1'],
suggestion: 'Add relevant keywords to improve document searchability',
});
}
// Missing accessibility tags
if (analysis.missing.includes('accessibility_tags')) {
issues.push({
type: 'metadata',
severity: 'high',
description: 'PDF is not tagged for accessibility',
wcagCriteria: ['1.3.1'],
suggestion: 'Enable accessibility tagging to support assistive technologies',
});
}
return issues;
}
async generateMetadataEnhancements(analysis, context, _llmProvider) {
const fixes = [];
// Enhanced title
if (analysis.missing.includes('title') || analysis.inadequate.includes('title')) {
fixes.push({
type: 'metadata_title',
description: 'Generated enhanced document title',
applied: false,
wcagImprovement: ['1.3.1'],
beforeValue: analysis.current.title || 'No title',
afterValue: analysis.recommendations.title || 'Enhanced Document Title',
});
}
// Enhanced subject
if (analysis.missing.includes('subject') || analysis.inadequate.includes('subject')) {
fixes.push({
type: 'metadata_subject',
description: 'Generated document subject description',
applied: false,
wcagImprovement: ['1.3.1'],
beforeValue: analysis.current.subject || 'No subject',
afterValue: analysis.recommendations.subject || 'Document description',
});
}
// Enhanced keywords
if (analysis.missing.includes('keywords') || analysis.inadequate.includes('keywords')) {
const keywords = analysis.recommendations.keywords || [];
fixes.push({
type: 'metadata_keywords',
description: `Generated ${keywords.length} relevant keywords`,
applied: false,
wcagImprovement: ['1.3.1'],
beforeValue: analysis.current.keywords || 'No keywords',
afterValue: keywords.join(', '),
});
}
// Language specification
if (analysis.missing.includes('language')) {
fixes.push({
type: 'metadata_language',
description: 'Set document language for accessibility',
applied: false,
wcagImprovement: ['3.1.1'],
beforeValue: 'No language specified',
afterValue: context.language,
});
}
// Accessibility tagging recommendation
if (analysis.missing.includes('accessibility_tags')) {
fixes.push({
type: 'metadata_accessibility',
description: 'Enable accessibility tagging and structure',
applied: false,
wcagImprovement: ['1.3.1'],
beforeValue: 'Not tagged for accessibility',
afterValue: 'Accessibility tags enabled with proper document structure',
});
}
return fixes;
}
}
exports.MetadataEnhancerTool = MetadataEnhancerTool;