Skip to content

Commit 704c63f

Browse files
committed
Release 2.2.0
1 parent f6ecea5 commit 704c63f

14 files changed

+481
-230
lines changed

CHANGELOG.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.2.0] - 2026-02-18
9+
10+
### Changed
11+
- **Refactored MD/MDX Fixer architecture** - Modular fixer system for better extensibility
12+
- Split fixer logic into separate fixer classes in `fixers/` directory
13+
- Created base `Fixer` class for all fixers to inherit from
14+
- Each fixer type now has its own module:
15+
- `InvalidJsxTagsFixer` - Fixes JSX tags starting with numbers
16+
- `HtmlCommentsFixer` - Converts HTML comments to JSX comments
17+
- `UnclosedTagsFixer` - Fixes unclosed void elements
18+
- `InvalidAttributesFixer` - Converts HTML attributes to JSX
19+
- `SelfClosingTagsFixer` - Fixes self-closing tag spacing
20+
- `NumericEntitiesFixer` - Fixes malformed numeric HTML entities
21+
- `JsxCurlyBracesFixer` - Escapes curly braces in code-like contexts
22+
- `MarkdownFixer` now dynamically uses available fixers
23+
- Fixers can be customized by passing a custom list to `MarkdownFixer`
24+
25+
### Added
26+
- **Enhanced curly brace handling** - Improved JSX curly brace escaping
27+
- Automatically converts single backticks to double backticks for inline code containing curly braces
28+
- Wraps code-like patterns (e.g., `key:value:{variable}`) in backticks to prevent MDX interpretation
29+
- Preserves Markdown compatibility while preventing MDX build errors
30+
- Ignores curly braces inside code blocks (```mermaid, ```python, etc.)
31+
- Fixes `ReferenceError: variable is not defined` errors in Docusaurus builds
32+
33+
### Improved
34+
- Better code organization and maintainability
35+
- Easier to add new fixers by creating a new class inheriting from `Fixer`
36+
- More flexible fixer configuration
37+
838
## [2.1.0] - 2026-01-04
939
### Added
1040
- Support for HTTPS authentication with PAT tokens

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "docusync"
3-
version = "2.1.0"
3+
version = "2.2.0"
44
description = "CLI tool for syncing documentation from multiple repositories for Docusaurus"
55
readme = "README.md"
66
requires-python = ">=3.12"

src/docusync/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""DocuSync - Documentation synchronization tool for Docusaurus sites."""
22

3-
__version__ = "2.1.0"
3+
__version__ = "2.2.0"

src/docusync/fixers/__init__.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"""Fixers for Markdown/MDX files."""
2+
3+
from typing import List
4+
5+
from .base import Fixer
6+
from .html_comments import HtmlCommentsFixer
7+
from .invalid_attributes import InvalidAttributesFixer
8+
from .invalid_jsx_tags import InvalidJsxTagsFixer
9+
from .jsx_curly_braces import JsxCurlyBracesFixer
10+
from .numeric_entities import NumericEntitiesFixer
11+
from .self_closing_tags import SelfClosingTagsFixer
12+
from .unclosed_tags import UnclosedTagsFixer
13+
14+
15+
def get_all_fixers() -> List[Fixer]:
16+
"""Get all available fixers.
17+
18+
:returns: List of fixer instances
19+
"""
20+
return [
21+
InvalidJsxTagsFixer(),
22+
HtmlCommentsFixer(),
23+
UnclosedTagsFixer(),
24+
InvalidAttributesFixer(),
25+
SelfClosingTagsFixer(),
26+
NumericEntitiesFixer(),
27+
JsxCurlyBracesFixer(),
28+
]
29+
30+
31+
__all__ = [
32+
"Fixer",
33+
"get_all_fixers",
34+
"InvalidJsxTagsFixer",
35+
"HtmlCommentsFixer",
36+
"UnclosedTagsFixer",
37+
"InvalidAttributesFixer",
38+
"SelfClosingTagsFixer",
39+
"NumericEntitiesFixer",
40+
"JsxCurlyBracesFixer",
41+
]

src/docusync/fixers/base.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""Base class for all fixers."""
2+
3+
from abc import ABC, abstractmethod
4+
from typing import Tuple
5+
6+
7+
class Fixer(ABC):
8+
"""Base class for all markdown fixers."""
9+
10+
@property
11+
@abstractmethod
12+
def name(self) -> str:
13+
"""Return the name of this fixer."""
14+
pass
15+
16+
@abstractmethod
17+
def fix(self, content: str) -> Tuple[str, int]:
18+
"""Apply the fix to content.
19+
20+
:param content: The content to fix
21+
:returns: Tuple of (fixed_content, number_of_fixes_applied)
22+
"""
23+
pass
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Fixer for HTML comments."""
2+
3+
import re
4+
from typing import Tuple
5+
6+
from .base import Fixer
7+
8+
9+
class HtmlCommentsFixer(Fixer):
10+
"""Convert HTML comments to JSX comments in JSX context.
11+
12+
HTML comments <!-- --> can cause issues in MDX.
13+
"""
14+
15+
@property
16+
def name(self) -> str:
17+
"""Return the name of this fixer."""
18+
return "HTML comments to JSX"
19+
20+
def fix(self, content: str) -> Tuple[str, int]:
21+
"""Convert HTML comments to JSX comments.
22+
23+
:param content: The content to fix
24+
:returns: Tuple of (fixed_content, number_of_fixes_applied)
25+
"""
26+
# Find HTML comments not in code blocks
27+
in_code_block = False
28+
lines = content.split("\n")
29+
fixed_lines = []
30+
fixes = 0
31+
32+
for line in lines:
33+
# Track code blocks
34+
if line.strip().startswith("```"):
35+
in_code_block = not in_code_block
36+
fixed_lines.append(line)
37+
continue
38+
39+
if not in_code_block:
40+
# Replace HTML comments with JSX comments
41+
if "<!--" in line and "-->" in line:
42+
# Simple inline HTML comment
43+
new_line = re.sub(
44+
r"<!--\s*(.*?)\s*-->",
45+
r"{/* \1 */}",
46+
line,
47+
)
48+
if new_line != line:
49+
fixes += 1
50+
line = new_line
51+
52+
fixed_lines.append(line)
53+
54+
return "\n".join(fixed_lines), fixes
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""Fixer for invalid HTML attributes."""
2+
3+
import re
4+
from typing import Tuple
5+
6+
from .base import Fixer
7+
8+
9+
class InvalidAttributesFixer(Fixer):
10+
"""Fix invalid HTML attributes for JSX.
11+
12+
Convert class -> className, for -> htmlFor, etc.
13+
"""
14+
15+
@property
16+
def name(self) -> str:
17+
"""Return the name of this fixer."""
18+
return "HTML to JSX attributes"
19+
20+
def fix(self, content: str) -> Tuple[str, int]:
21+
"""Fix invalid HTML attributes.
22+
23+
:param content: The content to fix
24+
:returns: Tuple of (fixed_content, number_of_fixes_applied)
25+
"""
26+
fixes = 0
27+
28+
# class -> className
29+
pattern = r"<(\w+)([^>]*?\s)class="
30+
matches = re.findall(pattern, content)
31+
if matches:
32+
content = re.sub(
33+
pattern,
34+
r"<\1\2className=",
35+
content,
36+
)
37+
fixes += len(matches)
38+
39+
# for -> htmlFor (in label tags)
40+
pattern = r"<label([^>]*?\s)for="
41+
matches = re.findall(pattern, content)
42+
if matches:
43+
content = re.sub(
44+
pattern,
45+
r"<label\1htmlFor=",
46+
content,
47+
)
48+
fixes += len(matches)
49+
50+
return content, fixes
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""Fixer for invalid JSX tag names."""
2+
3+
import re
4+
from typing import Tuple
5+
6+
from .base import Fixer
7+
8+
9+
class InvalidJsxTagsFixer(Fixer):
10+
"""Fix JSX tags that start with numbers or invalid characters.
11+
12+
Example: <1something> -> &lt;1something&gt;
13+
"""
14+
15+
@property
16+
def name(self) -> str:
17+
"""Return the name of this fixer."""
18+
return "Invalid JSX tag names"
19+
20+
def fix(self, content: str) -> Tuple[str, int]:
21+
"""Fix JSX tags that start with invalid characters.
22+
23+
:param content: The content to fix
24+
:returns: Tuple of (fixed_content, number_of_fixes_applied)
25+
"""
26+
# Match HTML/JSX tags that start with invalid characters
27+
pattern = r"<(\d+[a-zA-Z_][\w-]*)"
28+
matches = re.findall(pattern, content)
29+
30+
if matches:
31+
# Escape these as HTML entities instead
32+
for match in matches:
33+
content = content.replace(f"<{match}", f"&lt;{match}").replace(
34+
f"</{match}>", f"&lt;/{match}&gt;"
35+
)
36+
37+
return content, len(matches)
38+
39+
return content, 0
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""Fixer for JSX curly braces."""
2+
3+
import re
4+
from typing import Tuple
5+
6+
from .base import Fixer
7+
8+
9+
class JsxCurlyBracesFixer(Fixer):
10+
"""Fix unescaped curly braces that might be interpreted as JSX.
11+
12+
Handles curly braces in two ways:
13+
1. In inline code with single backticks: converts to double backticks
14+
2. In text that looks like code (e.g., key:{value}): wraps in backticks
15+
"""
16+
17+
@property
18+
def name(self) -> str:
19+
"""Return the name of this fixer."""
20+
return "Curly brace escaping"
21+
22+
def fix(self, content: str) -> Tuple[str, int]:
23+
"""Fix curly braces in inline code and code-like contexts.
24+
25+
:param content: The content to fix
26+
:returns: Tuple of (fixed_content, number_of_fixes_applied)
27+
"""
28+
in_code_block = False
29+
lines = content.split("\n")
30+
fixed_lines = []
31+
fixes = 0
32+
33+
for line in lines:
34+
# Track code blocks (``` blocks) - ignore any format like ```mermaid, ```python, etc.
35+
if line.strip().startswith("```"):
36+
in_code_block = not in_code_block
37+
# Add code block delimiter without processing
38+
fixed_lines.append(line)
39+
continue
40+
41+
# Skip processing if we're inside a code block
42+
# This ensures curly braces in code blocks (mermaid, python, etc.) are not modified
43+
if not in_code_block:
44+
original_line = line
45+
46+
# Step 1: Convert single backticks to double if content has braces
47+
def convert_to_double_backticks(match: re.Match[str]) -> str:
48+
"""Convert single backticks to double if content has braces."""
49+
code_content = match.group(1)
50+
if "{" in code_content or "}" in code_content:
51+
return f"``{code_content}``"
52+
return str(match.group(0))
53+
54+
pattern1 = r"(?<!`)`([^`\n]+)`(?!`)"
55+
line = re.sub(pattern1, convert_to_double_backticks, line)
56+
57+
# Step 2: Wrap code-like patterns with curly braces in backticks
58+
# Split line by existing backticks to process only text segments
59+
def wrap_code_like_patterns(text: str) -> str:
60+
"""Wrap code-like patterns with curly braces in backticks."""
61+
# Pattern 1: key:value:{variable} or key:{variable} (e.g., bot:health:{bot_id})
62+
# Match word:word:{word} or word:{word}
63+
text = re.sub(
64+
r"(?<!`)(\w+:\w+:\{[\w_]+\}|\w+:\{[\w_]+\})(?!`)",
65+
r"`\1`",
66+
text,
67+
)
68+
# Pattern 2: standalone {variable_name} that looks like code
69+
# Only if not already in backticks and not part of JSX expression
70+
text = re.sub(
71+
r"(?<!`)(?<!\w)\{[\w_]+\}(?!`)(?!\s*[:=])",
72+
r"`\0`",
73+
text,
74+
)
75+
return text
76+
77+
# Process text segments outside of backticks
78+
parts = re.split(r"(`+[^`]*`+)", line)
79+
processed_parts = []
80+
for i, part in enumerate(parts):
81+
if part.startswith("`") and part.endswith("`"):
82+
# Already in backticks, keep as is
83+
processed_parts.append(part)
84+
else:
85+
# Process text segments
86+
processed_parts.append(wrap_code_like_patterns(part))
87+
88+
line = "".join(processed_parts)
89+
90+
if line != original_line:
91+
fixes += 1
92+
# If in_code_block is True, line remains unchanged and is added as-is
93+
94+
# Add processed line (or original if inside code block)
95+
fixed_lines.append(line)
96+
97+
return "\n".join(fixed_lines), fixes
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Fixer for numeric HTML entities."""
2+
3+
import re
4+
from typing import Tuple
5+
6+
from .base import Fixer
7+
8+
9+
class NumericEntitiesFixer(Fixer):
10+
"""Fix numeric HTML entities that might cause issues.
11+
12+
Ensures numeric entities are properly formatted.
13+
"""
14+
15+
@property
16+
def name(self) -> str:
17+
"""Return the name of this fixer."""
18+
return "Numeric entity formatting"
19+
20+
def fix(self, content: str) -> Tuple[str, int]:
21+
"""Fix malformed numeric entities.
22+
23+
:param content: The content to fix
24+
:returns: Tuple of (fixed_content, number_of_fixes_applied)
25+
"""
26+
# Fix malformed numeric entities
27+
pattern = r"&#(?!x)(\d+)(?![;\d])"
28+
matches = re.findall(pattern, content)
29+
if matches:
30+
content = re.sub(pattern, r"&#\1;", content)
31+
return content, len(matches)
32+
33+
return content, 0

0 commit comments

Comments
 (0)