Skip to main content

mdformat-space-control

Build Status PyPI version

An mdformat plugin that provides unified control over Markdown spacing:

  • EditorConfig support: Configure list indentation via .editorconfig files
  • Tight list formatting: Automatically removes unnecessary blank lines between list items
  • Frontmatter spacing: Normalizes spacing after YAML frontmatter (works with mdformat-frontmatter)
  • Consecutive blank line normalization: Limits runs of 3+ empty lines to a maximum of 2
  • Trailing whitespace removal: Strips trailing whitespace outside code blocks
  • Escaped link repair: Fixes malformed multi-line links from web-clipped content
  • Smart dash conversion: Converts -- to en-dash (–) and --- to em-dash (—), preserving code blocks, inline code, HTML comments, HTML tags, URIs, CLI options, wikilinks, link destinations, and math
  • Wikilink preservation: Handles Obsidian-style [[links]], [[links|aliases]], [[page#heading]], [[page#^blockid]], and ![[embeds]]
  • Math preservation: Carries $inline$, $$inline$$, and $$ display-math LaTeX through verbatim, so \alpha, _, \,, and \{ are not re-escaped
  • Soft break joining: Joins soft breaks (plain newlines within paragraphs) into single lines, normalizing to single-line paragraphs

Installation

pip install mdformat-space-control

Or with pipx for command-line usage:

pipx install mdformat
pipx inject mdformat mdformat-space-control

Usage

After installation, mdformat will automatically use this plugin:

mdformat your-file.md

EditorConfig Support

Create an .editorconfig file in your project:

# .editorconfig
root = true

[*.md]
indent_style = space
indent_size = 4

Nested lists will use the configured indentation:

Before:

- Item 1
  - Nested item
- Item 2

After (with 4-space indent):

- Item 1
    - Nested item
- Item 2

Tight List Formatting

Lists with single-paragraph items are automatically formatted as tight lists:

Before:

- Item 1

- Item 2

- Item 3

After:

- Item 1
- Item 2
- Item 3

Multi-paragraph items preserve loose formatting:

- First item with multiple paragraphs

  Second paragraph of first item

- Second item

Frontmatter Spacing

When used with mdformat-frontmatter, this plugin removes blank lines between the frontmatter closing delimiter and the first content block:

Before:

---
title: My Document
---


# Introduction

After:

---
title: My Document
---
# Introduction

Install both plugins for this feature:

pip install mdformat-space-control mdformat-frontmatter

EditorConfig Properties

Property Status Notes
indent_style Supported space or tab for list indentation
indent_size Supported Number of spaces per indent level
tab_width Supported Used when indent_size = tab

Python API

When using the Python API, you can set the file context for EditorConfig lookup:

import mdformat
from mdformat_space_control import set_current_file

set_current_file("/path/to/your/file.md")
try:
    result = mdformat.text(markdown_text, extensions={"space_control"})
finally:
    set_current_file(None)

Smart Dash Conversion

Markdown dash sequences are automatically converted to their Unicode equivalents:

  • -- → en-dash (–, U+2013)
  • --- → em-dash (—, U+2014)

Before:

The result---unexpected as it was---changed everything.
Pages 10--20 of the report.

After:

The result—unexpected as it was—changed everything.
Pages 10–20 of the report.

Dashes are preserved inside fenced code blocks, inline code spans, HTML comments, HTML tags, URIs (gs://bucket--name), CLI options (--output), wikilink targets (![[images/a--b/frame.jpg]]), markdown link destinations ([text](docs/a--b.md)), and dollar-delimited math ($x--y$, $$ blocks). Thematic breaks (---), GFM table separator rows (| -- | -- |), and frontmatter delimiters are not affected. Sequences of 4+ dashes are left unchanged.

These exclusions apply equally inside blockquotes, at any nesting depth: leading > markers are stripped before block-level patterns are matched, so tables and code fences in a blockquote or Obsidian callout are preserved just as they are at the top level.

Obsidian-style wikilinks are preserved during formatting:

Link to [[another note]] or [[note|with alias]].
Embed an image: ![[photo.jpg]]
Link to heading: [[note#section]]
Block reference: [[note#^blockid]]

Wikilinks inside markdown link text are correctly handled without duplication:

[![[image.jpg]]](http://example.com)

Literal square brackets inside a target are preserved (e.g. a note titled like an email subject), and are not escaped as stray link syntax:

[[[EXTERNAL] requesting feedback]]

Inside GFM table cells, an alias pipe is escaped to \| so the table parser does not split the wikilink across two columns (this requires mdformat-gfm). The escaped pipe renders correctly in Obsidian, and prose wikilinks keep a bare |:

| Note | Status |
| -- | -- |
| [[path/to/note|Short Alias]] | Active |

Math Preservation

Dollar-delimited math is parsed as opaque tokens and rendered verbatim. Without this, markdown-it treats LaTeX as prose and the CommonMark renderer re-escapes it: \alpha becomes \\alpha, _ becomes \_, and \, \{ \| lose their backslash, which breaks MathJax rendering in Obsidian.

Inline $\mathbf{a}_{t:t+H} \mid \mathbf{o}_t$ and display $$E = mc^2$$ in a paragraph.

$$
\begin{aligned}
L &= \sum_{i=1}^{N} \log p(x_i \mid \theta) \\
  &= \tfrac{1}{2} \|w\|_2^2
\end{aligned}
$$

Rules, chosen to match how Obsidian pairs delimiters:

  • Any two $ on a line pair, including next to spaces or digits, so $5 and $6 is carried verbatim rather than escaped. A lone $ and an escaped \$ are prose.
  • A line starting with $$ opens a display block that closes on a line ending with $$; a single-line $$x$$ stays on one line. Blocks keep their internal line structure and relative indentation, work inside list items and blockquotes, and may directly follow a paragraph line without a blank line.
  • An unclosed $$ is not a block and formats as prose.
  • Equation labels ($$x$$ (eq1), MyST syntax) are not recognized.

Known gaps: a $ inside an inline code span can still be paired with a $ outside it on the same line, as in MathJax; and | inside math in a GFM table cell must be written \|, as Obsidian requires.

Soft Break Joining

Soft breaks (plain newlines within paragraphs) are joined into single lines with spaces. This normalizes editor-inserted line wraps to single-line paragraphs, matching CommonMark rendering behavior where soft breaks produce spaces in HTML output. The joining applies to paragraphs, list items, and blockquotes.

Before:

This is a paragraph with
a soft line break in the source.

After:

This is a paragraph with a soft line break in the source.

Explicit hard breaks (\ + newline) are preserved unchanged.

Compatible Plugins

This plugin is tested to work alongside:

For formatting files in an Obsidian vault, the recommended install is:

pip install mdformat-space-control mdformat-frontmatter mdformat-gfm

Note: Wikilink support is built-in; mdformat-wikilink is not needed.

Development

# Install dependencies
uv sync

# Run tests
uv run python -m pytest

# Run with coverage
uv run python -m pytest --cov=mdformat_space_control

License

MIT - see LICENSE file for details.

Metadata

Release files for mdformat-space-control 0.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for mdformat-space-control 0.5.0
File Size Uploaded
mdformat_space_control-0.5.0.tar.gz 27.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for mdformat-space-control 0.5.0
File Interpreter ABI Platform
mdformat_space_control-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 44.8 kB

Release files / mdformat_space_control-0.5.0.tar.gz

Download URL mdformat_space_control-0.5.0.tar.gz
Size 27.5 kB
Tags Source
SHA-256 checksum
How to use checksums
52418d30c0b8b204f7135bd859300af321718fe36b31e04a1195d31762a914e0
BLAKE2b-256 checksum
How to use checksums
70ba4627949b0146dc8e5c3b830822bffc975b131a79352da5cf9393b0b4384e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.15

Release files / mdformat_space_control-0.5.0-py3-none-any.whl

Download URL mdformat_space_control-0.5.0-py3-none-any.whl
Size 17.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a288ff402096cda1084be3d4775e59c972f5e1b29aaeef4516fef8b66ce56c50
BLAKE2b-256 checksum
How to use checksums
3d2209a9ae22ad775561e0ef249b5751077f968071f4a04385dc469db3a837de
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.15

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 release files

0.4.10

2 release files

0.4.9

2 release files

0.4.8

2 release files

0.4.7

2 release files

0.4.6

2 release files

0.4.5

2 release files

0.4.4

2 release files

0.4.3

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.2

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page