This is the abridged developer documentation for redlines
# Quickstart
> Install redlines and compare two texts from the CLI or from Python.
`redlines` compares two strings and produces structured output showing their differences. Changes are represented with strike-throughs and highlights, in the manner of Microsoft Word’s track changes, and the output carries change information, positions and statistics for programmatic use. ## Install [Section titled “Install”](#install)
```bash
pip install redlines
```
Python 3.10 to 3.14 are supported. ### Optional extras [Section titled “Optional extras”](#optional-extras) | Extra | Install | What it adds | | ------------- | ----------------------------------- | ----------------------------------------------------------------------------------------- | | `pdf` | `pip install redlines[pdf]` | Comparing PDF files | | `nupunkt` | `pip install redlines[nupunkt]` | Sentence boundary detection that handles abbreviations, citations and URLs (Python 3.11+) | | `levenshtein` | `pip install redlines[levenshtein]` | Levenshtein distance in the statistics | ## Compare from the command line [Section titled “Compare from the command line”](#compare-from-the-command-line) JSON is the default output, so a bare invocation is enough:
```bash
redlines "The quick brown fox jumps over the lazy dog." "The quick brown fox walks past the lazy dog."
```
Files work the same way, and `--pretty` makes the JSON readable:
```bash
redlines --pretty old_version.txt new_version.txt
```
## Compare from Python [Section titled “Compare from Python”](#compare-from-python)
```python
from redlines import Redlines
test = Redlines(
"The quick brown fox jumps over the lazy dog.",
"The quick brown fox walks past the lazy dog.",
markdown_style="none",
)
print(test.output_markdown)
# The quick brown fox jumps over walks past the lazy dog.
```
Other output formats are available on the same object: `output_json` for structured changes and statistics, `output_rich` for terminal display, and `compare(markdown_style=...)` for the six markdown styles. ## Where to go next [Section titled “Where to go next”](#where-to-go-next) * The [agent integration guide](/redlines/guides/agent-guide/) covers invocation from agents and automation, the JSON structure, error handling and integration patterns. * The [API reference](/redlines/api/) is generated from the docstrings and documents every class and method. * The [decision records](/redlines/project/adr/) explain why the library is shaped the way it is, and where it is going. ## Reading this site as an agent [Section titled “Reading this site as an agent”](#reading-this-site-as-an-agent) Every page is available as plain markdown by appending `.md` to its path — [`/guides/agent-guide.md`](/redlines/guides/agent-guide.md), for instance. [`/llms.txt`](/redlines/llms.txt) indexes the site, [`/llms-small.txt`](/redlines/llms-small.txt) is the usage documentation in one file, and [`/llms-full.txt`](/redlines/llms-full.txt) adds the planning documents and decision records.
# Agent integration guide
> Invocation, output formats, JSON structure, error handling and integration patterns for calling redlines from agents and automation.
This is the 0.6 guide It documents the current release and is accurate for it. For 1.0 it is being replaced by a short contract page plus task pages whose code comes from `examples/`, with the machine-readable parts — schemas, `llms.txt`, the MCP tool descriptions — carrying what prose used to. See [ADR-0027](/redlines/project/adr/0027-agent-docs-machine-surface/). ## Quick Start [Section titled “Quick Start”](#quick-start) ### 🤖 Agent-Friendly CLI (New!) [Section titled “🤖 Agent-Friendly CLI (New!)”](#-agent-friendly-cli-new) For maximum simplicity, you can now invoke redlines without specifying a command. It automatically outputs JSON (the most agent-friendly format):
```bash
# Simplest invocation - just provide two strings/files
redlines "source text" "test text"
# Pretty-print for readability
redlines --pretty "source text" "test text"
# Works with files too
redlines old_version.txt new_version.txt
```
**Why this is better for agents:** * No need to choose between `text`, `json`, `markdown`, `stats` commands upfront * JSON output is structured and parseable * Consistent, predictable behavior * Fewer tokens needed in prompts **Traditional commands still work** if you need specific output formats (see [Output Formats](#output-formats)). ### Installation [Section titled “Installation”](#installation)
```bash
# Basic installation
pip install redlines
# With PDF file comparison support
pip install redlines[pdf]
# With advanced sentence tokenization (Python 3.11+)
pip install redlines[nupunkt]
# With Levenshtein distance metrics
pip install redlines[levenshtein]
```
### Your First Comparison (30 seconds) [Section titled “Your First Comparison (30 seconds)”](#your-first-comparison-30-seconds)
```python
from redlines import Redlines
# Compare two strings
diff = Redlines(
"The quick brown fox jumps over the lazy dog.",
"The quick brown fox walks past the lazy dog."
)
# Get markdown output
print(diff.output_markdown)
# Output: The quick brown fox jumps over walks past the lazy dog.
```
### CLI Quick Start [Section titled “CLI Quick Start”](#cli-quick-start)
```bash
# Compare strings (command-less, outputs JSON by default)
redlines "Hello world" "Hi world"
# Pretty-print JSON output
redlines --pretty "Hello world" "Hi world"
# Compare files (auto-detected, command-less)
redlines old_version.txt new_version.txt
# Or use explicit commands for specific output formats
redlines text "Hello world" "Hi world"
redlines json old_version.txt new_version.txt --pretty
redlines markdown file1.txt file2.txt --markdown-style ghfm
# Check if files differ (for CI/CD)
if redlines file1.txt file2.txt > /dev/null 2>&1; then
echo "Files have changes"
else
echo "Files are identical"
fi
```
*** ## Common Patterns [Section titled “Common Patterns”](#common-patterns) ### Pattern 1: Compare Two Files [Section titled “Pattern 1: Compare Two Files”](#pattern-1-compare-two-files)
```python
from pathlib import Path
from redlines import Redlines
# Read files
source = Path("old_version.txt").read_text()
test = Path("new_version.txt").read_text()
# Compare
diff = Redlines(source, test)
# Get results
print(f"Total changes: {diff.stats().total_changes}")
print(diff.output_markdown)
```
### Pattern 1b: Compare PDF Files [Section titled “Pattern 1b: Compare PDF Files”](#pattern-1b-compare-pdf-files)
```python
from redlines import Redlines
from redlines.pdf import PDFFile
# Load PDF files (requires: pip install redlines[pdf])
source = PDFFile("contract_v1.pdf")
test = PDFFile("contract_v2.pdf")
# Compare
diff = Redlines(source, test)
# Get results
print(f"Total changes: {diff.stats().total_changes}")
print(diff.output_markdown)
# Access page information
print(f"Source has {source.page_count} pages")
for page in source.pages:
print(f"Page {page.page_number}: {len(page.text)} chars")
```
```bash
# CLI: PDF files are auto-detected
redlines contract_v1.pdf contract_v2.pdf --pretty
```
### Pattern 2: Get Machine-Readable JSON [Section titled “Pattern 2: Get Machine-Readable JSON”](#pattern-2-get-machine-readable-json)
```python
import json
from redlines import Redlines
diff = Redlines(source, test)
# Get JSON output
json_output = diff.output_json(pretty=True)
data = json.loads(json_output)
# Process changes
for change in data["changes"]:
print(f"{change['type']}: {change.get('source_text', '')} → {change.get('test_text', '')}")
```
### Pattern 3: Filter Specific Operations [Section titled “Pattern 3: Filter Specific Operations”](#pattern-3-filter-specific-operations)
```python
from redlines import Redlines
diff = Redlines(source, test)
# Get only insertions
insertions = diff.get_changes(operation="insert")
for change in insertions:
print(f"Added: {change.test_text}")
# Get only deletions
deletions = diff.get_changes(operation="delete")
for change in deletions:
print(f"Removed: {change.source_text}")
# Get only replacements
replacements = diff.get_changes(operation="replace")
for change in replacements:
print(f"Changed: {change.source_text} → {change.test_text}")
```
### Pattern 4: Collect Statistics [Section titled “Pattern 4: Collect Statistics”](#pattern-4-collect-statistics)
```python
from redlines import Redlines
diff = Redlines(source, test)
stats = diff.stats()
print(f"Total changes: {stats.total_changes}")
print(f"Insertions: {stats.insertions}")
print(f"Deletions: {stats.deletions}")
print(f"Replacements: {stats.replacements}")
print(f"Change ratio: {stats.change_ratio:.1%}")
print(f"Characters added: {stats.chars_added}")
print(f"Characters deleted: {stats.chars_deleted}")
print(f"Net change: {stats.chars_net_change}")
```
### Pattern 5: Batch Process Multiple Files [Section titled “Pattern 5: Batch Process Multiple Files”](#pattern-5-batch-process-multiple-files)
```python
from pathlib import Path
from redlines import Redlines
def compare_directory(dir1: Path, dir2: Path):
"""Compare all matching files in two directories."""
results = []
for file1 in dir1.glob("*.txt"):
file2 = dir2 / file1.name
if not file2.exists():
continue
diff = Redlines(
file1.read_text(),
file2.read_text()
)
stats = diff.stats()
if stats.total_changes > 0:
results.append({
"file": file1.name,
"changes": stats.total_changes,
"change_ratio": stats.change_ratio
})
return results
# Usage
results = compare_directory(Path("old/"), Path("new/"))
for result in results:
print(f"{result['file']}: {result['changes']} changes ({result['change_ratio']:.1%})")
```
### Pattern 6: Generate HTML Report [Section titled “Pattern 6: Generate HTML Report”](#pattern-6-generate-html-report)
```python
from redlines import Redlines
diff = Redlines(source, test, markdown_style="none")
html_template = f"""
Diff Report
{diff.output_markdown}
"""
Path("report.html").write_text(html_template)
```
*** ## Output Formats [Section titled “Output Formats”](#output-formats) ### Markdown Styles [Section titled “Markdown Styles”](#markdown-styles)
```python
from redlines import Redlines
from redlines.enums import MarkdownStyle
# Available styles:
styles = {
"red_green": MarkdownStyle.RED_GREEN, # Red strikethrough + green bold (default)
"none": MarkdownStyle.NONE, # Plain / HTML tags
"red": MarkdownStyle.RED, # All changes in red
"ghfm": MarkdownStyle.GHFM, # GitHub Flavored Markdown
"bbcode": MarkdownStyle.BBCODE, # BBCode format
"streamlit": MarkdownStyle.STREAMLIT, # Streamlit-compatible
}
# Use a style
diff = Redlines(source, test, markdown_style=MarkdownStyle.GHFM)
print(diff.output_markdown)
```
### Rich Terminal Output [Section titled “Rich Terminal Output”](#rich-terminal-output)
```python
from redlines import Redlines
from rich import print as rprint
diff = Redlines(source, test)
# Get Rich-formatted output for terminal
rprint(diff.output_rich)
```
### JSON Output [Section titled “JSON Output”](#json-output)
```python
import json
from redlines import Redlines
diff = Redlines(source, test)
# Pretty-printed JSON
json_str = diff.output_json(pretty=True)
# Compact JSON
json_str = diff.output_json(pretty=False)
# Parse and use
data = json.loads(json_str)
```
*** ## JSON Schema Reference [Section titled “JSON Schema Reference”](#json-schema-reference) ### Complete JSON Structure [Section titled “Complete JSON Structure”](#complete-json-structure)
```json
{
"source": "original text",
"test": "modified text",
"source_tokens": ["token1", "token2", " ¶ "],
"test_tokens": ["token1", "token3", " ¶ "],
"changes": [
{
"type": "replace",
"source_text": "token2",
"test_text": "token3",
"source_position": [1, 2],
"test_position": [1, 2],
"source_char_position": [7, 13],
"test_char_position": [7, 13]
}
],
"stats": {
"total_changes": 1,
"deletions": 0,
"insertions": 0,
"replacements": 1,
"longest_change_length": 6,
"shortest_change_length": 6,
"average_change_length": 6.0,
"change_ratio": 0.15,
"chars_added": 6,
"chars_deleted": 6,
"chars_net_change": 0,
"levenshtein_distance": 3
}
}
```
### Field Descriptions [Section titled “Field Descriptions”](#field-descriptions) #### Root Fields [Section titled “Root Fields”](#root-fields) * **`source`** (string): Original text * **`test`** (string): Modified text * **`source_tokens`** (array of strings): Tokenized source text (`¶` marks paragraph boundaries) * **`test_tokens`** (array of strings): Tokenized test text #### Change Object [Section titled “Change Object”](#change-object) * **`type`** (string): Operation type - `"insert"`, `"delete"`, or `"replace"` * **`source_text`** (string | null): Original text (null for insertions) * **`test_text`** (string | null): Modified text (null for deletions) * **`source_position`** (array of 2 ints | null): Token position `[start, end]` in source (null for insertions) * **`test_position`** (array of 2 ints | null): Token position `[start, end]` in test (null for deletions) * **`source_char_position`** (array of 2 ints | null): Character position `[start, end]` in source * **`test_char_position`** (array of 2 ints | null): Character position `[start, end]` in test #### Stats Object [Section titled “Stats Object”](#stats-object) * **`total_changes`** (int): Total number of change operations * **`deletions`** (int): Count of deletion operations * **`insertions`** (int): Count of insertion operations * **`replacements`** (int): Count of replacement operations * **`longest_change_length`** (int): Length of longest change in characters * **`shortest_change_length`** (int): Length of shortest change in characters * **`average_change_length`** (float): Mean change length in characters * **`change_ratio`** (float): Percentage of text modified (0.0 to 1.0) * **`chars_added`** (int): Total characters added (insertions + replacement additions) * **`chars_deleted`** (int): Total characters deleted (deletions + replacement deletions) * **`chars_net_change`** (int): Net character change (added - deleted) * **`levenshtein_distance`** (int | null): Edit distance (null if Levenshtein library not installed) *** ## Decision Matrix [Section titled “Decision Matrix”](#decision-matrix) | Use Case | Recommended Format | CLI Command | Reason | | ---------------------- | -------------------- | ------------------------------------------------------------- | -------------------------------------- | | **AI agents** | JSON | `redlines file1 file2` | Simplest invocation, structured output | | **CI/CD check** | JSON | `redlines file1 file2` or `redlines json file1 file2 --quiet` | Parseable, scriptable, exit codes | | **Human review** | Markdown (GHFM) | `redlines markdown file1 file2 --markdown-style ghfm` | GitHub-compatible rendering | | **Terminal display** | Rich text | `redlines text file1 file2` | Colored, formatted output | | **Metrics collection** | Stats | `redlines stats file1 file2 --quiet` | Log-friendly plain text | | **Automation/scripts** | JSON + exit codes | `redlines file1 file2` | Exit code 0=changes, 1=no changes | | **HTML report** | Markdown (none) | Programmatic API | Clean HTML tags for styling | | **Documentation** | Markdown (streamlit) | Programmatic API | Streamlit-compatible markup | ### Exit Code Usage [Section titled “Exit Code Usage”](#exit-code-usage)
```bash
# Check if files differ (useful in CI/CD) - command-less invocation
if redlines file1.txt file2.txt > /dev/null 2>&1; then
echo "Exit code 0: Changes detected"
else
exitcode=$?
if [ $exitcode -eq 1 ]; then
echo "Exit code 1: No changes (files identical)"
else
echo "Exit code 2: Error occurred"
fi
fi
# Or use the stats command for more verbose output
if redlines stats file1.txt file2.txt --quiet; then
echo "Exit code 0: Changes detected"
else
echo "Exit code 1: No changes or error occurred"
fi
```
*** ## Programmatic API [Section titled “Programmatic API”](#programmatic-api) ### Core Classes [Section titled “Core Classes”](#core-classes) #### `Redlines` Class [Section titled “Redlines Class”](#redlines-class)
```python
from redlines import Redlines
# Create instance
diff = Redlines(
source="original text",
test="modified text",
processor=None, # Optional: Custom processor (default: WholeDocumentProcessor)
markdown_style="red_green" # Optional: Markdown style
)
# Or compare later
diff = Redlines("original text")
result = diff.compare("modified text")
```
#### Key Properties and Methods [Section titled “Key Properties and Methods”](#key-properties-and-methods)
```python
# Get changes (excludes "equal" operations)
changes: list[Redline] = diff.changes
redlines: list[Redline] = diff.redlines # Alias for changes
# Filter changes by operation
insertions = diff.get_changes(operation="insert")
deletions = diff.get_changes(operation="delete")
replacements = diff.get_changes(operation="replace")
all_changes = diff.get_changes() # Same as .changes
# Get statistics
stats: Stats = diff.stats()
# Get opcodes (like difflib)
opcodes: list[tuple] = diff.opcodes # [(operation, i1, i2, j1, j2), ...]
# Output formats
markdown: str = diff.output_markdown
rich_text: Text = diff.output_rich
json_str: str = diff.output_json(pretty=False)
```
### `Redline` Dataclass [Section titled “Redline Dataclass”](#redline-dataclass)
```python
from redlines.processor import Redline
# Structure of a Redline object
@dataclass
class Redline:
operation: Literal["delete", "insert", "replace"]
source_text: str | None
test_text: str | None
source_position: tuple[int, int] | None # (start, end) token indices
test_position: tuple[int, int] | None
# Example usage
for change in diff.changes:
if change.operation == "replace":
print(f"Line {change.source_position}: {change.source_text} → {change.test_text}")
```
### `Stats` Dataclass [Section titled “Stats Dataclass”](#stats-dataclass)
```python
from redlines.processor import Stats
# Structure of a Stats object
@dataclass
class Stats:
total_changes: int
deletions: int
insertions: int
replacements: int
longest_change_length: int
shortest_change_length: int
average_change_length: float
change_ratio: float # 0.0 to 1.0
chars_added: int
chars_deleted: int
chars_net_change: int
levenshtein_distance: int | None # None if library not installed
# Example usage
stats = diff.stats()
print(f"Modified {stats.change_ratio:.1%} of the document")
print(f"Net change: {stats.chars_net_change:+d} characters")
```
*** ## Error Handling [Section titled “Error Handling”](#error-handling) ### Common Errors and Solutions [Section titled “Common Errors and Solutions”](#common-errors-and-solutions) #### 1. File Not Found (CLI) [Section titled “1. File Not Found (CLI)”](#1-file-not-found-cli)
```bash
$ redlines json nonexistent.txt other.txt
# Error: Failed to read file 'nonexistent.txt': [Errno 2] No such file or directory
# Solution: Check file paths
if [ -f "file.txt" ]; then
redlines json file.txt other.txt
else
echo "File not found"
fi
```
#### 2. Encoding Errors (CLI) [Section titled “2. Encoding Errors (CLI)”](#2-encoding-errors-cli)
```bash
# Error: Failed to read file 'file.txt': File encoding is not UTF-8
# Solution: Convert file to UTF-8
iconv -f ISO-8859-1 -t UTF-8 file.txt > file_utf8.txt
redlines json file_utf8.txt other.txt
```
#### 3. Invalid Operation Filter [Section titled “3. Invalid Operation Filter”](#3-invalid-operation-filter)
```python
from redlines import Redlines
diff = Redlines(source, test)
try:
changes = diff.get_changes(operation="invalid")
except ValueError as e:
print(f"Error: {e}") # Error: Invalid operation: invalid
# Valid operations: "insert", "delete", "replace", or None
```
#### 4. Missing Optional Dependencies [Section titled “4. Missing Optional Dependencies”](#4-missing-optional-dependencies)
```python
from redlines.processor import LEVENSHTEIN_AVAILABLE
if not LEVENSHTEIN_AVAILABLE:
print("Levenshtein library not installed - distance will be None")
# Install with: pip install redlines[levenshtein]
# Stats will still work, but levenshtein_distance will be None
stats = diff.stats()
if stats.levenshtein_distance is not None:
print(f"Edit distance: {stats.levenshtein_distance}")
```
#### 5. Empty or Identical Files [Section titled “5. Empty or Identical Files”](#5-empty-or-identical-files)
```python
from redlines import Redlines
# Empty files
diff = Redlines("", "")
stats = diff.stats()
assert stats.total_changes == 0
assert stats.change_ratio == 0.0
# Identical files
diff = Redlines("Hello world", "Hello world")
stats = diff.stats()
assert stats.total_changes == 0
# CLI exit code will be 1 (no changes)
```
### Defensive Programming Pattern [Section titled “Defensive Programming Pattern”](#defensive-programming-pattern)
```python
from pathlib import Path
from redlines import Redlines
import json
def safe_compare_files(file1: str, file2: str) -> dict:
"""Safely compare two files with comprehensive error handling."""
try:
# Validate files exist
path1, path2 = Path(file1), Path(file2)
if not path1.exists():
return {"error": f"File not found: {file1}"}
if not path2.exists():
return {"error": f"File not found: {file2}"}
# Read files
try:
source = path1.read_text(encoding="utf-8")
test = path2.read_text(encoding="utf-8")
except UnicodeDecodeError as e:
return {"error": f"Encoding error: {e}"}
# Compare
diff = Redlines(source, test)
stats = diff.stats()
return {
"success": True,
"file1": file1,
"file2": file2,
"total_changes": stats.total_changes,
"change_ratio": stats.change_ratio,
"has_changes": stats.total_changes > 0
}
except Exception as e:
return {"error": f"Unexpected error: {e}"}
# Usage
result = safe_compare_files("old.txt", "new.txt")
if "error" in result:
print(f"Error: {result['error']}")
else:
print(f"Changes: {result['total_changes']}")
```
*** ## Performance Guidelines [Section titled “Performance Guidelines”](#performance-guidelines) ### Processor Comparison [Section titled “Processor Comparison”](#processor-comparison) | Processor | Speed | Use Case | Python Version | | ------------------------------------ | ------------------ | --------------------------------------------- | -------------- | | **WholeDocumentProcessor** (default) | Fast (5-6x faster) | Simple documents, speed critical | 3.10+ | | **NupunktProcessor** | Slower | Legal/technical docs, sentence-level accuracy | 3.11+ | ### Speed Benchmarks [Section titled “Speed Benchmarks”](#speed-benchmarks) | File Size | WholeDocument | Nupunkt | Difference | | --------- | ------------- | --------- | ---------- | | 400 chars | 0.19 ms | 0.40 ms | 2.1x | | 4 KB | 0.39 ms | 2.34 ms | 6.0x | | 40 KB | 3.85 ms | 25.14 ms | 6.5x | | 400 KB | 37.92 ms | 241.68 ms | 6.4x | **Throughput:** * WholeDocumentProcessor: \~10 million chars/second * NupunktProcessor: \~1.6 million chars/second ### When to Use Each Processor [Section titled “When to Use Each Processor”](#when-to-use-each-processor) #### Use WholeDocumentProcessor (Default) When: [Section titled “Use WholeDocumentProcessor (Default) When:”](#use-wholedocumentprocessor-default-when) * Processing large volumes of documents * Speed is critical * Documents are simple without complex punctuation * Paragraph-level granularity is sufficient #### Use NupunktProcessor When: [Section titled “Use NupunktProcessor When:”](#use-nupunktprocessor-when)
```python
from redlines import Redlines
from redlines.processor import NupunktProcessor
processor = NupunktProcessor()
diff = Redlines(source, test, processor=processor)
```
* Working with legal or technical documents * Need sentence-level granularity * Documents contain many abbreviations (Dr., Mr., etc.) * URLs, email addresses, or decimal numbers in text * Performance overhead (2-6x) is acceptable Sentence mode preserves the input’s paragraph boundaries (fixed in 0.6.2): sentences are anchored within their paragraph, so output is not reflowed one sentence per paragraph. ### Performance Tips [Section titled “Performance Tips”](#performance-tips) 1. **Reuse Redlines instance for multiple comparisons:**
```python
diff = Redlines(source)
result1 = diff.compare(test1)
result2 = diff.compare(test2) # Faster than creating new instance
```
2. **Use CLI for one-off comparisons:**
```bash
# CLI is optimized for single comparisons
redlines json file1.txt file2.txt
```
3. **Batch processing pattern:**
```python
# Process multiple files efficiently
from concurrent.futures import ThreadPoolExecutor
def compare_file_pair(files):
file1, file2 = files
return Redlines(
Path(file1).read_text(),
Path(file2).read_text()
).stats()
with ThreadPoolExecutor(max_workers=4) as executor:
results = executor.map(compare_file_pair, file_pairs)
```
*** ## Integration Examples [Section titled “Integration Examples”](#integration-examples) ### Example 1: Pre-commit Hook [Section titled “Example 1: Pre-commit Hook”](#example-1-pre-commit-hook) .git/hooks/pre-commit
```bash
#!/bin/bash
# Compare staged files with HEAD
for file in $(git diff --cached --name-only --diff-filter=M); do
if [ -f "$file" ]; then
# Get old version
git show HEAD:"$file" > /tmp/old_version
# Compare with new version
if ! redlines stats /tmp/old_version "$file" --quiet > /dev/null; then
echo "Error comparing $file"
exit 1
fi
echo "✓ $file: Changes validated"
rm /tmp/old_version
fi
done
exit 0
```
### Example 2: CI/CD Check [Section titled “Example 2: CI/CD Check”](#example-2-cicd-check) .github/workflows/check-docs.yml
```yaml
name: Check Documentation Changes
on: [pull_request]
jobs:
check-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install redlines
run: pip install redlines
- name: Check README changes
run: |
git show main:README.md > old_readme.md
if redlines stats old_readme.md README.md --quiet; then
echo "README has changes"
redlines markdown old_readme.md README.md --markdown-style ghfm >> $GITHUB_STEP_SUMMARY
else
echo "README unchanged"
fi
```
### Example 3: Batch Report Generator [Section titled “Example 3: Batch Report Generator”](#example-3-batch-report-generator)
```python
#!/usr/bin/env python3
"""Generate HTML report comparing two directories."""
from pathlib import Path
from redlines import Redlines
import json
def generate_report(dir1: Path, dir2: Path, output: Path):
"""Generate HTML diff report for all files in two directories."""
results = []
for file1 in sorted(dir1.glob("**/*.txt")):
file2 = dir2 / file1.relative_to(dir1)
if not file2.exists():
continue
diff = Redlines(
file1.read_text(),
file2.read_text(),
markdown_style="none"
)
stats = diff.stats()
if stats.total_changes > 0:
results.append({
"file": str(file1.relative_to(dir1)),
"stats": {
"changes": stats.total_changes,
"ratio": f"{stats.change_ratio:.1%}",
"added": stats.chars_added,
"deleted": stats.chars_deleted,
},
"diff": diff.output_markdown
})
# Generate HTML
html = f"""
Diff Report