Markdown Error Handling and Validation: Complete Guide for Content Quality Assurance and Automated Checking
Markdown error handling and validation techniques enable content teams to maintain high-quality documentation through automated quality assurance processes that catch syntax errors, validate links, enforce style guidelines, and ensure consistent formatting across large-scale content repositories. By implementing comprehensive validation workflows, syntax checking systems, and error prevention strategies, technical writers can build robust content pipelines that prevent publishing errors while maintaining editorial efficiency and collaborative workflows.
Why Master Markdown Error Handling and Validation?
Professional content validation provides essential benefits for technical documentation teams:
- Quality Assurance: Automated validation prevents syntax errors and formatting inconsistencies
- Team Collaboration: Consistent validation standards improve multi-author workflows
- Publishing Reliability: Pre-publication checking ensures error-free content delivery
- Maintenance Efficiency: Early error detection reduces long-term content maintenance costs
- User Experience: Validated content renders consistently across platforms and devices
Foundation Validation Principles
Understanding Common Markdown Errors
Identifying and categorizing the most frequent Markdown syntax and formatting issues:
# Common Markdown Error Patterns
## Syntax Errors
### Malformed Headers
❌ **Incorrect**: Missing space after hash
#Header Without Space
##Another Malformed Header
✅ **Correct**: Proper spacing and consistency
# Header With Proper Space
## Properly Formatted Subheader
### Broken Link Syntax
❌ **Incorrect**: Malformed link structures
[Broken link(https://example.com)
[Another broken](https://example.com
[Missing URL]()
✅ **Correct**: Well-formed link syntax
[Proper link](https://example.com)
[Internal link](#section-anchor)
[Reference link][ref-id]
[ref-id]: https://example.com "Reference title"
### Inconsistent List Formatting
❌ **Incorrect**: Mixed indentation and markers
- First item
* Second item with different marker
- Inconsistent indentation
* Yet another marker type
✅ **Correct**: Consistent formatting
- First item
- Second item with same marker
- Proper nested indentation
- Another nested item with consistency
### Code Block Issues
❌ **Incorrect**: Unterminated code blocks
```javascript
function example() {
return "missing closing backticks";
❌ **Incorrect**: Wrong language specification
```javascritp
function example() {
return "typo in language";
}
✅ Correct: Proper code block formatting
function example() {
return "properly formatted";
}
Nested Formatting Problems
❌ Incorrect: Improperly nested elements
- First item
- Nested bullet
- Second item
- Wrong nested numbering
- Mixed nesting patterns
✅ Correct: Consistent nested structure
- First item
- Properly nested bullet
- Another nested item
- Second item
- Correct nested numbering
- Consistent indentation patterns
```
Content Structure Validation
Implementing systematic approaches to document structure validation:
# Document Structure Validation Patterns
## Heading Hierarchy Validation
### Proper Heading Sequence
✅ **Correct**: Logical heading progression
# Document Title (H1)
## Major Section (H2)
### Subsection (H3)
#### Detail Level (H4)
❌ **Incorrect**: Skipped heading levels
# Document Title (H1)
### Subsection (H3) ← Skips H2
##### Detail (H5) ← Skips H4
### Duplicate Heading Detection
❌ **Problematic**: Duplicate headings create anchor conflicts
## Getting Started
[Content here]
## Getting Started ← Duplicate heading
[More content]
✅ **Solution**: Unique, descriptive headings
## Getting Started with Installation
[Content here]
## Getting Started with Configuration
[Different content]
## Table Structure Validation
### Complete Table Formatting
✅ **Correct**: Well-formed table structure
| **Column 1** | **Column 2** | **Column 3** |
|:-------------|:------------:|-------------:|
| Left-aligned | Centered | Right-aligned |
| Data row 1 | Data row 1 | Data row 1 |
| Data row 2 | Data row 2 | Data row 2 |
❌ **Incorrect**: Malformed table structure
| Column 1 | Column 2 | Column 3
|----------|----------| ← Missing final pipe
| Data | Missing cell ← Incomplete row
| Too many | cells | here | extra | ← Too many cells
### Table Alignment Consistency
❌ **Inconsistent**: Mixed alignment patterns
| Item | Price | Description |
|------|------:|:------------| ← Mixed separator styles
| Widget | $10 | Good widget |
|Gadget|$20|Great gadget| ← Inconsistent spacing
✅ **Consistent**: Uniform alignment and spacing
| **Item** | **Price** | **Description** |
|:---------|----------:|:----------------|
| Widget | $10 | Good widget |
| Gadget | $20 | Great gadget |
Automated Validation Systems
Comprehensive Linting Configuration
Building robust automated validation systems for Markdown content:
// markdown-validator.js - Comprehensive validation system
const fs = require('fs').promises;
const path = require('path');
const yaml = require('js-yaml');
const markdownIt = require('markdown-it');
const markdownLint = require('markdownlint');
class MarkdownValidator {
constructor(options = {}) {
this.config = {
// Validation rules configuration
rules: {
// Heading validation
headings: {
enforceHierarchy: true,
requireH1: true,
noDuplicates: true,
maxLength: 60
},
// Link validation
links: {
checkInternal: true,
checkExternal: options.checkExternalLinks || false,
allowedDomains: options.allowedDomains || [],
timeout: options.linkTimeout || 5000
},
// Content validation
content: {
enforceLineLength: options.maxLineLength || 120,
requireFrontmatter: true,
validateCodeBlocks: true,
checkImageAltText: true
},
// Table validation
tables: {
requireHeaders: true,
enforceAlignment: true,
validateStructure: true
},
// List validation
lists: {
consistentMarkers: true,
properIndentation: true,
validateNesting: true
}
},
// Custom validation patterns
customRules: options.customRules || {},
// Output configuration
output: {
format: options.outputFormat || 'detailed',
saveReport: options.saveReport || false,
reportPath: options.reportPath || './validation-report.json'
}
};
// Initialize Markdown parser
this.md = markdownIt({
html: true,
breaks: false,
linkify: true
});
// Validation results storage
this.results = {
files: [],
summary: {
totalFiles: 0,
filesWithErrors: 0,
totalErrors: 0,
totalWarnings: 0
}
};
// Error tracking
this.errorTypes = new Map();
}
async validateDirectory(directoryPath, options = {}) {
console.log(`🔍 Starting validation of directory: ${directoryPath}`);
try {
const files = await this.findMarkdownFiles(directoryPath, options.recursive);
this.results.summary.totalFiles = files.length;
console.log(`📁 Found ${files.length} Markdown files to validate`);
// Validate files in parallel with concurrency limit
const concurrency = options.concurrency || 5;
const chunks = this.chunkArray(files, concurrency);
for (const chunk of chunks) {
await Promise.all(
chunk.map(file => this.validateFile(file))
);
}
// Generate summary
this.generateSummary();
// Save report if configured
if (this.config.output.saveReport) {
await this.saveValidationReport();
}
return this.results;
} catch (error) {
console.error('❌ Validation failed:', error.message);
throw error;
}
}
async validateFile(filePath) {
console.log(`📝 Validating: ${filePath}`);
const fileResult = {
path: filePath,
errors: [],
warnings: [],
info: {},
isValid: true
};
try {
const content = await fs.readFile(filePath, 'utf8');
const relativePath = path.relative(process.cwd(), filePath);
// Parse frontmatter and content
const { frontmatter, markdown } = this.parseFrontmatter(content);
// Store file info
fileResult.info = {
size: content.length,
lines: content.split('\n').length,
words: this.countWords(markdown),
frontmatter: frontmatter
};
// Run validation checks
await this.validateSyntax(fileResult, content, markdown);
await this.validateStructure(fileResult, markdown);
await this.validateContent(fileResult, markdown, frontmatter);
await this.validateLinks(fileResult, markdown, filePath);
await this.validateImages(fileResult, markdown, filePath);
await this.validateCodeBlocks(fileResult, markdown);
await this.validateTables(fileResult, markdown);
await this.validateLists(fileResult, markdown);
// Apply custom validation rules
if (Object.keys(this.config.customRules).length > 0) {
await this.applyCustomRules(fileResult, content, markdown);
}
// Determine overall validity
fileResult.isValid = fileResult.errors.length === 0;
if (!fileResult.isValid) {
this.results.summary.filesWithErrors++;
}
this.results.summary.totalErrors += fileResult.errors.length;
this.results.summary.totalWarnings += fileResult.warnings.length;
// Track error types for summary
fileResult.errors.forEach(error => {
const count = this.errorTypes.get(error.type) || 0;
this.errorTypes.set(error.type, count + 1);
});
} catch (error) {
fileResult.errors.push({
type: 'file_error',
message: `Failed to process file: ${error.message}`,
severity: 'error',
line: null,
column: null
});
fileResult.isValid = false;
this.results.summary.filesWithErrors++;
this.results.summary.totalErrors++;
}
this.results.files.push(fileResult);
return fileResult;
}
async validateSyntax(fileResult, content, markdown) {
// Use markdownlint for basic syntax validation
const lintConfig = {
'default': true,
'MD013': false, // Line length handled separately
'MD033': false, // Allow HTML
'MD041': this.config.rules.headings.requireH1
};
try {
const lintResults = markdownLint.sync({
strings: {
[fileResult.path]: content
},
config: lintConfig
});
const results = lintResults[fileResult.path];
if (results && results.length > 0) {
results.forEach(result => {
fileResult.errors.push({
type: 'syntax_error',
rule: result.ruleNames[0],
message: result.ruleDescription,
detail: result.errorDetail || '',
severity: 'error',
line: result.lineNumber,
column: result.errorRange ? result.errorRange[0] : null
});
});
}
} catch (error) {
fileResult.errors.push({
type: 'syntax_validation_error',
message: `Syntax validation failed: ${error.message}`,
severity: 'error',
line: null,
column: null
});
}
}
async validateStructure(fileResult, markdown) {
const lines = markdown.split('\n');
const headings = [];
let currentLevel = 0;
// Extract headings and validate hierarchy
lines.forEach((line, index) => {
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
if (headingMatch) {
const level = headingMatch[1].length;
const text = headingMatch[2].trim();
headings.push({
level,
text,
line: index + 1
});
// Check heading hierarchy
if (this.config.rules.headings.enforceHierarchy) {
if (level > currentLevel + 1) {
fileResult.errors.push({
type: 'heading_hierarchy_error',
message: `Heading level ${level} skips level ${currentLevel + 1}`,
severity: 'error',
line: index + 1,
column: 1
});
}
}
currentLevel = level;
// Check heading length
if (this.config.rules.headings.maxLength &&
text.length > this.config.rules.headings.maxLength) {
fileResult.warnings.push({
type: 'heading_length_warning',
message: `Heading exceeds maximum length (${text.length} > ${this.config.rules.headings.maxLength})`,
severity: 'warning',
line: index + 1,
column: 1
});
}
}
});
// Check for H1 requirement
if (this.config.rules.headings.requireH1) {
const hasH1 = headings.some(h => h.level === 1);
if (!hasH1) {
fileResult.errors.push({
type: 'missing_h1_error',
message: 'Document must contain at least one H1 heading',
severity: 'error',
line: null,
column: null
});
}
}
// Check for duplicate headings
if (this.config.rules.headings.noDuplicates) {
const headingTexts = new Map();
headings.forEach(heading => {
const normalized = heading.text.toLowerCase().trim();
if (headingTexts.has(normalized)) {
fileResult.errors.push({
type: 'duplicate_heading_error',
message: `Duplicate heading: "${heading.text}"`,
severity: 'error',
line: heading.line,
column: 1
});
}
headingTexts.set(normalized, heading);
});
}
}
async validateContent(fileResult, markdown, frontmatter) {
const lines = markdown.split('\n');
// Validate frontmatter
if (this.config.rules.content.requireFrontmatter) {
if (!frontmatter || Object.keys(frontmatter).length === 0) {
fileResult.errors.push({
type: 'missing_frontmatter_error',
message: 'Document must contain frontmatter',
severity: 'error',
line: 1,
column: 1
});
}
}
// Check line length
if (this.config.rules.content.enforceLineLength) {
lines.forEach((line, index) => {
if (line.length > this.config.rules.content.enforceLineLength) {
fileResult.warnings.push({
type: 'line_length_warning',
message: `Line exceeds maximum length (${line.length} > ${this.config.rules.content.enforceLineLength})`,
severity: 'warning',
line: index + 1,
column: this.config.rules.content.enforceLineLength + 1
});
}
});
}
// Check for common content issues
lines.forEach((line, index) => {
// Check for trailing whitespace
if (line.match(/\s+$/)) {
fileResult.warnings.push({
type: 'trailing_whitespace_warning',
message: 'Line contains trailing whitespace',
severity: 'warning',
line: index + 1,
column: line.length
});
}
// Check for multiple consecutive blank lines
if (index > 0 && line.trim() === '' && lines[index - 1].trim() === '') {
let consecutiveBlankLines = 1;
for (let i = index - 1; i >= 0; i--) {
if (lines[i].trim() === '') {
consecutiveBlankLines++;
} else {
break;
}
}
if (consecutiveBlankLines > 2) {
fileResult.warnings.push({
type: 'excessive_blank_lines_warning',
message: `Too many consecutive blank lines (${consecutiveBlankLines})`,
severity: 'warning',
line: index + 1,
column: 1
});
}
}
});
}
async validateLinks(fileResult, markdown, filePath) {
if (!this.config.rules.links.checkInternal &&
!this.config.rules.links.checkExternal) {
return;
}
// Extract all links
const linkRegex = /\[([^\]]*)\]\(([^)]+)\)/g;
let match;
const links = [];
while ((match = linkRegex.exec(markdown)) !== null) {
const text = match[1];
const url = match[2];
const position = this.getLineAndColumn(markdown, match.index);
links.push({
text,
url,
line: position.line,
column: position.column,
index: match.index
});
}
// Validate each link
for (const link of links) {
if (link.url.startsWith('#')) {
// Internal anchor link
if (this.config.rules.links.checkInternal) {
await this.validateAnchorLink(fileResult, link, markdown);
}
} else if (link.url.startsWith('http://') || link.url.startsWith('https://')) {
// External link
if (this.config.rules.links.checkExternal) {
await this.validateExternalLink(fileResult, link);
}
} else if (link.url.startsWith('./') || link.url.startsWith('../') ||
!link.url.includes('://')) {
// Relative link
if (this.config.rules.links.checkInternal) {
await this.validateRelativeLink(fileResult, link, filePath);
}
}
}
}
async validateAnchorLink(fileResult, link, markdown) {
const anchorId = link.url.substring(1).toLowerCase();
// Check if anchor exists in document
const headingRegex = /^#{1,6}\s+(.+)$/gm;
const headings = [];
let match;
while ((match = headingRegex.exec(markdown)) !== null) {
const headingText = match[1].trim();
const generatedId = this.generateAnchorId(headingText);
headings.push(generatedId);
}
// Also check for custom anchor tags
const customAnchorRegex = /<a\s+[^>]*id\s*=\s*["']([^"']+)["'][^>]*>/gi;
while ((match = customAnchorRegex.exec(markdown)) !== null) {
headings.push(match[1].toLowerCase());
}
if (!headings.includes(anchorId)) {
fileResult.errors.push({
type: 'broken_anchor_link_error',
message: `Anchor link target not found: "${link.url}"`,
severity: 'error',
line: link.line,
column: link.column
});
}
}
async validateExternalLink(fileResult, link) {
try {
// Simple URL format validation
new URL(link.url);
// Check against allowed domains if specified
if (this.config.rules.links.allowedDomains.length > 0) {
const url = new URL(link.url);
const domain = url.hostname;
if (!this.config.rules.links.allowedDomains.includes(domain)) {
fileResult.warnings.push({
type: 'external_domain_warning',
message: `External link to non-allowed domain: ${domain}`,
severity: 'warning',
line: link.line,
column: link.column
});
}
}
// Note: Actual HTTP checking would be implemented here
// but is omitted for performance reasons in this example
} catch (error) {
fileResult.errors.push({
type: 'invalid_url_error',
message: `Invalid URL format: "${link.url}"`,
severity: 'error',
line: link.line,
column: link.column
});
}
}
async validateRelativeLink(fileResult, link, currentFilePath) {
try {
const basePath = path.dirname(currentFilePath);
const fullPath = path.resolve(basePath, link.url);
// Check if file exists
try {
await fs.access(fullPath);
} catch (error) {
fileResult.errors.push({
type: 'broken_relative_link_error',
message: `Relative link target not found: "${link.url}"`,
severity: 'error',
line: link.line,
column: link.column
});
}
} catch (error) {
fileResult.errors.push({
type: 'invalid_relative_link_error',
message: `Invalid relative link format: "${link.url}"`,
severity: 'error',
line: link.line,
column: link.column
});
}
}
async validateImages(fileResult, markdown, filePath) {
const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
let match;
while ((match = imageRegex.exec(markdown)) !== null) {
const altText = match[1];
const src = match[2];
const position = this.getLineAndColumn(markdown, match.index);
// Check for alt text if required
if (this.config.rules.content.checkImageAltText && !altText.trim()) {
fileResult.warnings.push({
type: 'missing_image_alt_warning',
message: 'Image missing alt text for accessibility',
severity: 'warning',
line: position.line,
column: position.column
});
}
// Check if local image file exists
if (!src.startsWith('http://') && !src.startsWith('https://')) {
try {
const basePath = path.dirname(filePath);
const fullPath = path.resolve(basePath, src);
await fs.access(fullPath);
} catch (error) {
fileResult.errors.push({
type: 'missing_image_error',
message: `Image file not found: "${src}"`,
severity: 'error',
line: position.line,
column: position.column
});
}
}
}
}
async validateCodeBlocks(fileResult, markdown) {
if (!this.config.rules.content.validateCodeBlocks) {
return;
}
const lines = markdown.split('\n');
let inCodeBlock = false;
let codeBlockStart = -1;
let language = '';
lines.forEach((line, index) => {
const trimmedLine = line.trim();
if (trimmedLine.startsWith('```')) {
if (inCodeBlock) {
// Closing code block
inCodeBlock = false;
codeBlockStart = -1;
language = '';
} else {
// Opening code block
inCodeBlock = true;
codeBlockStart = index + 1;
language = trimmedLine.substring(3).trim();
// Validate language specification
if (language && !this.isValidLanguage(language)) {
fileResult.warnings.push({
type: 'invalid_code_language_warning',
message: `Potentially invalid code language: "${language}"`,
severity: 'warning',
line: index + 1,
column: 1
});
}
}
}
});
// Check for unterminated code block
if (inCodeBlock) {
fileResult.errors.push({
type: 'unterminated_code_block_error',
message: 'Code block is not properly terminated',
severity: 'error',
line: codeBlockStart,
column: 1
});
}
}
async validateTables(fileResult, markdown) {
const lines = markdown.split('\n');
let inTable = false;
let tableStart = -1;
let expectedColumns = 0;
lines.forEach((line, index) => {
const trimmedLine = line.trim();
// Detect table start
if (!inTable && trimmedLine.includes('|') &&
index + 1 < lines.length &&
lines[index + 1].trim().match(/^[\|\-\:\s]+$/)) {
inTable = true;
tableStart = index + 1;
expectedColumns = (trimmedLine.match(/\|/g) || []).length - 1;
// Validate header row
if (!trimmedLine.startsWith('|') || !trimmedLine.endsWith('|')) {
fileResult.warnings.push({
type: 'table_formatting_warning',
message: 'Table row should start and end with pipe character',
severity: 'warning',
line: index + 1,
column: 1
});
}
} else if (inTable) {
if (trimmedLine.includes('|')) {
// Table row
const columnCount = (trimmedLine.match(/\|/g) || []).length - 1;
if (columnCount !== expectedColumns) {
fileResult.errors.push({
type: 'table_column_mismatch_error',
message: `Table row has ${columnCount} columns, expected ${expectedColumns}`,
severity: 'error',
line: index + 1,
column: 1
});
}
if (!trimmedLine.startsWith('|') || !trimmedLine.endsWith('|')) {
fileResult.warnings.push({
type: 'table_formatting_warning',
message: 'Table row should start and end with pipe character',
severity: 'warning',
line: index + 1,
column: 1
});
}
} else if (trimmedLine === '') {
// End of table
inTable = false;
tableStart = -1;
expectedColumns = 0;
}
}
});
}
async validateLists(fileResult, markdown) {
const lines = markdown.split('\n');
let listStack = [];
let expectedMarker = null;
lines.forEach((line, index) => {
const listMatch = line.match(/^(\s*)([\-\*\+]|\d+\.)\s/);
if (listMatch) {
const indent = listMatch[1];
const marker = listMatch[2];
const level = Math.floor(indent.length / 2); // Assuming 2-space indentation
// Check consistent markers
if (this.config.rules.lists.consistentMarkers) {
if (marker.match(/[\-\*\+]/)) {
// Unordered list marker
if (level < listStack.length) {
const expectedUnorderedMarker = listStack[level];
if (expectedUnorderedMarker &&
expectedUnorderedMarker !== marker &&
expectedUnorderedMarker.match(/[\-\*\+]/)) {
fileResult.warnings.push({
type: 'inconsistent_list_marker_warning',
message: `Inconsistent list marker "${marker}", expected "${expectedUnorderedMarker}"`,
severity: 'warning',
line: index + 1,
column: indent.length + 1
});
}
}
}
}
// Validate proper indentation
if (this.config.rules.lists.properIndentation) {
if (indent.length % 2 !== 0) {
fileResult.warnings.push({
type: 'list_indentation_warning',
message: 'List indentation should be in multiples of 2 spaces',
severity: 'warning',
line: index + 1,
column: 1
});
}
}
// Update list stack
while (listStack.length > level + 1) {
listStack.pop();
}
listStack[level] = marker;
} else if (line.trim() === '') {
// Empty line might end the list context
// Keep list stack for proper nesting validation
} else if (!line.match(/^\s/)) {
// Non-indented line ends list
listStack = [];
}
});
}
async applyCustomRules(fileResult, content, markdown) {
// Apply user-defined custom validation rules
for (const [ruleName, ruleFunction] of Object.entries(this.config.customRules)) {
try {
const customResult = await ruleFunction(content, markdown, fileResult.path);
if (customResult && Array.isArray(customResult)) {
customResult.forEach(issue => {
if (issue.severity === 'error') {
fileResult.errors.push({
type: `custom_rule_${ruleName}`,
...issue
});
} else {
fileResult.warnings.push({
type: `custom_rule_${ruleName}`,
...issue
});
}
});
}
} catch (error) {
fileResult.errors.push({
type: 'custom_rule_error',
message: `Custom rule "${ruleName}" failed: ${error.message}`,
severity: 'error',
line: null,
column: null
});
}
}
}
async findMarkdownFiles(directory, recursive = true) {
const files = [];
async function scanDirectory(dir) {
const items = await fs.readdir(dir, { withFileTypes: true });
for (const item of items) {
const fullPath = path.join(dir, item.name);
if (item.isDirectory() && recursive) {
await scanDirectory(fullPath);
} else if (item.isFile() &&
(item.name.endsWith('.md') || item.name.endsWith('.markdown'))) {
files.push(fullPath);
}
}
}
await scanDirectory(directory);
return files.sort();
}
parseFrontmatter(content) {
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
const match = content.match(frontmatterRegex);
if (match) {
try {
const frontmatter = yaml.load(match[1]);
const markdown = match[2];
return { frontmatter, markdown };
} catch (error) {
return { frontmatter: null, markdown: content };
}
}
return { frontmatter: null, markdown: content };
}
countWords(text) {
return text.trim().split(/\s+/).filter(word => word.length > 0).length;
}
getLineAndColumn(text, index) {
const beforeIndex = text.substring(0, index);
const lines = beforeIndex.split('\n');
return {
line: lines.length,
column: lines[lines.length - 1].length + 1
};
}
generateAnchorId(text) {
return text
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
}
isValidLanguage(language) {
// Common programming languages and formats
const validLanguages = [
'javascript', 'js', 'typescript', 'ts', 'python', 'py', 'java',
'c', 'cpp', 'csharp', 'cs', 'php', 'ruby', 'go', 'rust',
'html', 'css', 'scss', 'sass', 'json', 'xml', 'yaml', 'yml',
'sql', 'bash', 'sh', 'shell', 'powershell', 'dockerfile',
'markdown', 'md', 'plaintext', 'text'
];
return validLanguages.includes(language.toLowerCase());
}
chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
generateSummary() {
this.results.summary.errorsByType = Object.fromEntries(this.errorTypes);
// Calculate health score
const totalIssues = this.results.summary.totalErrors + this.results.summary.totalWarnings;
const maxPossibleScore = this.results.summary.totalFiles * 100;
const deductionPerError = 10;
const deductionPerWarning = 2;
const deductions = (this.results.summary.totalErrors * deductionPerError) +
(this.results.summary.totalWarnings * deductionPerWarning);
this.results.summary.healthScore = Math.max(0,
Math.round(((maxPossibleScore - deductions) / maxPossibleScore) * 100)
);
// Performance metrics
this.results.summary.avgErrorsPerFile =
this.results.summary.totalFiles > 0 ?
(this.results.summary.totalErrors / this.results.summary.totalFiles).toFixed(2) : 0;
this.results.summary.cleanFiles =
this.results.summary.totalFiles - this.results.summary.filesWithErrors;
this.results.summary.cleanFilePercentage =
this.results.summary.totalFiles > 0 ?
Math.round((this.results.summary.cleanFiles / this.results.summary.totalFiles) * 100) : 0;
}
async saveValidationReport() {
const reportData = {
timestamp: new Date().toISOString(),
configuration: this.config,
results: this.results
};
await fs.writeFile(
this.config.output.reportPath,
JSON.stringify(reportData, null, 2)
);
console.log(`📊 Validation report saved to: ${this.config.output.reportPath}`);
}
printSummary() {
const summary = this.results.summary;
console.log('\n' + '='.repeat(60));
console.log('📋 MARKDOWN VALIDATION SUMMARY');
console.log('='.repeat(60));
console.log(`📁 Total Files: ${summary.totalFiles}`);
console.log(`✅ Clean Files: ${summary.cleanFiles} (${summary.cleanFilePercentage}%)`);
console.log(`❌ Files with Errors: ${summary.filesWithErrors}`);
console.log(`🚨 Total Errors: ${summary.totalErrors}`);
console.log(`⚠️ Total Warnings: ${summary.totalWarnings}`);
console.log(`🏥 Health Score: ${summary.healthScore}%`);
if (Object.keys(summary.errorsByType).length > 0) {
console.log('\n📊 ERROR BREAKDOWN:');
for (const [type, count] of Object.entries(summary.errorsByType)) {
console.log(` • ${type}: ${count}`);
}
}
// Show files with issues
const filesWithIssues = this.results.files.filter(f =>
f.errors.length > 0 || f.warnings.length > 0
);
if (filesWithIssues.length > 0) {
console.log('\n🔍 FILES WITH ISSUES:');
filesWithIssues.forEach(file => {
console.log(`\n📄 ${file.path}`);
if (file.errors.length > 0) {
console.log(` ❌ Errors: ${file.errors.length}`);
file.errors.slice(0, 3).forEach(error => {
const location = error.line ? `:${error.line}` : '';
console.log(` • ${error.message}${location}`);
});
if (file.errors.length > 3) {
console.log(` • ... and ${file.errors.length - 3} more`);
}
}
if (file.warnings.length > 0) {
console.log(` ⚠️ Warnings: ${file.warnings.length}`);
}
});
}
console.log('\n' + '='.repeat(60));
}
}
module.exports = MarkdownValidator;
// CLI interface
if (require.main === module) {
const args = process.argv.slice(2);
const directory = args[0] || './';
const validator = new MarkdownValidator({
checkExternalLinks: process.env.CHECK_EXTERNAL_LINKS === 'true',
outputFormat: 'detailed',
saveReport: true,
reportPath: './markdown-validation-report.json'
});
validator.validateDirectory(directory, { recursive: true })
.then(results => {
validator.printSummary();
const hasErrors = results.summary.totalErrors > 0;
process.exit(hasErrors ? 1 : 0);
})
.catch(error => {
console.error('❌ Validation failed:', error);
process.exit(1);
});
}
CI/CD Integration Patterns
Integrating validation into automated workflows:
# .github/workflows/markdown-validation.yml
name: Markdown Content Validation
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
paths:
- '**/*.md'
- '**/*.markdown'
schedule:
# Weekly validation of all content
- cron: '0 2 * * 1'
env:
NODE_VERSION: '18'
VALIDATION_CONFIG: '.markdown-validation.json'
jobs:
# Quick validation for PRs
quick-validation:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: |
npm ci
npm install -g markdownlint-cli2
npm install -g markdown-link-check
- name: Get changed Markdown files
id: changed-files
uses: tj-actions/changed-files@v39
with:
files: |
**/*.md
**/*.markdown
- name: Lint changed Markdown files
if: steps.changed-files.outputs.any_changed == 'true'
run: |
echo "Changed files: ${{ steps.changed-files.outputs.all_changed_files }}"
markdownlint-cli2 ${{ steps.changed-files.outputs.all_changed_files }}
- name: Check links in changed files
if: steps.changed-files.outputs.any_changed == 'true'
run: |
for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
echo "Checking links in: $file"
markdown-link-check "$file" --config .markdown-link-check.json
done
- name: Validate with custom rules
if: steps.changed-files.outputs.any_changed == 'true'
run: |
node scripts/markdown-validator.js --files "${{ steps.changed-files.outputs.all_changed_files }}"
# Comprehensive validation for main branch and scheduled runs
comprehensive-validation:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' || github.event_name == 'schedule'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: |
npm ci
npm install -g markdownlint-cli2
npm install -g markdown-link-check
npm install -g alex
npm install -g textlint
- name: Comprehensive Markdown linting
run: |
markdownlint-cli2 "**/*.{md,markdown}" --config .markdownlint.json
- name: Check all internal links
run: |
find . -name "*.md" -o -name "*.markdown" | \
xargs -I {} markdown-link-check {} --config .markdown-link-check.json
- name: Check external links (with retry)
run: |
find . -name "*.md" -o -name "*.markdown" | \
xargs -I {} markdown-link-check {} --config .markdown-link-check-external.json || true
- name: Validate content quality
run: |
# Alex for inclusive language
find . -name "*.md" -exec alex {} \; || true
# TextLint for writing style
find . -name "*.md" -exec textlint {} \; || true
- name: Custom validation suite
run: |
node scripts/markdown-validator.js . --comprehensive
- name: Generate validation report
run: |
node scripts/generate-validation-report.js
- name: Upload validation artifacts
uses: actions/upload-artifact@v3
with:
name: validation-results
path: |
markdown-validation-report.json
validation-report.html
link-check-results/
- name: Comment on PR with results
if: github.event_name == 'pull_request' && failure()
uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
const report = JSON.parse(fs.readFileSync('markdown-validation-report.json', 'utf8'));
const errorCount = report.results.summary.totalErrors;
const warningCount = report.results.summary.totalWarnings;
const healthScore = report.results.summary.healthScore;
const comment = `
## 📋 Markdown Validation Results
| Metric | Value |
|--------|-------|
| Health Score | ${healthScore}% |
| Errors | ${errorCount} |
| Warnings | ${warningCount} |
| Files Checked | ${report.results.summary.totalFiles} |
${errorCount > 0 ? '❌ Validation failed. Please fix the errors before merging.' : '✅ All checks passed!'}
<details>
<summary>View detailed results</summary>
\`\`\`json
${JSON.stringify(report.results.summary, null, 2)}
\`\`\`
</details>
`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
# Accessibility and content quality checks
accessibility-validation:
runs-on: ubuntu-latest
needs: comprehensive-validation
if: always()
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install accessibility tools
run: |
npm install -g axe-core
npm install -g pa11y-ci
npm install -g lighthouse-ci
- name: Build documentation site
run: |
# Build your documentation site (Jekyll, Hugo, etc.)
bundle install
bundle exec jekyll build
- name: Test accessibility
run: |
# Run accessibility tests on built site
pa11y-ci --sitemap http://localhost:4000/sitemap.xml \
--threshold 10
- name: Performance audit
run: |
# Start local server
bundle exec jekyll serve --detach --port 4000
sleep 5
# Run Lighthouse CI
lhci autorun --config .lighthouserc.js
- name: Upload accessibility results
uses: actions/upload-artifact@v3
with:
name: accessibility-results
path: |
pa11y-results/
lighthouse-results/
# Security scanning for Markdown content
security-validation:
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event_name == 'schedule'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Scan for secrets in Markdown
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: main
head: HEAD
extra_args: --debug --only-verified
- name: Check for sensitive information
run: |
# Custom script to check for sensitive patterns
node scripts/check-sensitive-content.js
- name: Validate external link security
run: |
# Check for potential security issues in external links
node scripts/validate-link-security.js
# Generate and deploy validation dashboard
deploy-dashboard:
runs-on: ubuntu-latest
needs: [comprehensive-validation, accessibility-validation]
if: github.ref == 'refs/heads/main' && always()
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download validation artifacts
uses: actions/download-artifact@v3
with:
name: validation-results
path: validation-results/
- name: Generate validation dashboard
run: |
node scripts/generate-dashboard.js
- name: Deploy dashboard to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./validation-dashboard
destination_dir: validation
Error Prevention Strategies
Pre-commit Hooks Implementation
Setting up automated validation before content commits:
#!/bin/bash
# .git/hooks/pre-commit - Pre-commit validation hook
echo "🔍 Running Markdown validation before commit..."
# Configuration
MARKDOWNLINT_CONFIG=".markdownlint.json"
LINK_CHECK_CONFIG=".markdown-link-check.json"
CUSTOM_VALIDATOR="scripts/markdown-validator.js"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Get staged Markdown files
STAGED_MD_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(md|markdown)$')
if [ -z "$STAGED_MD_FILES" ]; then
echo "✅ No Markdown files to validate"
exit 0
fi
echo "📄 Validating staged Markdown files:"
echo "$STAGED_MD_FILES"
# Initialize validation results
VALIDATION_ERRORS=0
VALIDATION_WARNINGS=0
# Function to validate a single file
validate_file() {
local file="$1"
local has_errors=0
echo ""
echo "🔍 Validating: $file"
# Basic syntax validation with markdownlint
if command -v markdownlint >/dev/null 2>&1; then
if ! markdownlint -c "$MARKDOWNLINT_CONFIG" "$file"; then
echo -e "${RED}❌ Syntax errors found in $file${NC}"
has_errors=1
else
echo -e "${GREEN}✅ Syntax validation passed for $file${NC}"
fi
else
echo -e "${YELLOW}⚠️ markdownlint not found, skipping syntax validation${NC}"
fi
# Link validation
if command -v markdown-link-check >/dev/null 2>&1; then
if ! markdown-link-check "$file" -c "$LINK_CHECK_CONFIG" -q; then
echo -e "${RED}❌ Link validation failed for $file${NC}"
has_errors=1
else
echo -e "${GREEN}✅ Link validation passed for $file${NC}"
fi
else
echo -e "${YELLOW}⚠️ markdown-link-check not found, skipping link validation${NC}"
fi
# Custom validation rules
if [ -f "$CUSTOM_VALIDATOR" ] && command -v node >/dev/null 2>&1; then
if ! node "$CUSTOM_VALIDATOR" --file "$file" --format concise; then
echo -e "${RED}❌ Custom validation failed for $file${NC}"
has_errors=1
else
echo -e "${GREEN}✅ Custom validation passed for $file${NC}"
fi
fi
return $has_errors
}
# Validate each staged file
echo ""
echo "🚀 Starting validation process..."
for file in $STAGED_MD_FILES; do
if validate_file "$file"; then
echo -e "${GREEN}✅ $file passed all validations${NC}"
else
echo -e "${RED}❌ $file has validation errors${NC}"
VALIDATION_ERRORS=$((VALIDATION_ERRORS + 1))
fi
done
# Summary and decision
echo ""
echo "=" * 50
echo "📊 VALIDATION SUMMARY"
echo "=" * 50
if [ $VALIDATION_ERRORS -eq 0 ]; then
echo -e "${GREEN}✅ All Markdown files passed validation!${NC}"
echo "💚 Commit proceeding..."
exit 0
else
echo -e "${RED}❌ $VALIDATION_ERRORS file(s) failed validation${NC}"
echo ""
echo "🛠️ To fix issues:"
echo " • Run 'markdownlint --fix' to auto-fix syntax issues"
echo " • Check and fix broken links manually"
echo " • Review custom validation output above"
echo ""
echo "🚫 Commit blocked until validation passes"
echo " Use 'git commit --no-verify' to bypass (not recommended)"
exit 1
fi
Editor Integration and Real-Time Validation
Configuring development environments for immediate feedback:
{
"name": "markdown-validation-workspace",
"version": "1.0.0",
"description": "Workspace configuration for Markdown validation",
"devDependencies": {
"markdownlint": "^0.29.0",
"markdownlint-cli2": "^0.8.1",
"markdown-link-check": "^3.11.0",
"remark": "^14.0.3",
"remark-lint": "^9.1.2",
"remark-preset-lint-recommended": "^6.1.3",
"textlint": "^13.3.2",
"alex": "^11.0.1"
},
"scripts": {
"validate": "node scripts/markdown-validator.js",
"validate:quick": "markdownlint-cli2 **/*.md",
"validate:links": "find . -name '*.md' | xargs markdown-link-check",
"validate:content": "textlint **/*.md",
"validate:inclusive": "alex **/*.md",
"fix": "markdownlint-cli2-fix **/*.md",
"validate:watch": "chokidar '**/*.md' -c 'npm run validate:quick'",
"validate:pre-commit": "lint-staged"
},
"lint-staged": {
"*.{md,markdown}": [
"markdownlint-cli2 --fix",
"node scripts/markdown-validator.js --file",
"git add"
]
},
"remarkConfig": {
"plugins": [
"remark-preset-lint-recommended",
["remark-lint-maximum-line-length", 120],
["remark-lint-no-duplicate-headings", false],
"remark-lint-no-empty-sections"
]
},
"textlintConfig": {
"rules": {
"preset-ja-technical-writing": true,
"prh": {
"rulePaths": ["./prh-rules/technical-terms.yml"]
},
"write-good": {
"passive": false,
"illusion": true,
"so": true,
"thereIs": true,
"weasel": true
}
}
}
}
VSCode Configuration
{
// .vscode/settings.json
"markdownlint.config": {
"MD013": false,
"MD033": false,
"MD041": false
},
"files.associations": {
"*.md": "markdown"
},
"editor.rulers": [80, 120],
"editor.wordWrap": "bounded",
"editor.wordWrapColumn": 120,
"[markdown]": {
"editor.defaultFormatter": "DavidAnson.vscode-markdownlint",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.markdownlint": true
},
"editor.quickSuggestions": {
"comments": "off",
"strings": "off",
"other": "off"
}
},
"markdownlint.run": "onType",
"markdown-link-check.aliveStatusCodes": [200, 206],
// Extensions configuration
"markdown.extension.toc.updateOnSave": true,
"markdown.extension.print.absoluteImgPath": false,
// Spell checking
"cSpell.enabledLanguageIds": [
"markdown"
],
"cSpell.customDictionaries": {
"project-terms": {
"name": "project-terms",
"path": "./.vscode/project-dictionary.txt",
"addWords": true,
"scope": "workspace"
}
}
}
Integration with Content Workflows
Markdown error handling and validation integrate seamlessly with comprehensive content management systems. When combined with automated testing and quality assurance workflows, validation systems create robust content pipelines where errors are caught early, content quality remains consistent, and collaborative workflows maintain high editorial standards across distributed teams.
For enterprise content operations, validation systems work effectively with performance optimization strategies to ensure that quality assurance processes don’t compromise publishing speed, where validation runs efficiently in CI/CD pipelines, and content delivery maintains both accuracy and performance standards.
When building sophisticated documentation platforms, error handling complements workflow automation systems by enabling intelligent content processing where validation results trigger automated fixes, content routing decisions, and quality score calculations that maintain editorial excellence at scale.
Advanced Error Recovery and Reporting
Intelligent Error Resolution
Creating systems that can automatically resolve common formatting issues:
// error-recovery.js - Automated error resolution system
const fs = require('fs').promises;
const path = require('path');
class MarkdownErrorRecovery {
constructor(options = {}) {
this.config = {
autoFix: {
headingSpaces: true,
trailingWhitespace: true,
consecutiveBlankLines: true,
listIndentation: true,
codeBlockTermination: true,
linkFormatting: true,
tableAlignment: true
},
backup: {
enabled: options.createBackups !== false,
directory: options.backupDirectory || './.backup'
},
reporting: {
enabled: true,
format: options.reportFormat || 'detailed',
outputPath: options.reportPath || './error-recovery-report.json'
}
};
this.fixes = [];
this.errors = [];
}
async processFile(filePath) {
console.log(`🔧 Processing file for error recovery: ${filePath}`);
try {
const originalContent = await fs.readFile(filePath, 'utf8');
let fixedContent = originalContent;
const appliedFixes = [];
// Create backup if enabled
if (this.config.backup.enabled) {
await this.createBackup(filePath, originalContent);
}
// Apply fixes in order
if (this.config.autoFix.headingSpaces) {
const result = this.fixHeadingSpaces(fixedContent);
fixedContent = result.content;
appliedFixes.push(...result.fixes);
}
if (this.config.autoFix.trailingWhitespace) {
const result = this.fixTrailingWhitespace(fixedContent);
fixedContent = result.content;
appliedFixes.push(...result.fixes);
}
if (this.config.autoFix.consecutiveBlankLines) {
const result = this.fixConsecutiveBlankLines(fixedContent);
fixedContent = result.content;
appliedFixes.push(...result.fixes);
}
if (this.config.autoFix.listIndentation) {
const result = this.fixListIndentation(fixedContent);
fixedContent = result.content;
appliedFixes.push(...result.fixes);
}
if (this.config.autoFix.codeBlockTermination) {
const result = this.fixCodeBlockTermination(fixedContent);
fixedContent = result.content;
appliedFixes.push(...result.fixes);
}
if (this.config.autoFix.linkFormatting) {
const result = this.fixLinkFormatting(fixedContent);
fixedContent = result.content;
appliedFixes.push(...result.fixes);
}
if (this.config.autoFix.tableAlignment) {
const result = this.fixTableAlignment(fixedContent);
fixedContent = result.content;
appliedFixes.push(...result.fixes);
}
// Write fixed content if changes were made
if (fixedContent !== originalContent) {
await fs.writeFile(filePath, fixedContent);
console.log(`✅ Applied ${appliedFixes.length} fixes to ${filePath}`);
} else {
console.log(`ℹ️ No fixes needed for ${filePath}`);
}
// Record results
this.fixes.push({
file: filePath,
appliedFixes: appliedFixes,
originalSize: originalContent.length,
fixedSize: fixedContent.length
});
return {
success: true,
fixesApplied: appliedFixes.length,
content: fixedContent
};
} catch (error) {
console.error(`❌ Error processing ${filePath}:`, error.message);
this.errors.push({
file: filePath,
error: error.message
});
return {
success: false,
error: error.message
};
}
}
fixHeadingSpaces(content) {
const fixes = [];
const lines = content.split('\n');
const fixedLines = lines.map((line, index) => {
const headingMatch = line.match(/^(#{1,6})([^\s].*)/);
if (headingMatch) {
const hashes = headingMatch[1];
const text = headingMatch[2];
const fixed = `${hashes} ${text}`;
fixes.push({
type: 'heading_space_added',
line: index + 1,
original: line,
fixed: fixed
});
return fixed;
}
return line;
});
return {
content: fixedLines.join('\n'),
fixes
};
}
fixTrailingWhitespace(content) {
const fixes = [];
const lines = content.split('\n');
const fixedLines = lines.map((line, index) => {
if (line.match(/\s+$/)) {
const fixed = line.replace(/\s+$/, '');
fixes.push({
type: 'trailing_whitespace_removed',
line: index + 1,
charactersRemoved: line.length - fixed.length
});
return fixed;
}
return line;
});
return {
content: fixedLines.join('\n'),
fixes
};
}
fixConsecutiveBlankLines(content) {
const fixes = [];
const lines = content.split('\n');
const fixedLines = [];
let consecutiveBlankLines = 0;
let blankLineStart = -1;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.trim() === '') {
if (consecutiveBlankLines === 0) {
blankLineStart = i;
}
consecutiveBlankLines++;
// Keep maximum 2 consecutive blank lines
if (consecutiveBlankLines <= 2) {
fixedLines.push(line);
}
} else {
if (consecutiveBlankLines > 2) {
fixes.push({
type: 'excessive_blank_lines_removed',
startLine: blankLineStart + 1,
endLine: i,
removed: consecutiveBlankLines - 2
});
}
consecutiveBlankLines = 0;
fixedLines.push(line);
}
}
return {
content: fixedLines.join('\n'),
fixes
};
}
fixListIndentation(content) {
const fixes = [];
const lines = content.split('\n');
const fixedLines = lines.map((line, index) => {
const listMatch = line.match(/^(\s*)([\-\*\+]|\d+\.)\s/);
if (listMatch) {
const currentIndent = listMatch[1];
const marker = listMatch[2];
const restOfLine = line.substring(listMatch[0].length);
// Calculate proper indentation (multiples of 2 spaces)
const indentLevel = Math.floor(currentIndent.length / 2);
const properIndent = ' '.repeat(indentLevel);
if (currentIndent !== properIndent) {
const fixed = `${properIndent}${marker} ${restOfLine}`;
fixes.push({
type: 'list_indentation_fixed',
line: index + 1,
original: line,
fixed: fixed
});
return fixed;
}
}
return line;
});
return {
content: fixedLines.join('\n'),
fixes
};
}
fixCodeBlockTermination(content) {
const fixes = [];
const lines = content.split('\n');
let inCodeBlock = false;
let codeBlockStart = -1;
// Check for unterminated code blocks
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.startsWith('```')) {
if (inCodeBlock) {
// Closing code block
inCodeBlock = false;
codeBlockStart = -1;
} else {
// Opening code block
inCodeBlock = true;
codeBlockStart = i;
}
}
}
// Fix unterminated code block
if (inCodeBlock && codeBlockStart !== -1) {
lines.push('```');
fixes.push({
type: 'code_block_terminated',
startLine: codeBlockStart + 1,
addedLine: lines.length
});
}
return {
content: lines.join('\n'),
fixes
};
}
fixLinkFormatting(content) {
const fixes = [];
// Fix common link formatting issues
let fixedContent = content;
// Fix missing closing parenthesis
const missingClosingParen = /\[([^\]]+)\]\(([^)]+)(?!\))/g;
fixedContent = fixedContent.replace(missingClosingParen, (match, text, url) => {
fixes.push({
type: 'link_closing_paren_added',
original: match,
fixed: `[${text}](${url})`
});
return `[${text}](${url})`;
});
// Fix missing opening parenthesis after ]
const missingOpeningParen = /\[([^\]]+)\]([^(\s][^)]*)/g;
fixedContent = fixedContent.replace(missingOpeningParen, (match, text, url) => {
if (!url.startsWith('[')) { // Not a reference link
fixes.push({
type: 'link_opening_paren_added',
original: match,
fixed: `[${text}](${url})`
});
return `[${text}](${url})`;
}
return match;
});
return {
content: fixedContent,
fixes
};
}
fixTableAlignment(content) {
const fixes = [];
const lines = content.split('\n');
const fixedLines = [];
let inTable = false;
let tableRows = [];
let tableStart = -1;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (this.isTableRow(line)) {
if (!inTable) {
inTable = true;
tableStart = i;
tableRows = [];
}
tableRows.push({ index: i, content: line });
} else if (inTable) {
// End of table - process and fix alignment
const fixedTable = this.alignTable(tableRows);
if (fixedTable.wasFixed) {
fixes.push({
type: 'table_alignment_fixed',
startLine: tableStart + 1,
endLine: i,
rowsFixed: fixedTable.fixes.length
});
fixedLines.push(...fixedTable.rows);
} else {
fixedLines.push(...tableRows.map(row => row.content));
}
inTable = false;
tableRows = [];
fixedLines.push(line);
} else {
fixedLines.push(line);
}
}
// Handle table at end of file
if (inTable && tableRows.length > 0) {
const fixedTable = this.alignTable(tableRows);
if (fixedTable.wasFixed) {
fixes.push({
type: 'table_alignment_fixed',
startLine: tableStart + 1,
endLine: lines.length,
rowsFixed: fixedTable.fixes.length
});
fixedLines.push(...fixedTable.rows);
} else {
fixedLines.push(...tableRows.map(row => row.content));
}
}
return {
content: fixedLines.join('\n'),
fixes
};
}
isTableRow(line) {
const trimmed = line.trim();
return trimmed.includes('|') &&
(trimmed.startsWith('|') || trimmed.endsWith('|'));
}
alignTable(tableRows) {
if (tableRows.length < 2) {
return { rows: tableRows.map(r => r.content), wasFixed: false, fixes: [] };
}
// Parse all rows
const parsedRows = tableRows.map(row => {
const cells = row.content.split('|').map(cell => cell.trim());
return { ...row, cells };
});
// Find maximum width for each column
const maxColumns = Math.max(...parsedRows.map(row => row.cells.length));
const columnWidths = new Array(maxColumns).fill(0);
parsedRows.forEach(row => {
row.cells.forEach((cell, index) => {
if (index < columnWidths.length) {
columnWidths[index] = Math.max(columnWidths[index], cell.length);
}
});
});
// Rebuild rows with proper alignment
const fixedRows = [];
const fixes = [];
let wasFixed = false;
parsedRows.forEach(row => {
const alignedCells = row.cells.map((cell, index) => {
if (index < columnWidths.length) {
return cell.padEnd(columnWidths[index]);
}
return cell;
});
const fixedRow = `| ${alignedCells.join(' | ')} |`;
if (fixedRow !== row.content) {
wasFixed = true;
fixes.push({
line: row.index + 1,
original: row.content,
fixed: fixedRow
});
}
fixedRows.push(fixedRow);
});
return { rows: fixedRows, wasFixed, fixes };
}
async createBackup(filePath, content) {
if (!this.config.backup.enabled) return;
const backupDir = this.config.backup.directory;
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const fileName = path.basename(filePath);
const backupFileName = `${fileName}.${timestamp}.backup`;
const backupPath = path.join(backupDir, backupFileName);
// Ensure backup directory exists
await fs.mkdir(backupDir, { recursive: true });
// Write backup file
await fs.writeFile(backupPath, content);
console.log(`📁 Backup created: ${backupPath}`);
}
async generateReport() {
const report = {
timestamp: new Date().toISOString(),
summary: {
filesProcessed: this.fixes.length,
filesWithErrors: this.errors.length,
totalFixes: this.fixes.reduce((sum, f) => sum + f.appliedFixes.length, 0),
successRate: this.fixes.length / (this.fixes.length + this.errors.length) * 100
},
fixes: this.fixes,
errors: this.errors,
configuration: this.config
};
if (this.config.reporting.enabled) {
await fs.writeFile(
this.config.reporting.outputPath,
JSON.stringify(report, null, 2)
);
console.log(`📊 Error recovery report saved: ${this.config.reporting.outputPath}`);
}
return report;
}
printSummary() {
const totalFixes = this.fixes.reduce((sum, f) => sum + f.appliedFixes.length, 0);
console.log('\n' + '='.repeat(60));
console.log('🔧 ERROR RECOVERY SUMMARY');
console.log('='.repeat(60));
console.log(`📁 Files processed: ${this.fixes.length}`);
console.log(`✅ Total fixes applied: ${totalFixes}`);
console.log(`❌ Files with errors: ${this.errors.length}`);
if (this.fixes.length > 0) {
console.log('\n📋 FIXES BY TYPE:');
const fixTypes = {};
this.fixes.forEach(file => {
file.appliedFixes.forEach(fix => {
fixTypes[fix.type] = (fixTypes[fix.type] || 0) + 1;
});
});
Object.entries(fixTypes).forEach(([type, count]) => {
console.log(` • ${type}: ${count}`);
});
}
console.log('\n' + '='.repeat(60));
}
}
module.exports = MarkdownErrorRecovery;
// CLI usage
if (require.main === module) {
const recovery = new MarkdownErrorRecovery({
createBackups: true,
reportFormat: 'detailed'
});
const targetPath = process.argv[2] || './';
// Process files recursively
const processDirectory = async (dirPath) => {
const items = await fs.readdir(dirPath, { withFileTypes: true });
for (const item of items) {
const fullPath = path.join(dirPath, item.name);
if (item.isDirectory() && !item.name.startsWith('.')) {
await processDirectory(fullPath);
} else if (item.isFile() &&
(item.name.endsWith('.md') || item.name.endsWith('.markdown'))) {
await recovery.processFile(fullPath);
}
}
};
processDirectory(targetPath)
.then(() => {
recovery.printSummary();
return recovery.generateReport();
})
.then(() => {
console.log('🎉 Error recovery completed successfully!');
})
.catch(error => {
console.error('❌ Error recovery failed:', error);
process.exit(1);
});
}
Conclusion
Markdown error handling and validation represent essential components of professional content management systems that ensure consistent quality, reduce maintenance overhead, and enable confident collaborative workflows. By implementing comprehensive validation systems, automated error recovery, and integrated quality assurance processes, technical teams can build robust content pipelines that catch errors early, maintain editorial standards, and deliver reliable content experiences across all platforms and publishing contexts.
The key to successful Markdown validation lies in balancing automated checking with editorial flexibility, implementing validation rules that serve content goals rather than hindering productivity, and creating feedback loops that help content creators learn and improve their Markdown skills over time. Whether you’re managing technical documentation, educational content, or collaborative knowledge bases, the validation techniques and error handling strategies covered in this guide provide the foundation for building maintainable, high-quality content systems.
Remember to tailor validation rules to your team’s specific needs and content requirements, implement gradual rollouts of new validation standards to minimize disruption, and continuously monitor validation effectiveness to ensure that quality assurance processes enhance rather than impede content creation workflows. With proper implementation of comprehensive error handling and validation systems, your Markdown content can maintain professional quality standards while preserving the simplicity and collaborative benefits that make Markdown an ideal format for modern technical communication.