|
4 | 4 | - EditorConfig-based indentation settings |
5 | 5 | - Tight list formatting with multi-paragraph awareness |
6 | 6 | - Frontmatter spacing normalization |
| 7 | +- Trailing whitespace removal (outside code blocks) |
7 | 8 | """ |
8 | 9 |
|
9 | 10 | import re |
@@ -186,21 +187,74 @@ def _render_ordered_list(node: RenderTreeNode, context: RenderContext) -> str: |
186 | 187 | } |
187 | 188 |
|
188 | 189 |
|
189 | | -def _postprocess_root(text: str, node: RenderTreeNode, context: RenderContext) -> str: |
| 190 | +def _normalize_frontmatter_spacing(text: str) -> str: |
190 | 191 | """Normalize spacing after YAML frontmatter. |
191 | 192 |
|
192 | | - Applies two spacing rules: |
193 | | - - Heading after frontmatter: no blank line (tight) |
194 | | - - Other content after frontmatter: exactly one blank line |
| 193 | + Removes all blank lines between frontmatter closing delimiter and the |
| 194 | + first content block, producing tight spacing universally. |
| 195 | +
|
| 196 | + IMPORTANT: Only matches actual frontmatter (document starts with ---) |
| 197 | + not thematic breaks appearing mid-document. |
| 198 | + """ |
| 199 | + # Only process if document starts with frontmatter opening delimiter |
| 200 | + if not text.startswith("---\n"): |
| 201 | + return text |
| 202 | + |
| 203 | + # Find the closing delimiter (second --- on its own line) |
| 204 | + # Pattern: opening --- at start, content, closing --- on its own line |
| 205 | + frontmatter_match = re.match(r"^---\n.*?\n(---\n)", text, flags=re.DOTALL) |
| 206 | + if not frontmatter_match: |
| 207 | + return text |
| 208 | + |
| 209 | + # Get position after closing delimiter |
| 210 | + closing_end = frontmatter_match.end(1) |
| 211 | + before_content = text[:closing_end] |
| 212 | + after_content = text[closing_end:] |
| 213 | + |
| 214 | + # Remove all blank lines after frontmatter (tight spacing for any content) |
| 215 | + after_content = re.sub(r"^\n+", "", after_content) |
| 216 | + |
| 217 | + return before_content + after_content |
| 218 | + |
| 219 | + |
| 220 | +def _strip_trailing_whitespace(text: str) -> str: |
| 221 | + """Strip trailing whitespace, preserving code blocks. |
| 222 | +
|
| 223 | + Fenced code blocks (``` or ~~~) preserve trailing whitespace |
| 224 | + since it may be semantically meaningful in code. |
| 225 | + """ |
| 226 | + lines = text.split("\n") |
| 227 | + result = [] |
| 228 | + in_code_block = False |
| 229 | + |
| 230 | + for line in lines: |
| 231 | + # Track fenced code block state |
| 232 | + stripped = line.lstrip() |
| 233 | + if stripped.startswith("```") or stripped.startswith("~~~"): |
| 234 | + in_code_block = not in_code_block |
| 235 | + result.append(line.rstrip()) # Strip fence line itself |
| 236 | + elif in_code_block: |
| 237 | + # Preserve trailing whitespace inside code blocks |
| 238 | + result.append(line) |
| 239 | + else: |
| 240 | + # Strip trailing whitespace everywhere else |
| 241 | + result.append(line.rstrip()) |
| 242 | + |
| 243 | + return "\n".join(result) |
| 244 | + |
| 245 | + |
| 246 | +def _postprocess_root(text: str, node: RenderTreeNode, context: RenderContext) -> str: |
| 247 | + """Combined postprocessor for all space control features. |
195 | 248 |
|
196 | | - This postprocessor works with mdformat-frontmatter without conflicts |
197 | | - since it operates on the rendered output, not the AST. |
| 249 | + Applies the following transformations in order: |
| 250 | + 1. Frontmatter spacing normalization |
| 251 | + 2. Trailing whitespace removal |
198 | 252 | """ |
199 | | - # Remove blank line(s) before heading: ---\n\n+# → ---\n# |
200 | | - text = re.sub(r"^(---\n)\n+(#)", r"\1\2", text, count=1, flags=re.MULTILINE) |
| 253 | + # 1. Frontmatter spacing |
| 254 | + text = _normalize_frontmatter_spacing(text) |
201 | 255 |
|
202 | | - # Normalize multiple blank lines to exactly one for non-headings |
203 | | - text = re.sub(r"^(---\n)\n{2,}(\S)", r"\1\n\2", text, count=1, flags=re.MULTILINE) |
| 256 | + # 2. Trailing whitespace removal |
| 257 | + text = _strip_trailing_whitespace(text) |
204 | 258 |
|
205 | 259 | return text |
206 | 260 |
|
|
0 commit comments