Something went wrong. Try again.
agents love to write markdown with a bunch of line breaks and it breaks my brain. instead of fighting them, i just have this auto-apply on save to any markdown (and qmd) files.
Something went wrong. Try again.
9.3 kB · 296 lines · Created 4w ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296#!/usr/bin/env python3"""Unwrap hard-wrapped Markdown prose without rewriting block structure.
Reads a document from stdin and writes the formatted document to stdout foreditor format-on-save. Markdown and Quarto blocks are preserved verbatim;only lines that are clearly part of the same prose paragraph are joined."""
import reimport sys
ATX_HEADING_RE = re.compile(r"^ {0,3}#{1,6}(?:\s|$)")ATTRIBUTE_RE = re.compile(r"^ {0,3}\{[#.][^}]*\}\s*$")DEFINITION_RE = re.compile(r"^ {0,3}[:~]\s+")FENCE_RE = re.compile(r"^ {0,3}(?P<fence>`{3,}|~{3,})(?P<info>.*)$")FENCED_DIV_RE = re.compile(r"^ {0,3}:{3,}(?:\s|\{|$)")LIST_RE = re.compile(r"^(?P<prefix>\s*(?:[-+*]|\d+[.)])\s+)(?P<body>.*)$")REFERENCE_RE = re.compile(r"^ {0,3}\[[^]]+\]:")SETEXT_RE = re.compile(r"^ {0,3}(?:=+|-+)\s*$")SHORTCODE_RE = re.compile(r"^\s*\{\{[<%].*[>%]\}\}\s*$")THEMATIC_RE = re.compile(r"^ {0,3}(?:(?:\*\s*){3,}|(?:-\s*){3,}|(?:_\s*){3,})$")
HTML_BLOCK_TAGS = ( "address|article|aside|base|basefont|blockquote|body|caption|center|" "col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|" "figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|" "legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|" "p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|" "tr|track|ul")HTML_BLOCK_RE = re.compile( rf"^ {{0,3}}</?(?:{HTML_BLOCK_TAGS})(?:\s|/?>|$)", re.IGNORECASE)HTML_RAW_RE = re.compile( r"^ {0,3}<(?P<tag>script|pre|style|textarea)(?:\s|>|$)", re.IGNORECASE)HTML_TAG_LINE_RE = re.compile(r"^ {0,3}</?[A-Za-z][^>]*>\s*$")TABLE_PIPE_RE = re.compile(r"(?<!\\)\|")
def _has_hard_break(line): """Return whether a source line intentionally forces a Markdown break.""" return line.endswith("\\") or len(line) - len(line.rstrip(" ")) >= 2
def _is_frontmatter_start(line): value = line.lstrip("\ufeff") return value if value in ("---", "+++") else None
def _frontmatter_end(lines, delimiter): endings = {delimiter} if delimiter == "+++" else {"---", "..."} for index in range(1, len(lines)): if lines[index].strip() in endings: return index return len(lines) - 1
def _fence_end(lines, start, fence): character = re.escape(fence[0]) closing = re.compile(rf"^ {{0,3}}{character}{{{len(fence)},}}\s*$") for index in range(start + 1, len(lines)): if closing.match(lines[index]): return index return len(lines) - 1
def _html_comment_end(lines, start): for index in range(start, len(lines)): if "-->" in lines[index]: return index return len(lines) - 1
def _raw_html_end(lines, start, tag): closing = re.compile(rf"</{re.escape(tag)}\s*>", re.IGNORECASE) for index in range(start, len(lines)): if closing.search(lines[index]): return index return len(lines) - 1
def _html_block_end(lines, start): index = start while index + 1 < len(lines) and lines[index + 1].strip(): index += 1 return index
def _math_block_end(lines, start, opener): if opener == "$$": closing = re.compile(r"^\s*\$\$(?:\s*\{[^}]*\})?\s*$") else: closing = re.compile(r"^\s*\\\]\s*$") for index in range(start + 1, len(lines)): if closing.match(lines[index]): return index return len(lines) - 1
def _latex_environment_end(lines, start, environment): closing = re.compile(rf"\\end\{{{re.escape(environment)}\}}") for index in range(start, len(lines)): if closing.search(lines[index]): return index return len(lines) - 1
def _is_structural_line(line, include_indented=True): stripped = line.lstrip() if not stripped: return True if include_indented and (line.startswith(" ") or line.startswith("\t")): return True if FENCE_RE.match(line): return True if stripped.startswith("<!--"): return True if ATX_HEADING_RE.match(line) or SETEXT_RE.match(line): return True if THEMATIC_RE.match(line) or FENCED_DIV_RE.match(line): return True if SHORTCODE_RE.match(line) or ATTRIBUTE_RE.match(line): return True if REFERENCE_RE.match(line) or DEFINITION_RE.match(line): return True if stripped.startswith(">") or LIST_RE.match(line): return True if TABLE_PIPE_RE.search(line): return True if stripped in ("$$", r"\[", r"\]"): return True if stripped.startswith(r"\begin{"): return True if HTML_RAW_RE.match(line) or HTML_BLOCK_RE.match(line): return True if HTML_TAG_LINE_RE.match(line): return True return False
def _join_prose_lines(lines): """Join prose while retaining explicit hard-break line endings.""" if not lines: return []
joined = [] current = lines[0].lstrip() previous = lines[0] for line in lines[1:]: if _has_hard_break(previous): joined.append(current) current = line.lstrip() else: current = current.rstrip() + " " + line.strip() previous = line joined.append(current) return joined
def _unwrap_blockquote(lines, start): content = [] index = start while index < len(lines): match = re.match(r"^\s*>\s?(.*)$", lines[index]) if not match: break content.append(match.group(1)) index += 1
unwrapped = unwrap_markdown("\n".join(content)).split("\n") output = ["> " + line if line else ">" for line in unwrapped] return output, index
def _unwrap_list_item(lines, start, match): prefix = match.group("prefix") prose = [match.group("body")] content_indent = len(prefix.expandtabs(4)) index = start + 1
while index < len(lines): line = lines[index] if not line.strip() or LIST_RE.match(line): break if _is_structural_line(line, include_indented=False): break indentation = len(line) - len(line.lstrip(" ")) if line.startswith("\t") or indentation >= content_indent + 4: break prose.append(line) index += 1
joined = _join_prose_lines(prose) output = [prefix + joined[0]] output.extend(" " * content_indent + line for line in joined[1:]) return output, index
def unwrap_markdown(text): """Return Markdown with only unambiguous prose wrapping removed.""" lines = text.split("\n") output = [] index = 0
if lines: delimiter = _is_frontmatter_start(lines[0]) if delimiter: end = _frontmatter_end(lines, delimiter) output.extend(lines[: end + 1]) index = end + 1
while index < len(lines): line = lines[index] stripped = line.lstrip()
if not stripped: output.append(line) index += 1 continue
fence_match = FENCE_RE.match(line) if fence_match: end = _fence_end(lines, index, fence_match.group("fence")) output.extend(lines[index : end + 1]) index = end + 1 continue
if stripped.startswith("<!--"): end = _html_comment_end(lines, index) output.extend(lines[index : end + 1]) index = end + 1 continue
raw_match = HTML_RAW_RE.match(line) if raw_match: end = _raw_html_end(lines, index, raw_match.group("tag")) output.extend(lines[index : end + 1]) index = end + 1 continue
if HTML_BLOCK_RE.match(line): end = _html_block_end(lines, index) output.extend(lines[index : end + 1]) index = end + 1 continue
if re.match(r"^\s*\$\$(?:\s*\{[^}]*\})?\s*$", line): end = _math_block_end(lines, index, "$$") output.extend(lines[index : end + 1]) index = end + 1 continue
if stripped == r"\[": end = _math_block_end(lines, index, r"\[") output.extend(lines[index : end + 1]) index = end + 1 continue
latex_match = re.match(r"^\s*\\begin\{([^}]+)\}", line) if latex_match: end = _latex_environment_end(lines, index, latex_match.group(1)) output.extend(lines[index : end + 1]) index = end + 1 continue
if stripped.startswith(">"): block, index = _unwrap_blockquote(lines, index) output.extend(block) continue
list_match = LIST_RE.match(line) if list_match: item, index = _unwrap_list_item(lines, index, list_match) output.extend(item) continue
if _is_structural_line(line): output.append(line) index += 1 continue
prose = [] while index < len(lines): candidate = lines[index] if _is_structural_line(candidate): break prose.append(candidate) index += 1 output.extend(_join_prose_lines(prose))
return "\n".join(output)
if __name__ == "__main__": sys.stdout.write(unwrap_markdown(sys.stdin.read()))