mdformat-space-control 0.3.2__tar.gz → 0.4.1__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.1
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.1"
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,84 @@ 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, inline code spans,
395
+ HTML comments, and HTML tags.
396
+ Skips lines that are only dashes (thematic breaks, frontmatter delimiters).
397
+
398
+ Em-dash is matched first (longer sequence) to prevent ``---`` from being
399
+ partially converted as en-dash + hyphen.
400
+ """
401
+ # Patterns for inline code span placeholders
402
+ inline_code_re = re.compile(r"(`+)(.+?)\1")
403
+
404
+ # HTML protection patterns
405
+ html_comment_re = re.compile(r"<!--.*?-->")
406
+ html_tag_re = re.compile(r"<[^>]+>")
407
+
408
+ # Dash conversion patterns: negative lookahead/behind prevent matching
409
+ # 4+ dash sequences (e.g., ---- horizontal rules in some contexts)
410
+ em_dash_re = re.compile(r"(?<!-)---(?!-)")
411
+ en_dash_re = re.compile(r"(?<!-)--(?!-)")
412
+
413
+ # Lines that are only dashes (thematic breaks, frontmatter delimiters)
414
+ only_dashes_re = re.compile(r"^-{2,}\s*$")
415
+
416
+ lines = text.split("\n")
417
+ result = []
418
+ in_code_block = False
419
+ in_html_comment = False
420
+
421
+ for line in lines:
422
+ stripped = line.lstrip()
423
+ if stripped.startswith("```") or stripped.startswith("~~~"):
424
+ in_code_block = not in_code_block
425
+ result.append(line)
426
+ elif in_code_block:
427
+ result.append(line)
428
+ elif in_html_comment:
429
+ # Inside a multi-line HTML comment, skip dash conversion
430
+ if "-->" in line:
431
+ in_html_comment = False
432
+ result.append(line)
433
+ elif only_dashes_re.match(line):
434
+ result.append(line)
435
+ else:
436
+ # Check for multi-line HTML comment opening (no closing on same line)
437
+ if "<!--" in line and "-->" not in line:
438
+ in_html_comment = True
439
+ result.append(line)
440
+ continue
441
+
442
+ # Protect inline code spans and HTML elements with placeholders
443
+ placeholders = []
444
+
445
+ def _save_placeholder(m: re.Match) -> str:
446
+ placeholders.append(m.group(0))
447
+ return f"\x00CODE{len(placeholders) - 1}\x00"
448
+
449
+ protected = inline_code_re.sub(_save_placeholder, line)
450
+
451
+ # Protect HTML comments first (longer matches), then tags
452
+ protected = html_comment_re.sub(_save_placeholder, protected)
453
+ protected = html_tag_re.sub(_save_placeholder, protected)
454
+
455
+ # Convert em-dash first (longer match), then en-dash
456
+ protected = em_dash_re.sub("\u2014", protected)
457
+ protected = en_dash_re.sub("\u2013", protected)
458
+
459
+ # Restore placeholders
460
+ for i, original in enumerate(placeholders):
461
+ protected = protected.replace(f"\x00CODE{i}\x00", original)
462
+
463
+ result.append(protected)
464
+
465
+ return "\n".join(result)
466
+
467
+
339
468
  def _normalize_consecutive_blank_lines(text: str) -> str:
340
469
  """Limit consecutive blank lines to a maximum of 2.
341
470
 
@@ -375,8 +504,9 @@ def _postprocess_root(text: str, node: RenderTreeNode, context: RenderContext) -
375
504
  Applies the following transformations in order:
376
505
  1. Frontmatter spacing normalization
377
506
  2. Escaped link repair
378
- 3. Consecutive blank line normalization
379
- 4. Trailing whitespace removal
507
+ 3. Smart dash conversion (-- → en-dash, --- → em-dash)
508
+ 4. Consecutive blank line normalization
509
+ 5. Trailing whitespace removal
380
510
  """
381
511
  # 1. Frontmatter spacing
382
512
  text = _normalize_frontmatter_spacing(text)
@@ -384,10 +514,13 @@ def _postprocess_root(text: str, node: RenderTreeNode, context: RenderContext) -
384
514
  # 2. Repair escaped links (before trailing whitespace removal)
385
515
  text = _repair_escaped_links(text)
386
516
 
387
- # 3. Limit consecutive blank lines to 2
517
+ # 3. Smart dash conversion
518
+ text = _convert_dash_sequences(text)
519
+
520
+ # 4. Limit consecutive blank lines to 2
388
521
  text = _normalize_consecutive_blank_lines(text)
389
522
 
390
- # 4. Trailing whitespace removal
523
+ # 5. Trailing whitespace removal
391
524
  text = _strip_trailing_whitespace(text)
392
525
 
393
526
  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",