Skip to content

Commit 1d92008

Browse files
jdmonacoclaude
andcommitted
Fix smart dash conversion escaping HTML comments and tags
Protect HTML comments (<!-- -->), multi-line comments, and HTML tags from -- → en-dash and --- → em-dash conversion using the existing placeholder mechanism. Bump version to 0.4.1. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 492572c commit 1d92008

3 files changed

Lines changed: 137 additions & 6 deletions

File tree

mdformat_space_control/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""An mdformat plugin for space control: EditorConfig indentation, tight lists, frontmatter spacing, smart dash conversion, explicit line breaks, and wikilinks."""
22

3-
__version__ = "0.4.0"
3+
__version__ = "0.4.1"
44

55
from .config import (
66
get_current_file,

mdformat_space_control/plugin.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,8 @@ def _convert_dash_sequences(text: str) -> str:
391391
"""Convert markdown dash sequences to Unicode em-dash and en-dash.
392392
393393
Converts ``---`` to em-dash (U+2014) and ``--`` to en-dash (U+2013).
394-
Preserves dashes inside fenced code blocks and inline code spans.
394+
Preserves dashes inside fenced code blocks, inline code spans,
395+
HTML comments, and HTML tags.
395396
Skips lines that are only dashes (thematic breaks, frontmatter delimiters).
396397
397398
Em-dash is matched first (longer sequence) to prevent ``---`` from being
@@ -400,6 +401,10 @@ def _convert_dash_sequences(text: str) -> str:
400401
# Patterns for inline code span placeholders
401402
inline_code_re = re.compile(r"(`+)(.+?)\1")
402403

404+
# HTML protection patterns
405+
html_comment_re = re.compile(r"<!--.*?-->")
406+
html_tag_re = re.compile(r"<[^>]+>")
407+
403408
# Dash conversion patterns: negative lookahead/behind prevent matching
404409
# 4+ dash sequences (e.g., ---- horizontal rules in some contexts)
405410
em_dash_re = re.compile(r"(?<!-)---(?!-)")
@@ -411,6 +416,7 @@ def _convert_dash_sequences(text: str) -> str:
411416
lines = text.split("\n")
412417
result = []
413418
in_code_block = False
419+
in_html_comment = False
414420

415421
for line in lines:
416422
stripped = line.lstrip()
@@ -419,23 +425,38 @@ def _convert_dash_sequences(text: str) -> str:
419425
result.append(line)
420426
elif in_code_block:
421427
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)
422433
elif only_dashes_re.match(line):
423434
result.append(line)
424435
else:
425-
# Protect inline code spans with placeholders
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
426443
placeholders = []
427444

428-
def _save_code_span(m: re.Match) -> str:
445+
def _save_placeholder(m: re.Match) -> str:
429446
placeholders.append(m.group(0))
430447
return f"\x00CODE{len(placeholders) - 1}\x00"
431448

432-
protected = inline_code_re.sub(_save_code_span, line)
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)
433454

434455
# Convert em-dash first (longer match), then en-dash
435456
protected = em_dash_re.sub("\u2014", protected)
436457
protected = en_dash_re.sub("\u2013", protected)
437458

438-
# Restore inline code spans
459+
# Restore placeholders
439460
for i, original in enumerate(placeholders):
440461
protected = protected.replace(f"\x00CODE{i}\x00", original)
441462

tests/test_dash_conversion.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,3 +275,113 @@ def test_empty_string(self):
275275
from mdformat_space_control.plugin import _convert_dash_sequences
276276

277277
assert _convert_dash_sequences("") == ""
278+
279+
def test_html_comment_preserved(self):
280+
"""Direct function test: HTML comments are preserved."""
281+
from mdformat_space_control.plugin import _convert_dash_sequences
282+
283+
assert _convert_dash_sequences("<!-- comment -->") == "<!-- comment -->"
284+
285+
def test_html_triple_dash_comment_preserved(self):
286+
"""Direct function test: triple-dash HTML comments are preserved."""
287+
from mdformat_space_control.plugin import _convert_dash_sequences
288+
289+
assert _convert_dash_sequences("<!--- comment --->") == "<!--- comment --->"
290+
291+
def test_html_tag_with_dashes_preserved(self):
292+
"""Direct function test: HTML tags with dash attributes are preserved."""
293+
from mdformat_space_control.plugin import _convert_dash_sequences
294+
295+
text = '<div data-value="test--value">'
296+
assert _convert_dash_sequences(text) == text
297+
298+
def test_html_self_closing_tag_preserved(self):
299+
"""Direct function test: self-closing HTML tags with dashes preserved."""
300+
from mdformat_space_control.plugin import _convert_dash_sequences
301+
302+
text = '<img alt="a--b" />'
303+
assert _convert_dash_sequences(text) == text
304+
305+
def test_html_comment_with_surrounding_dashes(self):
306+
"""Direct function test: dashes outside HTML convert, inside preserved."""
307+
from mdformat_space_control.plugin import _convert_dash_sequences
308+
309+
text = "word--word <!-- comment --> word---word"
310+
expected = "word\u2013word <!-- comment --> word\u2014word"
311+
assert _convert_dash_sequences(text) == expected
312+
313+
def test_multiline_html_comment_preserved(self):
314+
"""Direct function test: multi-line HTML comments are preserved."""
315+
from mdformat_space_control.plugin import _convert_dash_sequences
316+
317+
text = "<!--\ncomment with--dashes\n-->"
318+
assert _convert_dash_sequences(text) == text
319+
320+
def test_multiline_html_comment_with_surrounding_text(self):
321+
"""Direct function test: text around multi-line HTML comment converts."""
322+
from mdformat_space_control.plugin import _convert_dash_sequences
323+
324+
text = "before--after\n<!--\ncomment--here\n-->\nmore--text"
325+
expected = "before\u2013after\n<!--\ncomment--here\n-->\nmore\u2013text"
326+
assert _convert_dash_sequences(text) == expected
327+
328+
329+
class TestDashConversionHTML:
330+
"""Tests for HTML comment and tag preservation through mdformat."""
331+
332+
def test_single_line_html_comment(self):
333+
"""Single-line HTML comment should be preserved."""
334+
input_text = "<!-- comment -->\n"
335+
result = mdformat.text(input_text, extensions={"space_control"})
336+
assert "<!--" in result
337+
assert "-->" in result
338+
assert "\u2013" not in result
339+
assert "\u2014" not in result
340+
341+
def test_triple_dash_comment(self):
342+
"""Triple-dash HTML comment should be preserved."""
343+
input_text = "<!--- comment --->\n"
344+
result = mdformat.text(input_text, extensions={"space_control"})
345+
assert "<!---" in result
346+
assert "--->" in result
347+
assert "\u2014" not in result
348+
349+
def test_block_level_html_comment(self):
350+
"""Block-level HTML comment (own paragraph) should be preserved."""
351+
input_text = "Text.\n\n<!-- block comment -->\n\nMore text.\n"
352+
result = mdformat.text(input_text, extensions={"space_control"})
353+
assert "<!-- block comment -->" in result
354+
355+
def test_inline_html_comment_with_dashes(self):
356+
"""Dashes outside HTML comment convert; comment preserved."""
357+
input_text = "Text--here <!-- comment --> more---text.\n"
358+
result = mdformat.text(input_text, extensions={"space_control"})
359+
assert "<!-- comment -->" in result
360+
assert "\u2013" in result # en-dash from --
361+
assert "\u2014" in result # em-dash from ---
362+
363+
def test_multi_line_html_comment(self):
364+
"""Multi-line HTML comment should be preserved."""
365+
input_text = "Text.\n\n<!--\ncomment with--dashes\n-->\n\nMore text.\n"
366+
result = mdformat.text(input_text, extensions={"space_control"})
367+
assert "\u2013" not in result
368+
assert "\u2014" not in result
369+
370+
def test_html_tag_with_dash_attributes(self):
371+
"""HTML tags with dash-containing attributes should be preserved."""
372+
input_text = '<div data-value="test--value">\n\nContent.\n\n</div>\n'
373+
result = mdformat.text(input_text, extensions={"space_control"})
374+
assert 'data-value="test--value"' in result
375+
376+
def test_self_closing_tag_with_dashes(self):
377+
"""Self-closing HTML tags with dashes should be preserved."""
378+
input_text = '<img alt="a--b" />\n'
379+
result = mdformat.text(input_text, extensions={"space_control"})
380+
assert 'alt="a--b"' in result
381+
382+
def test_dashes_outside_html_still_convert(self):
383+
"""Dashes outside HTML elements should still convert normally."""
384+
input_text = "word--word and word---word\n"
385+
expected = "word\u2013word and word\u2014word\n"
386+
result = mdformat.text(input_text, extensions={"space_control"})
387+
assert result == expected

0 commit comments

Comments
 (0)