markdown-table-fixer 0.1.2__tar.gz → 0.1.3__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.
- {markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/PKG-INFO +3 -1
- {markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/pyproject.toml +2 -0
- {markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/src/markdown_table_fixer/_version.py +2 -2
- {markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/src/markdown_table_fixer/cli.py +25 -30
- {markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/src/markdown_table_fixer/models.py +10 -2
- {markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/src/markdown_table_fixer/table_fixer.py +275 -19
- {markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/src/markdown_table_fixer/table_parser.py +45 -2
- {markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/src/markdown_table_fixer/table_validator.py +17 -4
- {markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/.gitignore +0 -0
- {markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/.pre-commit-hooks.yaml +0 -0
- {markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/LICENSE +0 -0
- {markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/LICENSES/Apache-2.0.txt +0 -0
- {markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/README.md +0 -0
- {markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/scripts/README.md +0 -0
- {markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/src/markdown_table_fixer/__init__.py +0 -0
- {markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/src/markdown_table_fixer/exceptions.py +0 -0
- {markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/src/markdown_table_fixer/git_config.py +0 -0
- {markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/src/markdown_table_fixer/github_client.py +0 -0
- {markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/src/markdown_table_fixer/graphql_queries.py +0 -0
- {markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/src/markdown_table_fixer/pr_fixer.py +0 -0
- {markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/src/markdown_table_fixer/pr_scanner.py +0 -0
- {markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/src/markdown_table_fixer/progress_tracker.py +0 -0
- {markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/src/markdown_table_fixer/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: markdown-table-fixer
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Markdown table formatter and linter with GitHub integration
|
|
5
5
|
Project-URL: Homepage, https://github.com/lfit/markdown-table-fixer
|
|
6
6
|
Project-URL: Repository, https://github.com/lfit/markdown-table-fixer.git
|
|
@@ -25,6 +25,7 @@ Requires-Python: <3.14,>=3.10
|
|
|
25
25
|
Requires-Dist: aiolimiter>=1.2.0
|
|
26
26
|
Requires-Dist: httpx[http2]>=0.28.0
|
|
27
27
|
Requires-Dist: pydantic>=2.10.3
|
|
28
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
28
29
|
Requires-Dist: rich>=13.9.4
|
|
29
30
|
Requires-Dist: tenacity>=9.0.0
|
|
30
31
|
Requires-Dist: typer>=0.15.0
|
|
@@ -37,6 +38,7 @@ Requires-Dist: pytest-cov>=6.0.0; extra == 'dev'
|
|
|
37
38
|
Requires-Dist: pytest-mock>=3.14.0; extra == 'dev'
|
|
38
39
|
Requires-Dist: pytest>=8.3.4; extra == 'dev'
|
|
39
40
|
Requires-Dist: ruff>=0.8.4; extra == 'dev'
|
|
41
|
+
Requires-Dist: types-pyyaml>=6.0.0; extra == 'dev'
|
|
40
42
|
Description-Content-Type: text/markdown
|
|
41
43
|
|
|
42
44
|
<!--
|
|
@@ -38,6 +38,7 @@ dependencies = [
|
|
|
38
38
|
"aiolimiter>=1.2.0",
|
|
39
39
|
"tenacity>=9.0.0",
|
|
40
40
|
"wcwidth>=0.2.13",
|
|
41
|
+
"pyyaml>=6.0.0",
|
|
41
42
|
]
|
|
42
43
|
|
|
43
44
|
[project.optional-dependencies]
|
|
@@ -49,6 +50,7 @@ dev = [
|
|
|
49
50
|
"mypy>=1.13.0",
|
|
50
51
|
"ruff>=0.8.4",
|
|
51
52
|
"pre-commit>=4.0.1",
|
|
53
|
+
"types-PyYAML>=6.0.0",
|
|
52
54
|
]
|
|
53
55
|
|
|
54
56
|
[project.urls]
|
{markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/src/markdown_table_fixer/_version.py
RENAMED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.1.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
31
|
+
__version__ = version = '0.1.3'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 3)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -27,7 +27,6 @@ from .models import (
|
|
|
27
27
|
OutputFormat,
|
|
28
28
|
PRInfo,
|
|
29
29
|
ScanResult,
|
|
30
|
-
TableViolation,
|
|
31
30
|
)
|
|
32
31
|
from .pr_fixer import PRFixer
|
|
33
32
|
from .pr_scanner import PRScanner
|
|
@@ -172,7 +171,6 @@ def lint(
|
|
|
172
171
|
console.print(f"🔍 Scanning: {scan_path}")
|
|
173
172
|
if should_fix:
|
|
174
173
|
console.print("🔧 Auto-fix enabled")
|
|
175
|
-
console.print()
|
|
176
174
|
|
|
177
175
|
try:
|
|
178
176
|
scanner = MarkdownFileScanner(scan_path)
|
|
@@ -206,6 +204,9 @@ def lint(
|
|
|
206
204
|
if (check or fail_on_error) and scan_result.files_with_issues > 0:
|
|
207
205
|
raise typer.Exit(1)
|
|
208
206
|
|
|
207
|
+
except typer.Exit:
|
|
208
|
+
# Re-raise typer.Exit without catching it as a general exception
|
|
209
|
+
raise
|
|
209
210
|
except Exception as e:
|
|
210
211
|
console.print(f"[red]Error:[/red] {e}")
|
|
211
212
|
raise typer.Exit(1) from e
|
|
@@ -736,36 +737,30 @@ def _output_text_results(result: ScanResult, quiet: bool) -> None:
|
|
|
736
737
|
console.print(f" ... and {len(result.errors) - 5} more errors")
|
|
737
738
|
console.print()
|
|
738
739
|
|
|
739
|
-
# Show
|
|
740
|
-
if result.files_with_issues > 0:
|
|
740
|
+
# Show all files with violations or errors
|
|
741
|
+
if result.files_with_issues > 0 or result.errors:
|
|
741
742
|
console.print("[bold yellow]Files with issues:[/bold yellow]")
|
|
742
|
-
for file_result in result.file_results[:3]:
|
|
743
|
-
if file_result.has_violations:
|
|
744
|
-
console.print(f"{file_result.file_path}")
|
|
745
|
-
|
|
746
|
-
# Group violations by table
|
|
747
|
-
violations_by_table: dict[int, list[TableViolation]] = {}
|
|
748
|
-
for violation in file_result.violations:
|
|
749
|
-
table_line = violation.table_start_line
|
|
750
|
-
if table_line not in violations_by_table:
|
|
751
|
-
violations_by_table[table_line] = []
|
|
752
|
-
violations_by_table[table_line].append(violation)
|
|
753
|
-
|
|
754
|
-
# Show summary per table
|
|
755
|
-
for table_line in sorted(violations_by_table.keys()):
|
|
756
|
-
violations = violations_by_table[table_line]
|
|
757
|
-
# Count unique rows with violations
|
|
758
|
-
unique_rows = len({v.line_number for v in violations})
|
|
759
|
-
console.print(
|
|
760
|
-
f" Markdown table at line {table_line} has {unique_rows} "
|
|
761
|
-
f"row(s) with formatting issues"
|
|
762
|
-
)
|
|
763
|
-
console.print()
|
|
764
743
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
744
|
+
# Collect files with issues (violations or errors)
|
|
745
|
+
files_to_show = []
|
|
746
|
+
for file_result in result.file_results:
|
|
747
|
+
if file_result.has_violations or file_result.error:
|
|
748
|
+
files_to_show.append(file_result)
|
|
749
|
+
|
|
750
|
+
# Display each file with its issue count
|
|
751
|
+
for file_result in files_to_show:
|
|
752
|
+
if file_result.has_violations:
|
|
753
|
+
# Count total violations for this file
|
|
754
|
+
violation_count = len(file_result.violations)
|
|
755
|
+
error_word = "Error" if violation_count == 1 else "Errors"
|
|
756
|
+
console.print(
|
|
757
|
+
f" {file_result.file_path} [{violation_count} {error_word}]"
|
|
758
|
+
)
|
|
759
|
+
elif file_result.error:
|
|
760
|
+
# File has a parsing or processing error
|
|
761
|
+
console.print(
|
|
762
|
+
f" {file_result.file_path} [Parse Error: {file_result.error}]"
|
|
763
|
+
)
|
|
769
764
|
|
|
770
765
|
|
|
771
766
|
if __name__ == "__main__":
|
{markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/src/markdown_table_fixer/models.py
RENAMED
|
@@ -7,6 +7,7 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
from dataclasses import dataclass, field
|
|
9
9
|
from enum import Enum
|
|
10
|
+
import html
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
from typing import Any
|
|
12
13
|
|
|
@@ -47,12 +48,19 @@ class TableCell:
|
|
|
47
48
|
|
|
48
49
|
Uses wcwidth to properly calculate display width for Unicode characters,
|
|
49
50
|
including emojis which display as 2 characters wide but count as 1 in len().
|
|
51
|
+
Also handles HTML entities (e.g., | for |, 😀 for 😀).
|
|
50
52
|
"""
|
|
51
53
|
stripped = self.content.strip()
|
|
52
|
-
|
|
54
|
+
|
|
55
|
+
# Decode HTML entities to their actual characters
|
|
56
|
+
# This properly handles both named entities (<) and numeric entities (|, 😀)
|
|
57
|
+
# so that wide characters like emojis get their correct display width
|
|
58
|
+
decoded = html.unescape(stripped)
|
|
59
|
+
|
|
60
|
+
width = wcwidth.wcswidth(decoded)
|
|
53
61
|
# wcswidth returns -1 if string contains non-printable characters
|
|
54
62
|
# Fall back to len() in that case
|
|
55
|
-
return width if width >= 0 else len(
|
|
63
|
+
return width if width >= 0 else len(decoded)
|
|
56
64
|
|
|
57
65
|
|
|
58
66
|
@dataclass
|
{markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/src/markdown_table_fixer/table_fixer.py
RENAMED
|
@@ -5,8 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
8
10
|
from typing import TYPE_CHECKING
|
|
9
11
|
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
10
14
|
if TYPE_CHECKING:
|
|
11
15
|
from pathlib import Path
|
|
12
16
|
|
|
@@ -86,8 +90,11 @@ class TableFixer:
|
|
|
86
90
|
def _calculate_column_widths(self) -> list[int]:
|
|
87
91
|
"""Calculate the maximum width needed for each column.
|
|
88
92
|
|
|
93
|
+
For MD060 compliance, we need to align pipes by character position.
|
|
94
|
+
This uses string length, not display width or byte length.
|
|
95
|
+
|
|
89
96
|
Returns:
|
|
90
|
-
List of column widths
|
|
97
|
+
List of column widths (in characters)
|
|
91
98
|
"""
|
|
92
99
|
if not self.table.rows:
|
|
93
100
|
return []
|
|
@@ -105,6 +112,7 @@ class TableFixer:
|
|
|
105
112
|
if row.is_separator:
|
|
106
113
|
content_width = 3 # Minimum "---"
|
|
107
114
|
else:
|
|
115
|
+
# Use character length for MD060 compliance (pipe alignment)
|
|
108
116
|
content_width = len(cell.content.strip())
|
|
109
117
|
max_width = max(max_width, content_width)
|
|
110
118
|
widths.append(max_width)
|
|
@@ -116,7 +124,7 @@ class TableFixer:
|
|
|
116
124
|
|
|
117
125
|
Args:
|
|
118
126
|
row: The row to format
|
|
119
|
-
column_widths: Width of each column
|
|
127
|
+
column_widths: Width of each column (in characters)
|
|
120
128
|
|
|
121
129
|
Returns:
|
|
122
130
|
Formatted row string
|
|
@@ -129,7 +137,8 @@ class TableFixer:
|
|
|
129
137
|
column_widths[idx] if idx < len(column_widths) else len(content)
|
|
130
138
|
)
|
|
131
139
|
|
|
132
|
-
# Pad content to column width
|
|
140
|
+
# Pad content to column width based on character length
|
|
141
|
+
# This ensures pipes align by character position (MD060 compliance)
|
|
133
142
|
padded = content.ljust(width)
|
|
134
143
|
parts.append(f" {padded} ")
|
|
135
144
|
|
|
@@ -142,7 +151,7 @@ class TableFixer:
|
|
|
142
151
|
|
|
143
152
|
Args:
|
|
144
153
|
row: The separator row to format
|
|
145
|
-
column_widths: Width of each column
|
|
154
|
+
column_widths: Width of each column (in characters)
|
|
146
155
|
|
|
147
156
|
Returns:
|
|
148
157
|
Formatted separator row string
|
|
@@ -188,6 +197,202 @@ class FileFixer:
|
|
|
188
197
|
"""
|
|
189
198
|
self.file_path = file_path
|
|
190
199
|
self.max_line_length = max_line_length
|
|
200
|
+
self._md013_enabled = self._check_md013_enabled()
|
|
201
|
+
self._md060_enabled = self._check_md060_enabled()
|
|
202
|
+
|
|
203
|
+
def _remove_jsonc_comments(self, content: str) -> str:
|
|
204
|
+
"""Remove comments from JSONC content.
|
|
205
|
+
|
|
206
|
+
This is a simple implementation that removes // comments while being
|
|
207
|
+
aware of strings. It's not perfect but handles most common cases.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
content: JSONC content string
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Content with comments removed
|
|
214
|
+
"""
|
|
215
|
+
lines = []
|
|
216
|
+
for line in content.split("\n"):
|
|
217
|
+
# Track if we're inside a string
|
|
218
|
+
in_string = False
|
|
219
|
+
escape_next = False
|
|
220
|
+
processed = []
|
|
221
|
+
|
|
222
|
+
i = 0
|
|
223
|
+
while i < len(line):
|
|
224
|
+
char = line[i]
|
|
225
|
+
|
|
226
|
+
if escape_next:
|
|
227
|
+
processed.append(char)
|
|
228
|
+
escape_next = False
|
|
229
|
+
i += 1
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
if char == "\\":
|
|
233
|
+
processed.append(char)
|
|
234
|
+
escape_next = True
|
|
235
|
+
i += 1
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
if char == '"':
|
|
239
|
+
in_string = not in_string
|
|
240
|
+
processed.append(char)
|
|
241
|
+
i += 1
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
# Check for comment start (only outside strings)
|
|
245
|
+
if (
|
|
246
|
+
not in_string
|
|
247
|
+
and i < len(line) - 1
|
|
248
|
+
and line[i : i + 2] == "//"
|
|
249
|
+
):
|
|
250
|
+
# Rest of line is a comment
|
|
251
|
+
break
|
|
252
|
+
|
|
253
|
+
processed.append(char)
|
|
254
|
+
i += 1
|
|
255
|
+
|
|
256
|
+
lines.append("".join(processed))
|
|
257
|
+
|
|
258
|
+
return "\n".join(lines)
|
|
259
|
+
|
|
260
|
+
def _check_rule_enabled(self, rule_name: str) -> bool:
|
|
261
|
+
"""Check if a markdownlint rule is enabled in config.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
rule_name: Name of the rule (e.g., "MD013", "MD060")
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
True if rule checking is enabled, False otherwise
|
|
268
|
+
"""
|
|
269
|
+
# Look for markdownlint config files in parent directories
|
|
270
|
+
current_dir = self.file_path.parent
|
|
271
|
+
config_names = [
|
|
272
|
+
".markdownlint.json",
|
|
273
|
+
".markdownlint.jsonc",
|
|
274
|
+
".markdownlint.yaml",
|
|
275
|
+
".markdownlint.yml",
|
|
276
|
+
".markdownlintrc",
|
|
277
|
+
]
|
|
278
|
+
|
|
279
|
+
# Search up to 5 levels up
|
|
280
|
+
for _ in range(5):
|
|
281
|
+
for config_name in config_names:
|
|
282
|
+
config_path = current_dir / config_name
|
|
283
|
+
if config_path.exists():
|
|
284
|
+
try:
|
|
285
|
+
with open(config_path, encoding="utf-8") as f:
|
|
286
|
+
if config_name.endswith((".yaml", ".yml")):
|
|
287
|
+
config = yaml.safe_load(f)
|
|
288
|
+
# yaml.safe_load returns None for empty files
|
|
289
|
+
if config is None:
|
|
290
|
+
config = {}
|
|
291
|
+
elif config_name.endswith(
|
|
292
|
+
(".json", ".jsonc", "rc")
|
|
293
|
+
):
|
|
294
|
+
content = f.read()
|
|
295
|
+
if config_name.endswith(".jsonc"):
|
|
296
|
+
content = self._remove_jsonc_comments(
|
|
297
|
+
content
|
|
298
|
+
)
|
|
299
|
+
config = json.loads(content)
|
|
300
|
+
else:
|
|
301
|
+
continue
|
|
302
|
+
|
|
303
|
+
# Check if rule is explicitly disabled
|
|
304
|
+
if rule_name in config:
|
|
305
|
+
return config[rule_name] is not False
|
|
306
|
+
# If not specified, assume enabled (markdownlint default)
|
|
307
|
+
return True
|
|
308
|
+
except (json.JSONDecodeError, yaml.YAMLError, OSError):
|
|
309
|
+
# If config can't be read, assume rule is enabled
|
|
310
|
+
pass
|
|
311
|
+
|
|
312
|
+
# Move up one directory
|
|
313
|
+
if current_dir.parent == current_dir:
|
|
314
|
+
break # Reached root
|
|
315
|
+
current_dir = current_dir.parent
|
|
316
|
+
|
|
317
|
+
# No config found, assume rule is enabled by default
|
|
318
|
+
return True
|
|
319
|
+
|
|
320
|
+
def _check_md013_enabled(self) -> bool:
|
|
321
|
+
"""Check if MD013 is enabled in markdownlint config.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
True if MD013 checking is enabled, False otherwise
|
|
325
|
+
"""
|
|
326
|
+
return self._check_rule_enabled("MD013")
|
|
327
|
+
|
|
328
|
+
def _check_md060_enabled(self) -> bool:
|
|
329
|
+
"""Check if MD060 is enabled in markdownlint config.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
True if MD060 checking is enabled, False otherwise
|
|
333
|
+
"""
|
|
334
|
+
return self._check_rule_enabled("MD060")
|
|
335
|
+
|
|
336
|
+
def _table_has_emojis(self, table: MarkdownTable) -> bool:
|
|
337
|
+
"""Check if table contains emoji characters.
|
|
338
|
+
|
|
339
|
+
Emojis cause alignment issues because they take 1 character position
|
|
340
|
+
but display as 2 characters wide, making MD060 compliance impossible.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
table: The table to check
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
True if table contains emojis
|
|
347
|
+
"""
|
|
348
|
+
# Emoji pattern - matches most common emoji ranges
|
|
349
|
+
emoji_pattern = re.compile(
|
|
350
|
+
"["
|
|
351
|
+
"\U0001f600-\U0001f64f" # emoticons
|
|
352
|
+
"\U0001f300-\U0001f5ff" # symbols & pictographs
|
|
353
|
+
"\U0001f680-\U0001f6ff" # transport & map symbols
|
|
354
|
+
"\U0001f1e0-\U0001f1ff" # flags (iOS)
|
|
355
|
+
"\U00002702-\U000027b0" # dingbats
|
|
356
|
+
"\U000024c2-\U0001f251" # enclosed characters
|
|
357
|
+
"\U0001f900-\U0001f9ff" # supplemental symbols
|
|
358
|
+
"\U0001fa00-\U0001faff" # extended pictographs
|
|
359
|
+
"]+",
|
|
360
|
+
flags=re.UNICODE,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
for row in table.rows:
|
|
364
|
+
for cell in row.cells:
|
|
365
|
+
if emoji_pattern.search(cell.content):
|
|
366
|
+
return True
|
|
367
|
+
return False
|
|
368
|
+
|
|
369
|
+
def _parse_markdownlint_comment(
|
|
370
|
+
self, line: str, comment_type: str
|
|
371
|
+
) -> set[str]:
|
|
372
|
+
"""Parse markdownlint comment to extract rule names.
|
|
373
|
+
|
|
374
|
+
Args:
|
|
375
|
+
line: The line to parse
|
|
376
|
+
comment_type: Either "disable" or "enable"
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
Set of rule names found in the comment (e.g., {"MD013", "MD060"})
|
|
380
|
+
"""
|
|
381
|
+
# Match markdownlint comments with word boundaries for rule names
|
|
382
|
+
# Examples:
|
|
383
|
+
# <!-- markdownlint-disable MD013 MD060 -->
|
|
384
|
+
# <!-- markdownlint-enable MD013 -->
|
|
385
|
+
pattern = rf"<!--\s*markdownlint-{comment_type}\s+(.*?)\s*-->"
|
|
386
|
+
match = re.search(pattern, line)
|
|
387
|
+
if not match:
|
|
388
|
+
return set()
|
|
389
|
+
|
|
390
|
+
# Extract the rules part and split by whitespace
|
|
391
|
+
rules_text = match.group(1)
|
|
392
|
+
# Match MD followed by digits, using word boundaries to avoid false matches
|
|
393
|
+
rule_pattern = r"\bMD\d+\b"
|
|
394
|
+
rules = re.findall(rule_pattern, rules_text)
|
|
395
|
+
return set(rules)
|
|
191
396
|
|
|
192
397
|
def fix_file(
|
|
193
398
|
self, tables: list[MarkdownTable], dry_run: bool = False
|
|
@@ -232,7 +437,10 @@ class FileFixer:
|
|
|
232
437
|
# Build a map of all tables that need MD013 comments
|
|
233
438
|
tables_needing_md013: dict[int, tuple[MarkdownTable, list[str]]] = {}
|
|
234
439
|
|
|
235
|
-
#
|
|
440
|
+
# Build a map of tables needing MD060 comments (tables with emojis)
|
|
441
|
+
tables_needing_md060: dict[int, tuple[MarkdownTable, list[str]]] = {}
|
|
442
|
+
|
|
443
|
+
# Check all tables for line length violations and emoji issues
|
|
236
444
|
for table in all_tables:
|
|
237
445
|
# Get the table content (either fixed or original)
|
|
238
446
|
table_lines = []
|
|
@@ -261,24 +469,50 @@ class FileFixer:
|
|
|
261
469
|
if needs_md013:
|
|
262
470
|
tables_needing_md013[table.start_line] = (table, table_lines)
|
|
263
471
|
|
|
472
|
+
# Check if table has emojis (causes MD060 violations)
|
|
473
|
+
if self._md060_enabled and self._table_has_emojis(table):
|
|
474
|
+
tables_needing_md060[table.start_line] = (table, table_lines)
|
|
475
|
+
|
|
264
476
|
# Create a unified list of all table modifications to apply
|
|
265
|
-
# This includes
|
|
266
|
-
all_modifications: list[tuple[int, int, list[str], bool]] = []
|
|
477
|
+
# This includes fixes, MD013-only tables, and MD060-only tables
|
|
478
|
+
all_modifications: list[tuple[int, int, list[str], bool, bool]] = []
|
|
267
479
|
|
|
268
480
|
# Add fixes to modifications list
|
|
269
481
|
for fix in fixes:
|
|
270
482
|
fixed_lines = fix.fixed_content.split("\n")
|
|
271
483
|
needs_md013 = fix.start_line in tables_needing_md013
|
|
484
|
+
needs_md060 = fix.start_line in tables_needing_md060
|
|
272
485
|
all_modifications.append(
|
|
273
|
-
(
|
|
486
|
+
(
|
|
487
|
+
fix.start_line,
|
|
488
|
+
fix.end_line,
|
|
489
|
+
fixed_lines,
|
|
490
|
+
needs_md013,
|
|
491
|
+
needs_md060,
|
|
492
|
+
)
|
|
274
493
|
)
|
|
275
494
|
|
|
276
495
|
# Add tables that need MD013 but have no fixes
|
|
277
496
|
for start_line, (table, table_lines) in tables_needing_md013.items():
|
|
278
497
|
# Check if already in fixes
|
|
279
498
|
if not any(mod[0] == start_line for mod in all_modifications):
|
|
499
|
+
needs_md060 = start_line in tables_needing_md060
|
|
280
500
|
all_modifications.append(
|
|
281
|
-
(
|
|
501
|
+
(
|
|
502
|
+
table.start_line,
|
|
503
|
+
table.end_line,
|
|
504
|
+
table_lines,
|
|
505
|
+
True,
|
|
506
|
+
needs_md060,
|
|
507
|
+
)
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
# Add tables that need MD060 but have no fixes or MD013
|
|
511
|
+
for start_line, (table, table_lines) in tables_needing_md060.items():
|
|
512
|
+
# Check if already in fixes or MD013 list
|
|
513
|
+
if not any(mod[0] == start_line for mod in all_modifications):
|
|
514
|
+
all_modifications.append(
|
|
515
|
+
(table.start_line, table.end_line, table_lines, False, True)
|
|
282
516
|
)
|
|
283
517
|
|
|
284
518
|
# Apply all modifications in reverse order to maintain line numbers
|
|
@@ -286,22 +520,37 @@ class FileFixer:
|
|
|
286
520
|
if not all_modifications:
|
|
287
521
|
return
|
|
288
522
|
|
|
289
|
-
for
|
|
290
|
-
|
|
291
|
-
|
|
523
|
+
for (
|
|
524
|
+
start_line,
|
|
525
|
+
end_line,
|
|
526
|
+
content_lines,
|
|
527
|
+
needs_md013,
|
|
528
|
+
needs_md060,
|
|
529
|
+
) in sorted(all_modifications, key=lambda x: x[0], reverse=True):
|
|
292
530
|
start_idx = start_line - 1
|
|
293
531
|
end_idx = end_line
|
|
294
532
|
|
|
295
533
|
# Prepare the lines to insert
|
|
296
534
|
new_lines = [line + "\n" for line in content_lines]
|
|
297
535
|
|
|
298
|
-
#
|
|
299
|
-
|
|
536
|
+
# Collect all disable/enable rules needed
|
|
537
|
+
disable_rules = []
|
|
538
|
+
if needs_md013 and self._md013_enabled:
|
|
539
|
+
disable_rules.append("MD013")
|
|
540
|
+
if needs_md060 and self._md060_enabled:
|
|
541
|
+
disable_rules.append("MD060")
|
|
542
|
+
|
|
543
|
+
# Add markdownlint comments if needed
|
|
544
|
+
if disable_rules:
|
|
545
|
+
disable_comment = " ".join(disable_rules)
|
|
300
546
|
# Check if disable comment already exists within 3 lines before the table
|
|
301
547
|
has_disable = False
|
|
302
548
|
check_start = max(0, start_idx - 3)
|
|
303
549
|
for i in range(check_start, start_idx):
|
|
304
|
-
|
|
550
|
+
found_rules = self._parse_markdownlint_comment(
|
|
551
|
+
lines[i], "disable"
|
|
552
|
+
)
|
|
553
|
+
if all(rule in found_rules for rule in disable_rules):
|
|
305
554
|
has_disable = True
|
|
306
555
|
break
|
|
307
556
|
|
|
@@ -309,7 +558,10 @@ class FileFixer:
|
|
|
309
558
|
has_enable = False
|
|
310
559
|
check_end = min(len(lines), end_idx + 3)
|
|
311
560
|
for i in range(end_idx, check_end):
|
|
312
|
-
|
|
561
|
+
found_rules = self._parse_markdownlint_comment(
|
|
562
|
+
lines[i], "enable"
|
|
563
|
+
)
|
|
564
|
+
if all(rule in found_rules for rule in disable_rules):
|
|
313
565
|
has_enable = True
|
|
314
566
|
break
|
|
315
567
|
|
|
@@ -320,13 +572,15 @@ class FileFixer:
|
|
|
320
572
|
# No blank line, add both blank line and comment
|
|
321
573
|
new_lines.insert(0, "\n")
|
|
322
574
|
new_lines.insert(
|
|
323
|
-
1,
|
|
575
|
+
1,
|
|
576
|
+
f"<!-- markdownlint-disable {disable_comment} -->\n",
|
|
324
577
|
)
|
|
325
578
|
new_lines.insert(2, "\n")
|
|
326
579
|
else:
|
|
327
580
|
# Blank line exists, just add comment
|
|
328
581
|
new_lines.insert(
|
|
329
|
-
0,
|
|
582
|
+
0,
|
|
583
|
+
f"<!-- markdownlint-disable {disable_comment} -->\n",
|
|
330
584
|
)
|
|
331
585
|
new_lines.insert(1, "\n")
|
|
332
586
|
|
|
@@ -334,7 +588,9 @@ class FileFixer:
|
|
|
334
588
|
if not has_enable:
|
|
335
589
|
# Always add blank line and enable comment
|
|
336
590
|
new_lines.append("\n")
|
|
337
|
-
new_lines.append(
|
|
591
|
+
new_lines.append(
|
|
592
|
+
f"<!-- markdownlint-enable {disable_comment} -->\n"
|
|
593
|
+
)
|
|
338
594
|
|
|
339
595
|
# Replace the section
|
|
340
596
|
lines[start_idx:end_idx] = new_lines
|
{markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/src/markdown_table_fixer/table_parser.py
RENAMED
|
@@ -170,9 +170,9 @@ class TableParser:
|
|
|
170
170
|
if stripped.endswith("|"):
|
|
171
171
|
stripped = stripped[:-1]
|
|
172
172
|
|
|
173
|
-
# Split by pipe to get cells
|
|
173
|
+
# Split by pipe to get cells, but handle escaped pipes and HTML entities
|
|
174
174
|
# We need to track column positions for alignment checking
|
|
175
|
-
parts =
|
|
175
|
+
parts = self._split_by_unescaped_pipes(stripped)
|
|
176
176
|
|
|
177
177
|
current_col = 1 # Start after first pipe
|
|
178
178
|
for part in parts:
|
|
@@ -189,6 +189,49 @@ class TableParser:
|
|
|
189
189
|
|
|
190
190
|
return cells
|
|
191
191
|
|
|
192
|
+
def _split_by_unescaped_pipes(self, text: str) -> list[str]:
|
|
193
|
+
"""Split text by pipes, but not escaped pipes or HTML entities.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
text: Text to split
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
List of cell contents
|
|
200
|
+
"""
|
|
201
|
+
parts = []
|
|
202
|
+
current = []
|
|
203
|
+
i = 0
|
|
204
|
+
|
|
205
|
+
while i < len(text):
|
|
206
|
+
# Check for backslash-escaped pipe
|
|
207
|
+
if i < len(text) - 1 and text[i : i + 2] == r"\|":
|
|
208
|
+
# Keep the escaped pipe in the content
|
|
209
|
+
current.append(text[i : i + 2])
|
|
210
|
+
i += 2
|
|
211
|
+
# Check for HTML entity for pipe (|)
|
|
212
|
+
elif i < len(text) - 5 and text[i : i + 6] == "|":
|
|
213
|
+
# Keep the HTML entity in the content
|
|
214
|
+
current.append(text[i : i + 6])
|
|
215
|
+
i += 6
|
|
216
|
+
# Check for unescaped pipe (cell separator)
|
|
217
|
+
elif text[i] == "|":
|
|
218
|
+
# This is a cell separator
|
|
219
|
+
parts.append("".join(current))
|
|
220
|
+
current = []
|
|
221
|
+
i += 1
|
|
222
|
+
else:
|
|
223
|
+
# Regular character
|
|
224
|
+
current.append(text[i])
|
|
225
|
+
i += 1
|
|
226
|
+
|
|
227
|
+
# Add the last cell
|
|
228
|
+
if (
|
|
229
|
+
current or parts
|
|
230
|
+
): # Include empty last cell if there were previous parts
|
|
231
|
+
parts.append("".join(current))
|
|
232
|
+
|
|
233
|
+
return parts
|
|
234
|
+
|
|
192
235
|
|
|
193
236
|
class MarkdownFileScanner:
|
|
194
237
|
"""Scanner for finding markdown files."""
|
|
@@ -188,8 +188,11 @@ class TableValidator:
|
|
|
188
188
|
def _calculate_column_widths(self) -> list[int]:
|
|
189
189
|
"""Calculate the maximum width needed for each column.
|
|
190
190
|
|
|
191
|
+
For MD060 compliance, we calculate widths based on character length.
|
|
192
|
+
This uses string length, not display width or byte length.
|
|
193
|
+
|
|
191
194
|
Returns:
|
|
192
|
-
List of column widths
|
|
195
|
+
List of column widths (in characters)
|
|
193
196
|
"""
|
|
194
197
|
if not self.table.rows:
|
|
195
198
|
return []
|
|
@@ -203,6 +206,7 @@ class TableValidator:
|
|
|
203
206
|
for row in self.table.rows:
|
|
204
207
|
if col_idx < len(row.cells):
|
|
205
208
|
cell = row.cells[col_idx]
|
|
209
|
+
# Use character length for MD060 compliance (pipe alignment)
|
|
206
210
|
content_width = len(cell.content.strip())
|
|
207
211
|
max_width = max(max_width, content_width)
|
|
208
212
|
widths.append(max_width)
|
|
@@ -213,10 +217,10 @@ class TableValidator:
|
|
|
213
217
|
"""Calculate expected positions of pipes based on column widths.
|
|
214
218
|
|
|
215
219
|
Args:
|
|
216
|
-
column_widths: Width of each column
|
|
220
|
+
column_widths: Width of each column (in characters)
|
|
217
221
|
|
|
218
222
|
Returns:
|
|
219
|
-
List of pipe positions
|
|
223
|
+
List of pipe positions (in characters)
|
|
220
224
|
"""
|
|
221
225
|
positions: list[int] = [0] # Starting pipe at position 0
|
|
222
226
|
current_pos = 0
|
|
@@ -237,13 +241,22 @@ class TableValidator:
|
|
|
237
241
|
row: The row to analyze
|
|
238
242
|
|
|
239
243
|
Returns:
|
|
240
|
-
List of pipe positions
|
|
244
|
+
List of pipe positions (in characters)
|
|
241
245
|
"""
|
|
242
246
|
positions: list[int] = []
|
|
243
247
|
line = row.raw_line
|
|
244
248
|
|
|
245
249
|
for idx, char in enumerate(line):
|
|
246
250
|
if char == "|":
|
|
251
|
+
# Skip escaped pipes (preceded by an odd number of backslashes)
|
|
252
|
+
backslash_count = 0
|
|
253
|
+
j = idx - 1
|
|
254
|
+
while j >= 0 and line[j] == "\\":
|
|
255
|
+
backslash_count += 1
|
|
256
|
+
j -= 1
|
|
257
|
+
if backslash_count % 2 == 1:
|
|
258
|
+
# Odd number of backslashes means the pipe is escaped
|
|
259
|
+
continue
|
|
247
260
|
positions.append(idx)
|
|
248
261
|
|
|
249
262
|
return positions
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/src/markdown_table_fixer/__init__.py
RENAMED
|
File without changes
|
{markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/src/markdown_table_fixer/exceptions.py
RENAMED
|
File without changes
|
{markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/src/markdown_table_fixer/git_config.py
RENAMED
|
File without changes
|
{markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/src/markdown_table_fixer/github_client.py
RENAMED
|
File without changes
|
|
File without changes
|
{markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/src/markdown_table_fixer/pr_fixer.py
RENAMED
|
File without changes
|
{markdown_table_fixer-0.1.2 → markdown_table_fixer-0.1.3}/src/markdown_table_fixer/pr_scanner.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|