.--------------. .------------. .--------------. .--------------. .-----------. .--------------.
| __ | | _______ | | ______ | | _________ | | _____ | | ____ ____ |
| / \ | | / ___ | | | .' ___ | | | |_ ___ | | | |_ _| | | |_ _||_ _| |
| / /\ \ | | | (__ \_| | | / .' \_| | | | |_ \_| | | | | | | \ \ / / |
| / ____ \ | | '.___`-. | | | | | | | _| | | | | | | > `' < |
| _/ / \ \_ | | |`\____) | | | \ `.___.'\ | | _| |_ | | _| |_ | | _/ /'`\ \_ |
||____| |____|| | |_______.' | | `._____.' | | |_____| | | |_____| | | |____||____| |
| | | | | | | | | | | |
'--------------' '------------' '--------------' '--------------' '-----------' '--------------'
║ ║ ║ ║ ║ ║
╚════════════════╩════════════════╩═══════════════╩═════════════════╩══════════════╝
Automatic ASCII diagram repair tool for Markdown files
ascfix fixes misaligned ASCII diagrams in Markdown, especially those generated by LLMs. It normalizes box widths, aligns arrows, enforces uniform padding, and ensures diagrams remain maintainable.
Ideal for:
- AI-generated documentation with ASCII diagrams
- Markdown files with workflow diagrams
- CI/CD pipeline documentation
- Architecture diagrams in code repositories
ascfix prioritizes safety and reliability with a conservative approach:
- Content preservation - designed to never delete or modify unintended text
- Non-destructive processing - safe to run on Markdown files
- Idempotent operations - running multiple times produces consistent results for simple diagrams (see Known Limitations)
- Conservative default - when uncertain, preserves original formatting
- Automated quality validation - comprehensive test suite with 10+ passing unit fixtures
- Semantic transformation analysis - distinguishes beneficial improvements from potential corruption
- Context-aware processing - understands diagram elements vs. regular text
- Active development - complex nested structures handled conservatively
- Consistent formatting - uniform spacing, proper alignment for simple and moderate diagrams
- Basic nested box support - single-level nesting works reliably
- Arrow alignment - intelligent positioning for standard workflows
- Conservative mode - deeply nested (3+ levels) or complex overlapping structures preserved unchanged
- Diagram mode (default): Repair ASCII boxes, arrows, and diagram structures
- Safe mode: Normalize Markdown tables only - safest for any file
- Check mode: Validate without modifying (exit code 1 if changes needed - perfect for CI/CD)
- All mode: Shorthand for
--fences --mode=diagramto repair everything
- Box style preservation: Single-line (
┌┐└┘│─), double-line (╔╗╚╝║═), and rounded (╭╮╰╯│─) boxes - Box width normalization: Expands boxes to fit content with uniform padding
- Side-by-side balancing: Equalizes widths of adjacent boxes for visual consistency
- Nested box support: Parent boxes expand to properly contain children with margins
- Arrow alignment: Aligns vertical arrows to box centers, horizontal arrows to edges
- Connection lines: L-shaped paths with corner connectors
- Label preservation: Maintains text labels attached to primitives
- Table normalization: Aligns columns and fixes hard-wrapped cells
- List normalization: Standardizes bullet styles (
- * +→-), fixes indentation, adds spacing - Link preservation: Handles URLs with parentheses and reference-style links
- Fence repair: Fixes mismatched fence lengths, unclosed blocks, duplicate closing fences
- Directory processing: Recursively process directories with automatic file discovery
- Multiple extensions: Process
.md,.mdx,.txtfiles (customizable with--ext) - Smart filtering: Automatically skips hidden directories,
node_modules,target, build output - Size limits: Configurable max file size (
--max-size 5MB) - In-place editing: Modify files directly with
--in-place(default: stdout output) - Error resilience: Continue processing remaining files on individual errors
- Conservative: Only fixes well-understood structures; unknown patterns preserved unchanged
- Idempotent: Running twice produces identical output (safe for repeated application)
- No panics: Handles malformed input gracefully without crashes
- Zero unsafe code: Compile-time enforced with
#![forbid(unsafe_code)] - No data loss: Never deletes or rewords content - only improves formatting
- Fast: Linear O(n) processing time suitable for large files and CI/CD
- Exit codes: 0 = success/no changes needed, 1 = check failed or error (CI/CD friendly)
- GitHub Actions ready: Verified workflows for testing, linting, security auditing
- Configuration file support:
.ascfix.tomlfor project-wide settings
The simplest way to install ascfix is via crates.io:
cargo install ascfixTo build from the latest development version:
git clone https://github.com/evoludigit/ascfix.git
cd ascfix
cargo install --path .Permission denied when installing:
If you see Permission denied (os error 13) during installation, your system likely has /tmp mounted with noexec (a security configuration that prevents executing files from temporary directories). Set a build directory:
CARGO_TARGET_DIR=~/.cargo/build cargo install ascfixOr build from source:
git clone https://github.com/evoludigit/ascfix.git
cd ascfix
cargo install --path .# Fix a file (default: diagram mode, output to stdout)
ascfix README.md
# Fix a file in place (default: diagram mode)
ascfix README.md --in-place
# Only normalize tables and lists (safe mode)
ascfix README.md --in-place --mode safe
# Repair code fence boundaries
ascfix README.md --in-place --fences
# Repair everything (diagram mode + fence repair)
ascfix README.md --in-place --all
# Validate without modifying (exit code 1 if changes needed)
ascfix README.md --check# Process all .md and .mdx files in a directory
ascfix docs/ --in-place --mode=diagram
# Process a directory recursively (skips hidden dirs, node_modules, target, etc.)
ascfix . --in-place --all
# Process only .md files (default includes .md, .mdx, .txt)
ascfix docs/ -e .md --in-place
# Process .mdx files only
ascfix docs/ -e .mdx --in-place
# Complex filtering with find (exclude specific paths)
ascfix $(find . -name "*.md" -not -path "*/node_modules/*" -not -path "*/.git/*") --in-place
# Or use fd (faster)
ascfix $(fd -e md) --in-place
# Check multiple files without modifying
ascfix docs/ --check| Mode | Description | Use Case |
|---|---|---|
safe |
Fix only Markdown tables | Conservative, safe for any file |
diagram |
Fix boxes and arrows | For files with ASCII diagrams |
check |
Validate without writing | CI/CD validation |
| Flag | Short | Description | Default |
|---|---|---|---|
--in-place |
-i |
Modify files instead of printing to stdout | Off |
--check |
-c |
Validate files without modifying (returns exit code 1 if changes needed) | Off |
--fences |
Repair code fence boundaries | Off | |
--all |
Shorthand for --fences --mode=diagram |
Off | |
--mode |
Processing mode (safe, diagram, check) | safe | |
--ext |
-e |
File extensions to process (comma-separated, e.g., .md,.mdx) |
.md,.mdx |
--max-size |
Maximum file size to process (e.g., "100MB", "1GB") | Unlimited |
| Flag | Short | Description | Use Case |
|---|---|---|---|
--summary |
Print processing statistics (files processed, modified, errors) | Quick overview | |
--list-files |
Output only filenames that need fixing (one per line) | CI/CD, scripting | |
--verbose |
-v |
Show detailed processing information with colored output | Debugging, understanding |
--json |
Output results as JSON (machine-readable) | AI/programmatic use | |
--diff |
Show unified diff of changes without modifying files | Preview changes |
ascfix supports HTML comment markers to exclude specific content from processing. This is useful for:
- Preserving intentional ASCII art that shouldn't be "fixed"
- Protecting decorative elements like logos or banners
- Excluding complex diagrams that should remain unchanged
Supported marker syntax:
<!-- ascfix:ignore -->
[content to ignore]
<!-- /ascfix:ignore -->Or alternatively:
<!-- ascfix-ignore-start -->
[content to ignore]
<!-- ascfix-ignore-end -->Example:
<!-- ascfix:ignore -->
```
.----------------. .----------------.
| .--------------. || .--------------. |
| | LOGO | || | HERE | |
| '--------------' || '--------------' |
'----------------' '----------------'
```
<!-- /ascfix:ignore -->
This box will be processed:
┌──────┐
│ Test │
└──────┘Content inside ignore markers is completely skipped by ascfix, regardless of mode. Ignore markers work with:
- Code fences (content inside fences within ignore blocks is preserved)
- Inline diagrams
- Tables
- Any other Markdown content
Important: Ignore markers are removed from the output (they're HTML comments, so they won't appear in rendered Markdown).
Before (inconsistent column widths):
| Name | Age | City |
|---------|-----|----------|
| Alice | 30 | New York |
| Bob | 25 | Boston |
| Charlie | 35 | Chicago |After (normalized columns):
| Name | Age | City |
|---------|-----|----------|
| Alice | 30 | New York |
| Bob | 25 | Boston |
| Charlie | 35 | Chicago |Command:
ascfix table.md --in-placeBefore (mismatched fence lengths):
```python
def hello():
print("Hello, World!")
`````After (balanced fences):
`````python
def hello():
print("Hello, World!")
`````Also handles:
- Unclosed fences (adds closing fence)
- Inconsistent lengths (uses longest)
- Preserves language specifiers
- Skips type mismatches (conservative approach)
Command:
ascfix docs/*.md --in-place --fencesBefore (misaligned, inconsistent):
↓
┌──────┐
│ Step │
└──────┘
↓
┌─────────┐
│ Process │
└─────────┘After (normalized):
↓
┌────────┐
│ Step │
└────────┘
↓
┌────────┐
│ Process│
└────────┘Command:
ascfix workflow.md --in-place --mode=diagramBefore (text too close to border):
┌────────┐
│Process │
└────────┘After (proper padding):
┌─────────┐
│ Process │
└─────────┘Before (AI-generated, misaligned):
Source Code
↓
┌──────────────────┐
│ Build & Test │
└──────────────────┘
↓
┌────────┐
│ Deploy │
└────────┘After (normalized, consistent):
Source Code
↓
┌─────────────────┐
│ Build & Test │
└─────────────────┘
↓
┌──────────────────┐
│ Deploy │
└──────────────────┘Before (LLM-generated, text cramped against borders, arrows misaligned):
┌───────┐ ┌────────┐
│Process│ │Database│
└───────┘ └────────┘
↓ ↓
┌──────────────────┐
│ResultProcessor │
└──────────────────┘After (proper padding added, arrows aligned to box centers):
┌─────────┐ ┌──────────┐
│ Process │ │ Database │
└─────────┘ └──────────┘
│ │
┌────────────────────────┐
│ Result Processor │
└────────────────────────┘Changes made:
- Expanded boxes to fit content with 1-space padding
- Aligned arrows to box center columns
- Normalized interior spacing
Command:
ascfix workflow.md --in-place --mode=diagramBefore (arrows at inconsistent positions, no padding):
Source Code
↓
┌──────────┐
│Build │
└──────────┘
↓
┌─────────────┐
│Test Suite │
└─────────────┘
↓
┌──────┐
│Deploy│
└──────┘After (arrows normalized to consistent columns, boxes expanded with padding):
Source Code
│
┌──────────────┐
│ Build │
└──────────────┘
│
┌───────────────────┐
│ Test Suite │
└───────────────────┘
│
┌────────────┐
│ Deploy │
└────────────┘Changes made:
- Aligned all vertical arrows to consistent column
- Added interior padding to all boxes (text no longer touching borders)
- Expanded boxes to fit content with proper spacing
Command:
ascfix pipeline.md --in-place --mode=diagramBefore (arrows offset from box edges, inconsistent padding):
┌──────┐
│Input │
└──────┘
──→
┌─────────┐
│Processing│
└─────────┘
──→
┌──────┐
│Output│
└──────┘After (arrows snapped to box edges, uniform padding):
┌────────┐
│ Input │
└────────┘
→
┌───────────────┐
│ Processing │
└───────────────┘
→
┌────────┐
│ Output │
└────────┘Changes made:
- Expanded boxes to fit content with padding
- Snapped arrow endpoints to box edge columns
- Made padding uniform (1 space) inside boxes
Command:
ascfix workflow.md --in-place --mode=diagramValidate without modifying (useful for pull requests):
# Exit 0 if file is already normalized
# Exit 1 if file needs fixing
ascfix docs/*.md --check --mode=diagramPerfect for GitHub Actions:
- name: Check diagram alignment
run: ascfix docs/*.md --check --mode=diagram
# Fails the build if any diagrams need fixingSummary mode - quick statistics:
ascfix docs/ --summary --in-place --mode=diagramSummary:
Total files processed: 15
Modified: 8
Unchanged: 7
Errors: 0
List files mode - for CI/CD scripts:
# Get list of files that need fixing
files=$(ascfix docs/ --check --list-files --mode=diagram)
if [ -n "$files" ]; then
echo "Files need fixing:"
echo "$files"
exit 1
fiJSON mode - for AI/programmatic use:
ascfix docs/ --json --check --mode=diagram{
"files": [
{
"status": "modified",
"file": "docs/api.md"
},
{
"status": "unchanged",
"file": "docs/guide.md"
}
],
"stats": {
"total": 2,
"modified": 1,
"unchanged": 1,
"errors": 0,
"skipped": 0
}
}Diff mode - preview changes:
ascfix workflow.md --diff --mode=diagramdiff workflow.md
# Workflow
┌──────┐
-│Process│
+│ Process │
└──────┘Verbose mode - debugging:
ascfix docs/ --verbose --in-place --mode=diagram[verbose] Found 3 files to process
[success] Modified: docs/api.md
[verbose] Unchanged: docs/guide.md
[success] Modified: docs/workflow.md
Combined modes - maximum visibility:
ascfix docs/ --verbose --summary --in-place --mode=diagramAll examples below are verified test fixtures from our test suite (see tests/data/). Each example demonstrates specific features with real before/after transformations.
ascfix detects and preserves different box drawing styles:
Single-line boxes (┌ ┐ └ ┘ │ ─):
┌────────┐
│ Single │
└────────┘
Double-line boxes (╔ ╗ ╚ ╝ ║ ═):
╔════════════════╗
║ Double-Line ║
║ Box Style ║
╚════════════════╝
Rounded-corner boxes (╭ ╮ ╰ ╯):
╭────────────────╮
│ Rounded │
│ Corner Box │
╰────────────────╯
ascfix automatically normalizes widths of horizontally adjacent boxes:
Before:
┌───────┐ ┌─────┐ ┌────────────┐
│ Box 1 │ │ B2 │ │ Box Three │
└───────┘ └─────┘ └────────────┘
After:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Box 1 │ │ B2 │ │ Box Three │
└─────────────┘ └─────────────┘ └─────────────┘
Parent boxes expand to properly contain children with appropriate margins:
Before:
┌──────────────┐
│ Parent │
│ ┌────────┐ │
│ │ Child │ │
│ └────────┘ │
└──────────────┘
After:
┌────────────────────┐
│ Parent │
│ │
│ ┌──────────────┐ │
│ │ Child │ │
│ └──────────────┘ │
│ │
└────────────────────┘
Real-world pipeline with consistent arrow alignment:
┌──────────────┐
│ Source Code │
└──────────────┘
↓
┌──────────────┐
│ Build Job │
└──────────────┘
↓
┌──────────────┐
│ Test Suite │
└──────────────┘
↓
┌──────────────┐
│ Deploy │
└──────────────┘
API documentation with mixed diagrams and tables:
┌─────────────────┐ ┌─────────────────┐
│ Client App │ │ API Gateway │
│ │ │ │
│ - Mobile App │ │ - Authentication │
│ - Web App │ │ - Rate Limiting │
└─────────┬───────┘ └─────────┬───────┘
│ │
▼ ▼
The project includes 55+ test fixtures organized in tests/data/:
Verified Working Examples:
- Safe mode fixtures: Table normalization, list formatting, link preservation
- Basic diagrams: Simple boxes, arrows, side-by-side alignment
- Standard workflows: CI/CD pipelines, simple nested structures
Edge Cases & Error Handling:
- Malformed inputs: Broken tables, wrapped cells
- Boundary conditions: Empty files, minimal diagrams
- Real-world patterns: API docs, ML pipelines (in conservative mode)
Conservative Mode Active: Some complex diagram fixtures (deep nesting, overlapping elements, mixed styles with labels) exercise the tool's conservative behavior. When structures are ambiguous or highly complex, ascfix preserves the original formatting unchanged rather than risk corruption. This prioritizes safety and data preservation over aggressive transformation.
Verify examples yourself:
# Run all tests to verify fixtures
cargo test
# Process a specific file
ascfix README.md --mode=diagramtests/data/unit/- Simple, focused examplestests/data/integration/- Complex real-world scenariostests/data/integration/dirty/- Malformed inputs and error cases
Every example in this README can be verified:
# Run tests to verify all fixtures
cargo test
# Process a specific fixture
ascfix tests/data/unit/input/ci_pipeline.md --mode=diagramascfix supports multiple box drawing styles and automatically preserves them:
Single-line boxes (default):
┌─────┐
│ Box │
└─────┘
Double-line boxes:
╔═════╗
║ Box ║
╚═════╝
Rounded-corner boxes:
╭─────╮
│ Box │
╰─────╯
All styles are detected, preserved, and normalized consistently. Mixed styles in the same diagram are handled correctly.
ascfix detects and supports basic parent-child box relationships. Single-level nesting works reliably:
Before (parent too small):
┌──────────────┐
│ Parent │
│ ┌────────┐ │
│ │ Child │ │
│ └────────┘ │
└──────────────┘
After (parent expanded with margins):
┌────────────────────┐
│ Parent │
│ │
│ ┌──────────────┐ │
│ │ Child │ │
│ └──────────────┘ │
│ │
└────────────────────┘
Important: Complex nested structures (>2 levels deep) or diagrams with overlapping elements are currently handled in conservative mode - they are preserved unchanged rather than risk corruption. This ensures safety over aggressive transformation.
ascfix automatically balances the width of horizontally adjacent boxes for visual consistency:
Before (inconsistent widths):
┌───────┐ ┌─────┐ ┌────────────┐
│ Box 1 │ │ B2 │ │ Box Three │
└───────┘ └─────┘ └────────────┘
After (uniform widths):
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Box 1 │ │ B2 │ │ Box Three │
└─────────────┘ └─────────────┘ └─────────────┘
ascfix recognizes and normalizes various arrow styles:
- Standard arrows:
→ ↓ ↑ ← - Double arrows:
⇒ ⇓ ⇑ ⇐ - Extended arrows:
⟶ ⟹
All arrows are aligned to box centers and maintained at consistent columns throughout the diagram.
ascfix supports L-shaped connection paths with limited scope (4 segments maximum per connection):
┌────────┐
│ Start │
└───┬────┘
│
└─────┬─────────┐
│ │
┌───▼───┐ ┌──▼──┐
│ Path1 │ │Path2│
└───────┘ └─────┘
Connection detection is conservative to avoid false positives. Very complex paths are skipped.
ascfix includes framework support for preserving text labels attached to primitives during normalization. Label detection and rendering are currently conservative (skeleton implementation):
- Labels attached to boxes are tracked
- Labels attached to arrows are preserved
- Relative offsets are maintained during normalization
- Collision detection prevents label-diagram overlap
ascfix is designed to be idempotent (running multiple times produces the same result), and this works reliably for most use cases:
✅ Fully Idempotent:
- Simple ASCII boxes and arrows
- Markdown tables in Safe mode
- Single-level nested boxes
- Standard list formatting
Why This Happens:
- First pass: Parent box expands to fit children
- Second pass: Detection sees new box dimensions and may recalculate relationships
- For deeply nested hierarchies (3+ levels), this can cascade
Recommended Approach:
- Run ascfix once on complex diagrams
- Review the output
- Commit the normalized version
- For table-only processing, use
--mode=safewhich is always idempotent
For technical details about the detection phase and spatial relationship algorithms, see ARCHITECTURE.md. For workarounds and testing strategies, see LIBRARY_USAGE.md.
# Run all tests
cargo test
# Check code quality
cargo clippy --all-targets --all-features -- -D warnings
# Build release binary
cargo build --releaseAll code is tested with TDD discipline - unit tests, integration tests, and golden file tests. Test data is organized in tests/data/ with clear separation between unit and integration test cases.
ascfix is designed for easy integration into scripts, AI agents, and CI/CD pipelines.
Use JSON mode for structured output:
# Get machine-readable results
ascfix docs/ --json --check --mode=diagram > results.jsonParse the JSON to understand what changed:
{
"files": [
{
"status": "modified",
"file": "docs/workflow.md"
},
{
"status": "error",
"file": "docs/broken.md",
"error": "Failed to read: Permission denied"
}
],
"stats": {
"total": 2,
"modified": 1,
"errors": 1
}
}Use list-files mode for simple pipelines:
#!/bin/bash
# Fix all files that need it
for file in $(ascfix docs/ --list-files --check --mode=diagram); do
echo "Fixing: $file"
ascfix "$file" --in-place --mode=diagram
doneUse exit codes for CI/CD:
# Exit 0 if all files are clean, 1 if fixes needed
ascfix docs/ --check --mode=diagram
if [ $? -ne 0 ]; then
echo "⚠️ Diagrams need normalization"
exit 1
fiimport subprocess
import json
result = subprocess.run(
["ascfix", "docs/", "--json", "--check", "--mode=diagram"],
capture_output=True,
text=True
)
data = json.loads(result.stdout)
modified_files = [f["file"] for f in data["files"] if f["status"] == "modified"]
print(f"Files needing fixes: {modified_files}")name: Validate Diagrams
on: [pull_request]
jobs:
check-diagrams:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install ascfix
run: cargo install ascfix
- name: Check diagrams
run: |
ascfix docs/ --check --summary --mode=diagram
if [ $? -ne 0 ]; then
echo "::error::Diagrams need normalization. Run: ascfix docs/ --in-place --mode=diagram"
exit 1
fi- CONFIG.md - Configuration options, file formats, and examples
- LIBRARY_USAGE.md - Using ascfix as a Rust library, API documentation, and integration examples
- ARCHITECTURE.md - Design overview, module structure, and data flow
- SECURITY.md - Security policies, audit status, and best practices
- CONTRIBUTING.md - Development guidelines and contribution process
- CHANGELOG.md - Version history and release notes
- tests/README.md - Test data organization and testing approach
Licensed under the MIT License (LICENSE or https://opensource.org/licenses/MIT)
Contributions are welcome! Please feel free to submit pull requests.