mdformat-space-control 0.3.2__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,7 +32,7 @@ uv run python -m pytest tests/test_editorconfig.py # Run specific test file
32
32
  mdformat_space_control/
33
33
  ├── __init__.py # Public API exports, version
34
34
  ├── config.py # EditorConfig lookup, file context tracking
35
- └── plugin.py # List renderers (RENDERERS dict)
35
+ └── plugin.py # Renderers, postprocessors, parser extensions
36
36
  ```
37
37
 
38
38
  **Key components:**
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdformat_space_control
3
- Version: 0.3.2
4
- Summary: An mdformat plugin for space control: EditorConfig indentation, tight lists, frontmatter spacing, and wikilinks.
3
+ Version: 0.4.0
4
+ Summary: An mdformat plugin for space control: EditorConfig indentation, tight lists, frontmatter spacing, smart dash conversion, explicit line breaks, and wikilinks.
5
5
  Project-URL: Homepage, https://github.com/jdmonaco/mdformat-space-control
6
6
  Author-email: Joseph Monaco <joe@selfmotion.net>
7
7
  License-File: LICENSE
8
- Keywords: editorconfig,formatter,frontmatter,indentation,lists,markdown,mdformat,obsidian,wikilink
8
+ Keywords: editorconfig,formatter,frontmatter,indentation,lists,markdown,mdformat,obsidian,typography,wikilink
9
9
  Classifier: Development Status :: 3 - Alpha
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: License :: OSI Approved :: MIT License
@@ -39,6 +39,7 @@ An [mdformat](https://github.com/executablebooks/mdformat) plugin that provides
39
39
  - **Consecutive blank line normalization**: Limits runs of 3+ empty lines to a maximum of 2
40
40
  - **Trailing whitespace removal**: Strips trailing whitespace outside code blocks
41
41
  - **Escaped link repair**: Fixes malformed multi-line links from web-clipped content
42
+ - **Smart dash conversion**: Converts `--` to en-dash (–) and `---` to em-dash (—), preserving code blocks and inline code
42
43
  - **Wikilink preservation**: Handles Obsidian-style `[[links]]`, `[[links|aliases]]`, `[[page#heading]]`, `[[page#^blockid]]`, and `![[embeds]]`
43
44
  - **Explicit line breaks**: Converts soft breaks to hard breaks (`\` + newline) so source line breaks render visually in all Markdown renderers
44
45
 
@@ -173,6 +174,27 @@ finally:
173
174
  set_current_file(None)
174
175
  ```
175
176
 
177
+ ### Smart Dash Conversion
178
+
179
+ Markdown dash sequences are automatically converted to their Unicode equivalents:
180
+
181
+ - `--` → en-dash (–, U+2013)
182
+ - `---` → em-dash (—, U+2014)
183
+
184
+ **Before:**
185
+ ```markdown
186
+ The result---unexpected as it was---changed everything.
187
+ Pages 10--20 of the report.
188
+ ```
189
+
190
+ **After:**
191
+ ```markdown
192
+ The result—unexpected as it was—changed everything.
193
+ Pages 10–20 of the report.
194
+ ```
195
+
196
+ Dashes are preserved inside fenced code blocks and inline code spans. Thematic breaks (`---`) and frontmatter delimiters are not affected. Sequences of 4+ dashes are left unchanged.
197
+
176
198
  ### Wikilink Preservation
177
199
 
178
200
  Obsidian-style wikilinks are preserved during formatting:
@@ -11,6 +11,7 @@ An [mdformat](https://github.com/executablebooks/mdformat) plugin that provides
11
11
  - **Consecutive blank line normalization**: Limits runs of 3+ empty lines to a maximum of 2
12
12
  - **Trailing whitespace removal**: Strips trailing whitespace outside code blocks
13
13
  - **Escaped link repair**: Fixes malformed multi-line links from web-clipped content
14
+ - **Smart dash conversion**: Converts `--` to en-dash (–) and `---` to em-dash (—), preserving code blocks and inline code
14
15
  - **Wikilink preservation**: Handles Obsidian-style `[[links]]`, `[[links|aliases]]`, `[[page#heading]]`, `[[page#^blockid]]`, and `![[embeds]]`
15
16
  - **Explicit line breaks**: Converts soft breaks to hard breaks (`\` + newline) so source line breaks render visually in all Markdown renderers
16
17
 
@@ -145,6 +146,27 @@ finally:
145
146
  set_current_file(None)
146
147
  ```
147
148
 
149
+ ### Smart Dash Conversion
150
+
151
+ Markdown dash sequences are automatically converted to their Unicode equivalents:
152
+
153
+ - `--` → en-dash (–, U+2013)
154
+ - `---` → em-dash (—, U+2014)
155
+
156
+ **Before:**
157
+ ```markdown
158
+ The result---unexpected as it was---changed everything.
159
+ Pages 10--20 of the report.
160
+ ```
161
+
162
+ **After:**
163
+ ```markdown
164
+ The result—unexpected as it was—changed everything.
165
+ Pages 10–20 of the report.
166
+ ```
167
+
168
+ Dashes are preserved inside fenced code blocks and inline code spans. Thematic breaks (`---`) and frontmatter delimiters are not affected. Sequences of 4+ dashes are left unchanged.
169
+
148
170
  ### Wikilink Preservation
149
171
 
150
172
  Obsidian-style wikilinks are preserved during formatting:
@@ -1,6 +1,6 @@
1
- """An mdformat plugin for space control: EditorConfig indentation, tight lists, frontmatter spacing, explicit line breaks, and wikilinks."""
1
+ """An mdformat plugin for space control: EditorConfig indentation, tight lists, frontmatter spacing, smart dash conversion, explicit line breaks, and wikilinks."""
2
2
 
3
- __version__ = "0.3.2"
3
+ __version__ = "0.4.0"
4
4
 
5
5
  from .config import (
6
6
  get_current_file,
@@ -4,6 +4,7 @@ Provides custom renderers that combine:
4
4
  - EditorConfig-based indentation settings
5
5
  - Tight list formatting with multi-paragraph awareness
6
6
  - Frontmatter spacing normalization
7
+ - Smart dash conversion (-- → en-dash, --- → em-dash)
7
8
  - Trailing whitespace removal (outside code blocks)
8
9
  - Wikilink preservation ([[link]] and ![[embed]] syntax)
9
10
  """
@@ -112,12 +113,55 @@ def _get_indent(default_width: int) -> tuple[str, int]:
112
113
  return (" " * size, size)
113
114
 
114
115
 
116
+ def _is_task_list_item(node: RenderTreeNode) -> bool:
117
+ """Check if a list item is a GFM task list item.
118
+
119
+ When mdformat-gfm is installed, the tasklists markdown-it plugin annotates
120
+ task list items with a 'task-list-item' CSS class.
121
+ """
122
+ classes = node.attrs.get("class", "")
123
+ return isinstance(classes, str) and "task-list-item" in classes
124
+
125
+
126
+ def _extract_task_checkbox(node: RenderTreeNode) -> str | None:
127
+ """Extract and remove the task checkbox HTML from a task list item node.
128
+
129
+ The tasklists markdown-it plugin injects an HTML <input> element as the
130
+ first child of the inline token. This function extracts the checked state
131
+ and removes the HTML node so it doesn't appear in the rendered output.
132
+
133
+ Returns:
134
+ "x" for checked, " " for unchecked, or None if not a task list item.
135
+ """
136
+ if not _is_task_list_item(node):
137
+ return None
138
+
139
+ paragraph_node = node.children[0]
140
+ inline_node = paragraph_node.children[0]
141
+ if inline_node.type != "inline" or not inline_node.children:
142
+ return None
143
+
144
+ html_inline_node = inline_node.children[0]
145
+ if 'class="task-list-item-checkbox"' not in getattr(html_inline_node, "content", ""):
146
+ return None
147
+
148
+ # Remove the HTML checkbox node before rendering
149
+ inline_node.children.remove(html_inline_node)
150
+ return "x" if 'checked="checked"' in html_inline_node.content else " "
151
+
152
+
115
153
  def _render_list_item(node: RenderTreeNode, context: RenderContext) -> str:
116
154
  """Render a list item with appropriate tight/loose formatting.
117
155
 
118
156
  For single-paragraph items in tight lists, use tight formatting.
119
157
  For multi-paragraph items, preserve loose formatting.
158
+
159
+ Handles GFM task list items when mdformat-gfm is installed: extracts the
160
+ checkbox state from the HTML token and prepends [x] or [ ] to the output.
120
161
  """
162
+ # Extract task checkbox before rendering (mutates the node tree)
163
+ checkmark = _extract_task_checkbox(node)
164
+
121
165
  # Check if this item has multiple paragraphs
122
166
  if has_multiple_paragraphs(node):
123
167
  # Use loose list formatting for multi-paragraph items
@@ -136,6 +180,13 @@ def _render_list_item(node: RenderTreeNode, context: RenderContext) -> str:
136
180
 
137
181
  if not text.strip():
138
182
  return ""
183
+
184
+ if checkmark is not None:
185
+ # Strip leading space entities left after removing the HTML checkbox node
186
+ text = re.sub(r"^(&#32;)+", "", text)
187
+ text = text.lstrip()
188
+ return f"[{checkmark}] {text}"
189
+
139
190
  return text
140
191
 
141
192
 
@@ -336,6 +387,63 @@ def _strip_trailing_whitespace(text: str) -> str:
336
387
  return "\n".join(result)
337
388
 
338
389
 
390
+ def _convert_dash_sequences(text: str) -> str:
391
+ """Convert markdown dash sequences to Unicode em-dash and en-dash.
392
+
393
+ Converts ``---`` to em-dash (U+2014) and ``--`` to en-dash (U+2013).
394
+ Preserves dashes inside fenced code blocks and inline code spans.
395
+ Skips lines that are only dashes (thematic breaks, frontmatter delimiters).
396
+
397
+ Em-dash is matched first (longer sequence) to prevent ``---`` from being
398
+ partially converted as en-dash + hyphen.
399
+ """
400
+ # Patterns for inline code span placeholders
401
+ inline_code_re = re.compile(r"(`+)(.+?)\1")
402
+
403
+ # Dash conversion patterns: negative lookahead/behind prevent matching
404
+ # 4+ dash sequences (e.g., ---- horizontal rules in some contexts)
405
+ em_dash_re = re.compile(r"(?<!-)---(?!-)")
406
+ en_dash_re = re.compile(r"(?<!-)--(?!-)")
407
+
408
+ # Lines that are only dashes (thematic breaks, frontmatter delimiters)
409
+ only_dashes_re = re.compile(r"^-{2,}\s*$")
410
+
411
+ lines = text.split("\n")
412
+ result = []
413
+ in_code_block = False
414
+
415
+ for line in lines:
416
+ stripped = line.lstrip()
417
+ if stripped.startswith("```") or stripped.startswith("~~~"):
418
+ in_code_block = not in_code_block
419
+ result.append(line)
420
+ elif in_code_block:
421
+ result.append(line)
422
+ elif only_dashes_re.match(line):
423
+ result.append(line)
424
+ else:
425
+ # Protect inline code spans with placeholders
426
+ placeholders = []
427
+
428
+ def _save_code_span(m: re.Match) -> str:
429
+ placeholders.append(m.group(0))
430
+ return f"\x00CODE{len(placeholders) - 1}\x00"
431
+
432
+ protected = inline_code_re.sub(_save_code_span, line)
433
+
434
+ # Convert em-dash first (longer match), then en-dash
435
+ protected = em_dash_re.sub("\u2014", protected)
436
+ protected = en_dash_re.sub("\u2013", protected)
437
+
438
+ # Restore inline code spans
439
+ for i, original in enumerate(placeholders):
440
+ protected = protected.replace(f"\x00CODE{i}\x00", original)
441
+
442
+ result.append(protected)
443
+
444
+ return "\n".join(result)
445
+
446
+
339
447
  def _normalize_consecutive_blank_lines(text: str) -> str:
340
448
  """Limit consecutive blank lines to a maximum of 2.
341
449
 
@@ -375,8 +483,9 @@ def _postprocess_root(text: str, node: RenderTreeNode, context: RenderContext) -
375
483
  Applies the following transformations in order:
376
484
  1. Frontmatter spacing normalization
377
485
  2. Escaped link repair
378
- 3. Consecutive blank line normalization
379
- 4. Trailing whitespace removal
486
+ 3. Smart dash conversion (-- → en-dash, --- → em-dash)
487
+ 4. Consecutive blank line normalization
488
+ 5. Trailing whitespace removal
380
489
  """
381
490
  # 1. Frontmatter spacing
382
491
  text = _normalize_frontmatter_spacing(text)
@@ -384,10 +493,13 @@ def _postprocess_root(text: str, node: RenderTreeNode, context: RenderContext) -
384
493
  # 2. Repair escaped links (before trailing whitespace removal)
385
494
  text = _repair_escaped_links(text)
386
495
 
387
- # 3. Limit consecutive blank lines to 2
496
+ # 3. Smart dash conversion
497
+ text = _convert_dash_sequences(text)
498
+
499
+ # 4. Limit consecutive blank lines to 2
388
500
  text = _normalize_consecutive_blank_lines(text)
389
501
 
390
- # 4. Trailing whitespace removal
502
+ # 5. Trailing whitespace removal
391
503
  text = _strip_trailing_whitespace(text)
392
504
 
393
505
  return text
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
5
5
  [project]
6
6
  name = "mdformat_space_control"
7
7
  dynamic = ["version"]
8
- description = "An mdformat plugin for space control: EditorConfig indentation, tight lists, frontmatter spacing, and wikilinks."
8
+ description = "An mdformat plugin for space control: EditorConfig indentation, tight lists, frontmatter spacing, smart dash conversion, explicit line breaks, and wikilinks."
9
9
  authors = [
10
10
  { name = "Joseph Monaco", email = "joe@selfmotion.net" },
11
11
  ]
@@ -22,7 +22,7 @@ classifiers = [
22
22
  "Topic :: Software Development :: Libraries :: Python Modules",
23
23
  "Topic :: Text Processing :: Markup :: Markdown",
24
24
  ]
25
- keywords = ["mdformat", "markdown", "formatter", "editorconfig", "indentation", "lists", "frontmatter", "wikilink", "obsidian"]
25
+ keywords = ["mdformat", "markdown", "formatter", "editorconfig", "indentation", "lists", "frontmatter", "wikilink", "obsidian", "typography"]
26
26
  requires-python = ">=3.10"
27
27
  dependencies = [
28
28
  "mdformat>=0.7.0",