README
ΒΆ
gomarklint
A fast, opinionated Markdown linter for engineering teams. Built in Go, designed for CI.
- Catch broken links and headings before your docs ship.
- Enforce predictable structure (no more βwhy is this H4 under H2?β).
- Output thatβs friendly for both humans and machines (JSON).
Why
Docs break quietly and trust erodes loudly. gomarklint focuses on reproducible rules that prevent βsmall but costlyβ failures:
- Heading hierarchies that drift during edits
- Duplicate headings that break anchor links
- Subtle dead links (including internal anchors)
- Large repos where βone-off checksβ donβt scale
Goal: treat documentation quality like code qualityβfast feedback locally, strict in CI, zero drama.
β¨ Features
- β‘οΈ Blazingly fast: Process 100,000+ lines in ~170ms (structural checks only, M4 Mac)
- Recursive .md search (multi-file & multi-directory)
- Frontmatter-aware parsing (YAML/TOML ignored when needed)
- File name & line number in diagnostics
- Human-readable and JSON outputs
- Fast single-binary CLI (Go), ideal for CI/CD
- Rules with clear rationales (see below)
Planned/ongoing:
- Severity levels per rule
- Customizable rule enable/disable
- VS Code extension for in-editor feedback
Quick Start
# install (choose one)
go install github.com/shinagawa-web/gomarklint@latest
# or clone and build manually
git clone https://github.com/shinagawa-web/gomarklint
cd gomarklint
make build # or: go build ./cmd/gomarklint
1) Initialize config (optional but recommended)
gomarklint init
This creates .gomarklint.json with sensible defaults:
{
"include": ["."],
"ignore": ["node_modules", "vendor"],
"minHeadingLevel": 2,
"enableHeadingLevelCheck": true,
"enableDuplicateHeadingCheck": true,
"enableLinkCheck": false,
"enableNoSetextHeadingsCheck": true,
"skipLinkPatterns": [],
"outputFormat": "text"
}
You can edit it anytime β CLI flags override config values.
2) Run it
# lint current directory recursively
gomarklint ./...
# lint specific targets
gomarklint docs README.md internal/handbook
Exit code is non-zero if any violations are found, zero otherwise.
3) JSON output (for CI / tooling)
gomarklint ./... --output json
Rules (current)
gomarklint currently runs the following checks (ordered as executed):
| Rule key | What it detects | Notes / Options |
|---|---|---|
final-blank-line |
Missing final blank line at EOF | Always on |
unclosed-code-block |
Unclosed fenced code blocks (````` / ~~~) |
Always on |
empty-alt-text |
Image syntax with an empty alt text | Always on |
heading-level |
Invalid heading level progression (e.g., H2 β H4 skip) | Toggle: --enable-heading-level-check (default on) / --min-heading (default 2) |
duplicate-heading |
Duplicate headings within one file | Toggle: --enable-duplicate-heading-check (default on) |
no-multiple-blank-lines |
Multiple consecutive blank lines | Toggle: --enable-no-multiple-blank-lines-check (default on) |
external-link |
External links that fail validation | Toggle: --enable-link-check (default off). Skips URLs that match --skip-link-patterns (regex). |
no-setext-headings |
Setext heading used instead of ATX style | Toggle: --enable-no-setext-headings-check (default on) |
Execution details:
- Files/dirs are expanded with ignore patterns from config (see Configuration).
- Per-file issues are sorted by line asc before printing.
- Line count is computed as \n count + 1 for reporting.
CLI
gomarklint [files or directories] [flags]
If no paths are given, the tool will:
- Use
includefrom.gomarklint.jsonif present, otherwise error out with βplease provide a markdown file or directory (or set 'include' in .gomarklint.json)β.
Flags
| Flag | Type | Default | Description |
|---|---|---|---|
--config |
string | .gomarklint.json |
Path to config file. Loaded if the file exists. |
--min-heading |
int | 2 |
Minimum heading level considered by the heading-level rule. |
--enable-link-check |
bool | false |
Enable external link checking. |
--enable-heading-level-check |
bool | true |
Enable heading level validation. |
--enable-duplicate-heading-check |
bool | true |
Enable duplicate heading detection. |
--skip-link-patterns |
string[] (regex) | [] |
Regex patterns; matching URLs are skipped by link check. Can be passed multiple times. |
--output |
text |
json |
text |
Notes:
- Flags override config values when explicitly provided.
- Paths are expanded (globs/dirs) and filtered by ignore (from config).
- Exit behavior: the command returns a non-nil error (non-zero exit), zero otherwise.
Configuration
A JSON config is read from the path given by --config (defaults to .gomarklint.json) if the file exists. Example:
{
"include": ["docs", "README.md"],
"ignore": ["node_modules", "vendor"],
"outputFormat": "text",
"minHeadingLevel": 2,
"enableLinkCheck": false,
"enableHeadingLevelCheck": true,
"enableDuplicateHeadingCheck": true,
"skipLinkPatterns": [
"^https://localhost(:[0-9]+)?/",
"example\\.com"
]
}
Field effects:
- If CLI flags are set, they take precedence over config.
- If no CLI paths are provided, include (when present) becomes the target set.
Output
Human-readable (--output text, default)
- Prints grouped file sections only when a file has issues:
β― gomarklint testdata/sample_links.md
Errors in testdata/sample_links.md:
testdata/sample_links.md:1: First heading should be level 2 (found level 1)
testdata/sample_links.md:4: Link unreachable: https://httpstat.us/404
testdata/sample_links.md:12: Link unreachable: http://localhost-test:3001
testdata/sample_links.md:16: duplicate heading: "overview"
testdata/sample_links.md:18: image with empty alt text
β 5 issues found
β Checked 1 file(s), 19 line(s) in 757ms
- Summary and timing:
- If issues:
β N issues found - If none:
β No issues found - Always prints:
Checked <files>, <lines> in <Xms|Ys>with colored ticks.
- If issues:
JSON (--output json)
{
"files": 1,
"lines": 19,
"errors": 5,
"elapsed_ms": 790,
"details": {
"testdata/sample_links.md": [
{
"File": "testdata/sample_links.md",
"Line": 1,
"Message": "First heading should be level 2 (found level 1)"
},
{
"File": "testdata/sample_links.md",
"Line": 4,
"Message": "Link unreachable: https://httpstat.us/404"
},
{
"File": "testdata/sample_links.md",
"Line": 12,
"Message": "Link unreachable: http://localhost-test:3001"
},
{
"File": "testdata/sample_links.md",
"Line": 16,
"Message": "duplicate heading: \"overview\""
},
{
"File": "testdata/sample_links.md",
"Line": 18,
"Message": "image with empty alt text"
}
]
}
}
- details maps file path β list of issues (
file,line,message). - elapsed_ms is total wall time for the run.
β‘οΈ Performance
gomarklint is built for speed, with optimizations for both file parsing and external link validation.
Structural checks (headings, code blocks, etc.):
- Scanning 185 files and 104,000+ lines takes under 60ms
External link checking (--enable-link-check):
- Optimized concurrent validation with intelligent batching
- ~2,000 external links validated in under 10 seconds
- Significantly faster than traditional sequential HTTP checks
β Recommended usage
For rapid local feedback:
- Run without
--enable-link-checkβ completes in milliseconds - Perfect for catching structural issues while editing
For comprehensive validation:
- Enable
--enable-link-checkfor:- Nightly CI runs
- Pre-release validation
- Verifying newly added content
- Performance remains practical even at scale
β±οΈ TL;DR:
Fast enough for local dev (no link check), robust enough for CI (with link check).
π Benchmarking
gomarklint includes comprehensive benchmarks to track performance and prevent regressions.
Running Benchmarks Locally
# Run all benchmarks
make bench
# Run benchmarks for a specific package
go test -bench=. ./internal/rule/
# Run with memory profiling
go test -bench=. -benchmem ./...
Benchmark Coverage
Benchmarks are available for the main linting workflows:
- Lint rules: Each rule has dedicated benchmarks (e.g.,
BenchmarkCheckHeadingLevel) - Full linting: End-to-end benchmarks for running gomarklint across files (see
cmd/root_bench_test.go)
CI Integration
Pull requests automatically run benchmark comparisons against the main branch:
- Shows performance differences for each benchmarked function
- Highlights regressions with visual indicators (β /β οΈ/β)
- Results are posted as PR comments for easy review
The benchmark workflow ensures performance remains stable across code changes.
π§ͺ GitHub Actions Integration
You can use gomarklint in your CI workflows using the official GitHub Action:
β οΈ Note: When using
gomarklintin GitHub Actions, you must first create a.gomarklint.jsonconfiguration file in your repository root. This ensures all options are explicitly defined and reproducible in CI environments.
You can generate a default config with:
gomarklint init
Example: .github/workflows/lint.yml
name: Lint Markdown
on:
push:
paths:
- '**/*.md'
pull_request:
paths:
- '**/*.md'
jobs:
markdown-lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run gomarklint Action
uses: shinagawa-web/gomarklint-action@v1
π£οΈ Roadmap (Post v1.0.0)
β Core Quality & Rule Expansion
-
max-line-length: Enforce maximum line width -
no-multiple-consecutive-blank-lines: Disallow multiple blank lines -
image-alt-textimprovements: Enforce alt text style and length - Rule severity levels (e.g.
warning,error)
π§© Extensibility
- Plugin system for custom rules (via Go interface or external binary)
- Allow disabling specific rules via inline comments (e.g.
<!-- gomarklint-disable -->)
π§ͺ Testing & Stability
- Snapshot testing support for easier rule verification
- Regression test suite for real-world Markdown samples
π οΈ Developer UX
- VS Code extension using gomarklint core
- Interactive mode (e.g. prompt to fix or explain errors)
- File caching for faster repeated linting
π¦ Ecosystem & CI
- GitHub Actions integration
- Prebuilt binaries via
goreleaser(macOS/Linux/Windows) - Homebrew formula
- Docker image (e.g.
ghcr.io/shinagawa-web/gomarklint)
π Internationalization
- Localized messages (e.g. Japanese, Spanish)
- Rule messages with IDs and documentation links
Feel free to suggest more ideas by opening an issue or discussion on GitHub!
π Project Structure
gomarklint/
βββ cmd/ # CLI commands
β βββ init.go # Configuration initialization
β βββ root.go # Root command and CLI orchestration
β βββ root_bench_test.go # Benchmark tests for CLI
βββ internal/
β βββ config/ # Configuration management
β β βββ config.go # Config struct and defaults
β β βββ config_test.go
β β βββ load.go # Configuration file loading
β β βββ merge.go # Config merging and flag handling
β β βββ merge_test.go
β βββ file/ # File system operations
β β βββ expand.go # File expansion and glob pattern matching
β β βββ expand_test.go
β β βββ pathutil.go # Path utilities
β β βββ pathutil_test.go
β β βββ reader.go # File reading with frontmatter handling
β β βββ reader_test.go
β βββ linter/ # Core linting logic
β β βββ linter.go # Linter implementation with concurrent processing
β β βββ linter_test.go
β βββ output/ # Output formatting
β β βββ formatter.go # Formatter interface
β β βββ json.go # JSON output formatter
β β βββ json_test.go
β β βββ text.go # Text output formatter
β β βββ text_test.go
β β βββ testutil_test.go
β βββ rule/ # Lint rules implementation
β β βββ code_block.go
β β βββ code_block_test.go
β β βββ code_block_bench_test.go
β β βββ duplicate_headings.go
β β βββ duplicate_headings_test.go
β β βββ duplicate_headings_bench_test.go
β β βββ empty_alt_text.go
β β βββ empty_alt_text_test.go
β β βββ empty_alt_text_bench_test.go
β β βββ external_link.go
β β βββ external_link_test.go
β β βββ external_link_bench_test.go
β β βββ final_blank_line.go
β β βββ final_blank_line_test.go
β β βββ final_blank_line_bench_test.go
β β βββ heading_level.go
β β βββ heading_level_test.go
β β βββ heading_level_bench_test.go
β β βββ no_multiple_blank_lines.go
β β βββ no_multiple_blank_lines_test.go
β β βββ no_multiple_blank_lines_bench_test.go
β β βββ setext_headings.go
β β βββ setext_headings_test.go
β βββ testutil/ # Testing utilities
β βββ path.go
β βββ path_test.go
βββ e2e/ # End-to-end tests
β βββ e2e_test.go
β βββ fixtures/ # Test fixture markdown files
β βββ .gomarklint.json
βββ testdata/ # Unit test fixtures
βββ main.go # Application entry point
βββ doc.go # Package documentation
π Path Handling
When specifying files or directories, gomarklint will:
- Recursively search
.mdfiles usingfilepath.WalkDir - Ignore hidden directories like
.git/ - Skip symbolic links
- Report all files, regardless of
.gitignore - Silently skip missing files (
os.IsNotExist)
π Local Development
To set up a local development environment for gomarklint:
Using Make (Recommended)
# Show all available commands
make help
# Build the binary
make build
# Run unit tests
make test
# Run end-to-end tests
make test-e2e
# Run all tests (unit + E2E)
make test-all
# Run tests with coverage report
make test-coverage
# Lint the included sample files in ./testdata
make lint
# Lint the repo's README
make lint-self
# Run gomarklint with custom arguments
make run-dev ARGS="README.md"
# Generate a default .gomarklint.json
make init
# Clean build artifacts
make clean
Testing Strategy
Unit Tests
Unit tests for individual rules and utilities are located in *_test.go files alongside the code they test:
internal/rule/*_test.goβ Test individual lint rulesinternal/linter/*_test.goβ Test core linting logicinternal/file/*_test.goβ Test file operations and path utilitiesinternal/config/*_test.goβ Test configuration loading and merginginternal/output/*_test.goβ Test output formatters
Run with: make test
End-to-End Tests
E2E tests verify the complete CLI behavior by running the compiled binary against fixture files:
- Located in
e2e/e2e_test.go - Test fixtures in
e2e/fixtures/(Markdown files with various rule violations) - Tests are organized into logical categories:
- Basic Functionality: Individual rule detection (heading levels, duplicates, blank lines, code blocks, alt text, external links)
- Configuration: CLI flag overrides and rule disabling
- Output Formats: Text and JSON output validation
- Multiple Files: Multi-file and directory recursion handling
- Edge Cases: Non-existent files, invalid configs, empty files, frontmatter handling, multiple violations in single file
Run with: make test-e2e
Notes:
go run .uses the local source directly, so you don't need togo installduring development.- When adding new CLI flags or config fields, confirm they appear in
--helpand the generated.gomarklint.json. - Tests should remain fast and self-contained β contributions that break this will be rejected.
- E2E tests may take longer due to external link validation; use
enableLinkCheck: falsein test config when testing rules unrelated to links.
π€ Contributing
Issues, suggestions, and PRs are welcome! Before submitting a pull request, please follow the guidelines below to ensure a smooth review process.
Requirements:
- Go version: Go
1.22+(latest stable recommended)
π License
MIT License