#!/usr/bin/env bash # format-markdown.sh # Author: Matthew Raymer # Date: 2025-07-09 # Description: Format markdown files to comply with project markdown ruleset # Enhanced: Auto-fix, parallel lint, summary, CI-friendly, multi-linter support # Always fixes missing blank lines around headings. set -e # Fix missing blank lines above and below headings in a Markdown file fix_blank_lines_around_headings() { local file="$1" awk ' BEGIN { prev=""; } { if ($0 ~ /^#{1,6} /) { if (NR > 1 && prev != "") print ""; print $0; getline nextline; if (nextline != "") print ""; print nextline; prev = nextline; next; } print $0; prev = $0; } ' "$file" > "$file.tmp" && mv "$file.tmp" "$file" } show_help() { echo "Usage: $0 [--fix] [--ci] [--linter ] [more files...]" echo "Formats and lints markdown files." echo "Options:" echo " -h, --help Show this help message" echo " --fix Auto-fix lint errors (if supported)" echo " --ci CI-friendly output (machine-readable)" echo " --linter Choose linter: markdownlint, prettier, or both (default: both)" echo " Markdown files or directories to process" exit 0 } # Default options fix=0 ci=0 linter="both" args=() # Parse flags while [[ $# -gt 0 ]]; do case "$1" in -h|--help) show_help ;; --fix) fix=1 ; shift ;; --ci) ci=1 ; shift ;; --linter) linter="$2" ; shift 2 ;; --) shift ; break ;; -*) echo "Unknown option: $1"; show_help ;; *) args+=("$1"); shift ;; esac done if [ ${#args[@]} -eq 0 ]; then show_help fi # Tool checks for tool in awk sed; do if ! command -v $tool >/dev/null 2>&1; then echo "$tool is required but not installed. Exiting." exit 1 fi done if ! command -v npx >/dev/null 2>&1; then echo "npx is required for linting. Exiting." exit 1 fi # Check for prettier and markdownlint availability has_prettier=0 has_markdownlint=0 if npx prettier --version >/dev/null 2>&1; then has_prettier=1 fi if npx markdownlint --version >/dev/null 2>&1; then has_markdownlint=1 fi # Respect .markdownlintignore if present mlint_ignore="" if [ -f .markdownlintignore ]; then mlint_ignore="--ignore-path .markdownlintignore" fi # Gather files all_files=() for target in "${args[@]}"; do if [ -d "$target" ]; then while IFS= read -r -d $'\0' file; do all_files+=("$file") done < <(find "$target" -type f -name "*.md" -print0) else all_files+=("$target") fi done # Remove duplicates all_files=( $(printf "%s\n" "${all_files[@]}" | sort -u) ) # Format and lint files lint_errors=0 failed_files=() passed_files=() format_and_lint() { file="$1" # Fix missing blank lines around headings fix_blank_lines_around_headings "$file" # Basic formatting sed -i 's/[[:space:]]*$//' "$file" awk 'NF{blank=0} !NF{blank++} blank<2' "$file" > "$file.tmp" && mv "$file.tmp" "$file" awk '1; END{if (NR && $0!="") print ""}' "$file" > "$file.tmp" && mv "$file.tmp" "$file" # Auto-fix with prettier if [[ $fix -eq 1 && ( "$linter" == "prettier" || "$linter" == "both" ) && $has_prettier -eq 1 ]]; then npx prettier --write "$file" >/dev/null 2>&1 && echo "Auto-fixed with prettier: $file" fi # Auto-fix with markdownlint if [[ $fix -eq 1 && ( "$linter" == "markdownlint" || "$linter" == "both" ) && $has_markdownlint -eq 1 ]]; then npx markdownlint --fix $mlint_ignore "$file" >/dev/null 2>&1 && echo "Auto-fixed with markdownlint: $file" fi # Linting lint_ok=1 if [[ "$linter" == "prettier" || "$linter" == "both" ]] && [[ $has_prettier -eq 1 ]]; then if ! npx prettier --check "$file" >/dev/null 2>&1; then lint_ok=0 echo "Prettier lint errors in $file" fi fi if [[ "$linter" == "markdownlint" || "$linter" == "both" ]] && [[ $has_markdownlint -eq 1 ]]; then if ! npx markdownlint $mlint_ignore "$file"; then lint_ok=0 echo "Markdownlint errors in $file" fi fi if [[ $lint_ok -eq 1 ]]; then passed_files+=("$file") [[ $ci -eq 0 ]] && echo "PASS: $file" else failed_files+=("$file") lint_errors=1 [[ $ci -eq 0 ]] && echo "FAIL: $file" [[ $ci -eq 1 ]] && echo "$file" fi } # Export for xargs export -f format_and_lint export fix linter has_prettier has_markdownlint mlint_ignore ci # Run in parallel (4 at a time) printf "%s\n" "${all_files[@]}" | xargs -n 1 -P 4 -I {} bash -c 'format_and_lint "$@"' _ {} # Summary if [[ $ci -eq 0 ]]; then echo echo "Lint summary:" echo "Passed: ${#passed_files[@]}" echo "Failed: ${#failed_files[@]}" if [ ${#failed_files[@]} -gt 0 ]; then printf '%s\n' "${failed_files[@]}" fi fi if [ $lint_errors -ne 0 ]; then echo "Some files have markdownlint or prettier errors. Please fix them." exit 1 fi echo "Markdown formatting complete."