#!/usr/bin/env nu # Fix MD013 line length errors by wrapping long lines at natural break points def wrap-line [line: string, max_len: int]: string -> string { let len = $line | str length # Skip if line is short enough if $len <= $max_len { return $line } # Skip table rows (can't easily wrap) if ($line | str trim | str starts-with "|") { return $line } # Skip code blocks (shouldn't wrap) if ($line | str trim | str starts-with "```") { return $line } # Skip heading lines if ($line | str trim | str starts-with "#") { return $line } # Skip list items that start with - or * let trimmed = $line | str trim if ($trimmed | str starts-with "- ") or ($trimmed | str starts-with "* ") or ($trimmed =~ '^\d+\. ') { # List items - try to wrap at sentence boundaries return (wrap-at-punctuation $line $max_len) } # Regular prose - wrap at sentence or clause boundaries wrap-at-punctuation $line $max_len } def wrap-at-punctuation [line: string, max_len: int]: string -> string { let len = $line | str length if $len <= $max_len { return $line } # Find the last space before max_len that follows punctuation or a good break point let break_points = [". " ", " "; " ": " "- " "— " " and " " or " " but " " which " " that " " when " " where "] mut best_pos = -1 for bp in $break_points { # Find positions of this break point let positions = find-all-positions $line $bp for pos in $positions { # Check if this position is valid (before max_len but not too early) let break_at = $pos + ($bp | str length) if $break_at <= $max_len and $break_at > $best_pos and $break_at > 50 { $best_pos = $break_at } } } # If no good break point found, fall back to last space before max_len if $best_pos == -1 { let spaces = find-all-positions $line " " for pos in ($spaces | reverse) { if $pos < $max_len and $pos > 40 { $best_pos = $pos + 1 break } } } # If still no break point, return as-is (can't wrap safely) if $best_pos == -1 { return $line } # Split at best position let first = $line | str substring 0..$best_pos | str trim-end let rest = $line | str substring $best_pos.. let rest_trimmed = $rest | str trim-start # Return wrapped result $"($first)\n($rest_trimmed)" } def find-all-positions [text: string, pattern: string]: list { mut positions = [] mut start = 0 let text_len = $text | str length let pattern_len = $pattern | str length while $start < ($text_len - $pattern_len) { let sub = $text | str substring $start..($start + $pattern_len) if $sub == $pattern { $positions = ($positions | append $start) } $start = $start + 1 } $positions } def process-file [file: path, max_len: int] { let content = open $file --raw let lines = $content | lines mut in_code_block = false mut new_lines = [] for line in $lines { # Track code blocks if ($line | str trim | str starts-with "```") { $in_code_block = not $in_code_block $new_lines = ($new_lines | append $line) continue } # Don't process lines inside code blocks if $in_code_block { $new_lines = ($new_lines | append $line) continue } # Wrap long lines let wrapped = wrap-line $line $max_len # Split wrapped result back into lines let wrapped_lines = $wrapped | lines $new_lines = ($new_lines | append $wrapped_lines) } let new_content = $new_lines | str join "\n" $new_content | save -f $file } def main [ file?: path # Specific file to process (optional) --max-len: int = 150 # Maximum line length --dry-run (-d) # Show what would be changed without modifying files ] { if $file != null { print $"Processing: ($file)" process-file $file $max_len } else { # Get files with MD013 errors from markdownlint print "Getting files with MD013 errors..." let result = (markdownlint-cli2 --config .markdownlint-cli2.jsonc "**/*.md" | complete) let errors = $result.stderr | lines | where { $in | str contains "MD013" } let files = $errors | each { $in | split row ":" | first } | uniq print $"Found ($files | length) files with MD013 errors" for f in $files { if $dry_run { print $"Would fix: ($f)" } else { print $"Fixing: ($f)" process-file $f $max_len } } } print "Done" }