rolfedh-doc-utils 0.1.30__tar.gz → 0.1.32__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.
- {rolfedh_doc_utils-0.1.30/rolfedh_doc_utils.egg-info → rolfedh_doc_utils-0.1.32}/PKG-INFO +1 -1
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/callout_lib/converter_bullets.py +10 -2
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/callout_lib/converter_deflist.py +10 -2
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/callout_lib/detector.py +38 -8
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/callout_lib/table_parser.py +170 -55
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/convert_callouts_interactive.py +26 -3
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/convert_callouts_to_deflist.py +145 -14
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/doc_utils/version.py +1 -1
- rolfedh_doc_utils-0.1.32/doc_utils/warnings_report.py +237 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/pyproject.toml +1 -1
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32/rolfedh_doc_utils.egg-info}/PKG-INFO +1 -1
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/rolfedh_doc_utils.egg-info/SOURCES.txt +1 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/LICENSE +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/README.md +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/archive_unused_files.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/archive_unused_images.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/callout_lib/__init__.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/callout_lib/converter_comments.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/check_scannability.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/doc_utils/__init__.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/doc_utils/extract_link_attributes.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/doc_utils/file_utils.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/doc_utils/format_asciidoc_spacing.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/doc_utils/replace_link_attributes.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/doc_utils/scannability.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/doc_utils/spinner.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/doc_utils/topic_map_parser.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/doc_utils/unused_adoc.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/doc_utils/unused_attributes.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/doc_utils/unused_images.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/doc_utils/validate_links.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/doc_utils/version_check.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/doc_utils_cli.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/extract_link_attributes.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/find_unused_attributes.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/format_asciidoc_spacing.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/replace_link_attributes.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/rolfedh_doc_utils.egg-info/dependency_links.txt +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/rolfedh_doc_utils.egg-info/entry_points.txt +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/rolfedh_doc_utils.egg-info/requires.txt +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/rolfedh_doc_utils.egg-info/top_level.txt +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/setup.cfg +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/setup.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/tests/test_archive_unused_files.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/tests/test_archive_unused_images.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/tests/test_auto_discovery.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/tests/test_check_scannability.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/tests/test_cli_entry_points.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/tests/test_extract_link_attributes.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/tests/test_file_utils.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/tests/test_fixture_archive_unused_files.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/tests/test_fixture_archive_unused_images.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/tests/test_fixture_check_scannability.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/tests/test_parse_exclude_list.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/tests/test_replace_link_attributes.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/tests/test_symlink_handling.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/tests/test_table_callout_conversion.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/tests/test_table_parser.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/tests/test_topic_map_parser.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/tests/test_unused_attributes.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/tests/test_validate_links.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/tests/test_version_check.py +0 -0
- {rolfedh_doc_utils-0.1.30 → rolfedh_doc_utils-0.1.32}/validate_links.py +0 -0
|
@@ -16,7 +16,7 @@ class BulletListConverter:
|
|
|
16
16
|
USER_VALUE_PATTERN = re.compile(r'(?<!<)<([a-zA-Z][^>]*)>')
|
|
17
17
|
|
|
18
18
|
@staticmethod
|
|
19
|
-
def convert(callout_groups: List[CalloutGroup], explanations: Dict[int, Callout]) -> List[str]:
|
|
19
|
+
def convert(callout_groups: List[CalloutGroup], explanations: Dict[int, Callout], table_title: str = "") -> List[str]:
|
|
20
20
|
"""
|
|
21
21
|
Create bulleted list from callout groups and explanations.
|
|
22
22
|
|
|
@@ -33,12 +33,20 @@ class BulletListConverter:
|
|
|
33
33
|
|
|
34
34
|
Args:
|
|
35
35
|
callout_groups: List of CalloutGroup objects from code block
|
|
36
|
+
table_title: Optional table title (e.g., ".Descriptions of delete event")
|
|
37
|
+
Will be converted to lead-in sentence
|
|
36
38
|
explanations: Dict mapping callout numbers to Callout objects
|
|
37
39
|
|
|
38
40
|
Returns:
|
|
39
41
|
List of strings representing the bulleted list
|
|
40
42
|
"""
|
|
41
|
-
|
|
43
|
+
# Convert table title to lead-in sentence if present
|
|
44
|
+
if table_title:
|
|
45
|
+
# Remove leading dot and trailing period if present
|
|
46
|
+
title_text = table_title.lstrip('.').rstrip('.')
|
|
47
|
+
lines = [f'\n{title_text}:'] # Use colon for bulleted list lead-in
|
|
48
|
+
else:
|
|
49
|
+
lines = [''] # Start with blank line before list
|
|
42
50
|
|
|
43
51
|
# Process each group (which may contain one or more callouts)
|
|
44
52
|
for group in callout_groups:
|
|
@@ -16,7 +16,7 @@ class DefListConverter:
|
|
|
16
16
|
USER_VALUE_PATTERN = re.compile(r'(?<!<)<([a-zA-Z][^>]*)>')
|
|
17
17
|
|
|
18
18
|
@staticmethod
|
|
19
|
-
def convert(callout_groups: List[CalloutGroup], explanations: Dict[int, Callout]) -> List[str]:
|
|
19
|
+
def convert(callout_groups: List[CalloutGroup], explanations: Dict[int, Callout], table_title: str = "") -> List[str]:
|
|
20
20
|
"""
|
|
21
21
|
Create definition list from callout groups and explanations.
|
|
22
22
|
|
|
@@ -29,11 +29,19 @@ class DefListConverter:
|
|
|
29
29
|
Args:
|
|
30
30
|
callout_groups: List of CalloutGroup objects from code block
|
|
31
31
|
explanations: Dict mapping callout numbers to Callout objects
|
|
32
|
+
table_title: Optional table title (e.g., ".Descriptions of delete event")
|
|
33
|
+
Will be converted to lead-in sentence (e.g., "Descriptions of delete event, where:")
|
|
32
34
|
|
|
33
35
|
Returns:
|
|
34
36
|
List of strings representing the definition list
|
|
35
37
|
"""
|
|
36
|
-
|
|
38
|
+
# Convert table title to lead-in sentence if present
|
|
39
|
+
if table_title:
|
|
40
|
+
# Remove leading dot and trailing period if present
|
|
41
|
+
title_text = table_title.lstrip('.').rstrip('.')
|
|
42
|
+
lines = [f'\n{title_text}, where:']
|
|
43
|
+
else:
|
|
44
|
+
lines = ['\nwhere:']
|
|
37
45
|
|
|
38
46
|
# Process each group (which may contain one or more callouts)
|
|
39
47
|
for group in callout_groups:
|
|
@@ -60,6 +60,8 @@ class CalloutDetector:
|
|
|
60
60
|
def __init__(self):
|
|
61
61
|
"""Initialize detector with table parser."""
|
|
62
62
|
self.table_parser = TableParser()
|
|
63
|
+
self.last_table_title = "" # Track title from most recent table extraction
|
|
64
|
+
self.last_table = None # Track last table found for validation diagnostics
|
|
63
65
|
|
|
64
66
|
def find_code_blocks(self, lines: List[str]) -> List[CodeBlock]:
|
|
65
67
|
"""Find all code blocks in the document."""
|
|
@@ -181,6 +183,10 @@ class CalloutDetector:
|
|
|
181
183
|
|
|
182
184
|
def _extract_from_table(self, table) -> Tuple[Dict[int, Callout], int]:
|
|
183
185
|
"""Extract callout explanations from a table format."""
|
|
186
|
+
# Store table for use by converters and validation
|
|
187
|
+
self.last_table = table
|
|
188
|
+
self.last_table_title = table.title if hasattr(table, 'title') else ""
|
|
189
|
+
|
|
184
190
|
explanations = {}
|
|
185
191
|
table_data = self.table_parser.extract_callout_explanations_from_table(table)
|
|
186
192
|
|
|
@@ -215,6 +221,10 @@ class CalloutDetector:
|
|
|
215
221
|
Extract callout explanations from a 3-column table format.
|
|
216
222
|
Format: Item (number) | Value | Description
|
|
217
223
|
"""
|
|
224
|
+
# Store table for use by converters and validation
|
|
225
|
+
self.last_table = table
|
|
226
|
+
self.last_table_title = table.title if hasattr(table, 'title') else ""
|
|
227
|
+
|
|
218
228
|
explanations = {}
|
|
219
229
|
table_data = self.table_parser.extract_3column_callout_explanations(table)
|
|
220
230
|
|
|
@@ -256,6 +266,10 @@ class CalloutDetector:
|
|
|
256
266
|
|
|
257
267
|
def _extract_from_list(self, lines: List[str], start_line: int) -> Tuple[Dict[int, Callout], int]:
|
|
258
268
|
"""Extract callout explanations from list format (<1> text)."""
|
|
269
|
+
# Clear table data since list format doesn't have tables
|
|
270
|
+
self.last_table = None
|
|
271
|
+
self.last_table_title = ""
|
|
272
|
+
|
|
259
273
|
explanations = {}
|
|
260
274
|
i = start_line + 1 # Start after the closing delimiter
|
|
261
275
|
|
|
@@ -310,17 +324,33 @@ class CalloutDetector:
|
|
|
310
324
|
cleaned.append(self.CALLOUT_WITH_COMMENT.sub('', line).rstrip())
|
|
311
325
|
return cleaned
|
|
312
326
|
|
|
313
|
-
def validate_callouts(self, callout_groups: List[CalloutGroup], explanations: Dict[int, Callout]) -> Tuple[bool,
|
|
327
|
+
def validate_callouts(self, callout_groups: List[CalloutGroup], explanations: Dict[int, Callout]) -> Tuple[bool, List[int], List[int]]:
|
|
314
328
|
"""
|
|
315
329
|
Validate that callout numbers in code match explanation numbers.
|
|
316
|
-
Returns tuple of (is_valid,
|
|
330
|
+
Returns tuple of (is_valid, code_nums_list, explanation_nums_list).
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
- is_valid: True if unique callout numbers match
|
|
334
|
+
- code_nums_list: List of callout numbers from code (unique, sorted)
|
|
335
|
+
- explanation_nums_list: List of callout numbers from explanations
|
|
336
|
+
(preserves duplicates if from table, sorted)
|
|
317
337
|
"""
|
|
318
|
-
# Extract
|
|
319
|
-
|
|
338
|
+
# Extract unique callout numbers from code groups
|
|
339
|
+
code_nums_set = set()
|
|
320
340
|
for group in callout_groups:
|
|
321
|
-
|
|
341
|
+
code_nums_set.update(group.callout_numbers)
|
|
342
|
+
|
|
343
|
+
# Get explanation numbers, preserving duplicates if from a table
|
|
344
|
+
if self.last_table:
|
|
345
|
+
# Use table parser to get raw callout numbers (with duplicates)
|
|
346
|
+
explanation_nums_list = self.table_parser.get_table_callout_numbers(self.last_table)
|
|
347
|
+
else:
|
|
348
|
+
# List format: dict keys are already unique
|
|
349
|
+
explanation_nums_list = list(explanations.keys())
|
|
350
|
+
|
|
351
|
+
explanation_nums_set = set(explanation_nums_list)
|
|
322
352
|
|
|
323
|
-
|
|
353
|
+
# Validation compares unique numbers only
|
|
354
|
+
is_valid = code_nums_set == explanation_nums_set
|
|
324
355
|
|
|
325
|
-
is_valid
|
|
326
|
-
return is_valid, code_nums, explanation_nums
|
|
356
|
+
return is_valid, sorted(code_nums_set), sorted(explanation_nums_list)
|
|
@@ -38,6 +38,7 @@ class AsciiDocTable:
|
|
|
38
38
|
end_line: int
|
|
39
39
|
attributes: str # Table attributes like [cols="1,3"]
|
|
40
40
|
rows: List[TableRow]
|
|
41
|
+
title: str = "" # Block title like ".Table description"
|
|
41
42
|
|
|
42
43
|
|
|
43
44
|
class TableParser:
|
|
@@ -47,8 +48,8 @@ class TableParser:
|
|
|
47
48
|
TABLE_START = re.compile(r'^\[.*?\]$')
|
|
48
49
|
TABLE_DELIMITER = re.compile(r'^\|===\s*$')
|
|
49
50
|
|
|
50
|
-
# Pattern for table cell separator
|
|
51
|
-
CELL_SEPARATOR = re.compile(r'
|
|
51
|
+
# Pattern for table cell separator (| or cell type specifier like a|, s|, etc.)
|
|
52
|
+
CELL_SEPARATOR = re.compile(r'^(\||[ashdmev]\|)')
|
|
52
53
|
|
|
53
54
|
# Pattern for conditional directives
|
|
54
55
|
IFDEF_PATTERN = re.compile(r'^(ifdef::|ifndef::).+\[\]\s*$')
|
|
@@ -56,6 +57,26 @@ class TableParser:
|
|
|
56
57
|
|
|
57
58
|
# Pattern for callout number (used for callout table detection)
|
|
58
59
|
CALLOUT_NUMBER = re.compile(r'^<(\d+)>\s*$')
|
|
60
|
+
PLAIN_NUMBER = re.compile(r'^(\d+)\s*$')
|
|
61
|
+
|
|
62
|
+
def _is_callout_or_number(self, text: str) -> tuple[bool, int]:
|
|
63
|
+
"""
|
|
64
|
+
Check if text is a callout number (<1>) or plain number (1).
|
|
65
|
+
Returns (is_match, number) or (False, 0) if no match.
|
|
66
|
+
"""
|
|
67
|
+
text = text.strip()
|
|
68
|
+
|
|
69
|
+
# Try callout format first: <1>
|
|
70
|
+
match = self.CALLOUT_NUMBER.match(text)
|
|
71
|
+
if match:
|
|
72
|
+
return (True, int(match.group(1)))
|
|
73
|
+
|
|
74
|
+
# Try plain number format: 1
|
|
75
|
+
match = self.PLAIN_NUMBER.match(text)
|
|
76
|
+
if match:
|
|
77
|
+
return (True, int(match.group(1)))
|
|
78
|
+
|
|
79
|
+
return (False, 0)
|
|
59
80
|
|
|
60
81
|
def _finalize_row_if_complete(self, current_row_cells, conditionals_before_row,
|
|
61
82
|
conditionals_after_row, expected_columns, rows):
|
|
@@ -103,16 +124,27 @@ class TableParser:
|
|
|
103
124
|
while i < len(lines):
|
|
104
125
|
# Look for table delimiter
|
|
105
126
|
if self.TABLE_DELIMITER.match(lines[i]):
|
|
106
|
-
# Check
|
|
127
|
+
# Check for attributes and title before the table
|
|
107
128
|
attributes = ""
|
|
129
|
+
title = ""
|
|
108
130
|
start_line = i
|
|
109
131
|
|
|
132
|
+
# Check line before delimiter for attributes [cols="..."]
|
|
110
133
|
if i > 0 and self.TABLE_START.match(lines[i - 1]):
|
|
111
134
|
attributes = lines[i - 1]
|
|
112
135
|
start_line = i - 1
|
|
113
136
|
|
|
137
|
+
# Check line before attributes for title .Title
|
|
138
|
+
if i > 1 and lines[i - 2].strip().startswith('.') and not lines[i - 2].strip().startswith('..'):
|
|
139
|
+
title = lines[i - 2].strip()
|
|
140
|
+
start_line = i - 2
|
|
141
|
+
elif i > 0 and lines[i - 1].strip().startswith('.') and not lines[i - 1].strip().startswith('..'):
|
|
142
|
+
# Title directly before delimiter (no attributes)
|
|
143
|
+
title = lines[i - 1].strip()
|
|
144
|
+
start_line = i - 1
|
|
145
|
+
|
|
114
146
|
# Parse table content
|
|
115
|
-
table = self._parse_table(lines, start_line, i)
|
|
147
|
+
table = self._parse_table(lines, start_line, i, title)
|
|
116
148
|
if table:
|
|
117
149
|
tables.append(table)
|
|
118
150
|
i = table.end_line + 1
|
|
@@ -121,11 +153,13 @@ class TableParser:
|
|
|
121
153
|
|
|
122
154
|
return tables
|
|
123
155
|
|
|
124
|
-
def _parse_table(self, lines: List[str], start_line: int, delimiter_line: int) -> Optional[AsciiDocTable]:
|
|
156
|
+
def _parse_table(self, lines: List[str], start_line: int, delimiter_line: int, title: str = "") -> Optional[AsciiDocTable]:
|
|
125
157
|
"""
|
|
126
158
|
Parse a single table starting at the delimiter.
|
|
127
159
|
|
|
128
160
|
AsciiDoc table format:
|
|
161
|
+
.Optional title
|
|
162
|
+
[optional attributes]
|
|
129
163
|
|===
|
|
130
164
|
|Cell1
|
|
131
165
|
|Cell2
|
|
@@ -137,7 +171,15 @@ class TableParser:
|
|
|
137
171
|
# Get attributes and parse column count
|
|
138
172
|
attributes = ""
|
|
139
173
|
if start_line < delimiter_line:
|
|
140
|
-
|
|
174
|
+
# Check if start line is title or attributes
|
|
175
|
+
start_content = lines[start_line].strip()
|
|
176
|
+
if start_content.startswith('.') and not start_content.startswith('..'):
|
|
177
|
+
# Start line is title, attributes might be on next line
|
|
178
|
+
if start_line + 1 < delimiter_line:
|
|
179
|
+
attributes = lines[start_line + 1]
|
|
180
|
+
else:
|
|
181
|
+
# Start line is attributes
|
|
182
|
+
attributes = lines[start_line]
|
|
141
183
|
|
|
142
184
|
expected_columns = self._parse_column_count(attributes)
|
|
143
185
|
|
|
@@ -170,16 +212,13 @@ class TableParser:
|
|
|
170
212
|
conditionals_after=conditionals_after_row.copy()
|
|
171
213
|
))
|
|
172
214
|
|
|
173
|
-
# Get attributes if present
|
|
174
|
-
attributes = ""
|
|
175
|
-
if start_line < delimiter_line:
|
|
176
|
-
attributes = lines[start_line]
|
|
177
|
-
|
|
215
|
+
# Get attributes if present (already extracted above)
|
|
178
216
|
return AsciiDocTable(
|
|
179
217
|
start_line=start_line,
|
|
180
218
|
end_line=i,
|
|
181
219
|
attributes=attributes,
|
|
182
|
-
rows=rows
|
|
220
|
+
rows=rows,
|
|
221
|
+
title=title
|
|
183
222
|
)
|
|
184
223
|
|
|
185
224
|
# Check for conditional directives
|
|
@@ -230,19 +269,15 @@ class TableParser:
|
|
|
230
269
|
i += 1
|
|
231
270
|
continue
|
|
232
271
|
|
|
233
|
-
# Check for cell separator (|)
|
|
272
|
+
# Check for cell separator (|) or cell type specifier (a|, s|, etc.)
|
|
234
273
|
if self.CELL_SEPARATOR.match(line):
|
|
235
|
-
#
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
#
|
|
241
|
-
|
|
242
|
-
in_asciidoc_cell = True
|
|
243
|
-
# Don't create a cell yet - content comes on following lines
|
|
244
|
-
i += 1
|
|
245
|
-
continue
|
|
274
|
+
# Determine if line starts with | or with a cell type specifier
|
|
275
|
+
if line.startswith('|'):
|
|
276
|
+
# Standard cell separator
|
|
277
|
+
cell_content = line[1:] # Remove leading |
|
|
278
|
+
else:
|
|
279
|
+
# Cell type specifier without leading | (e.g., "a|text")
|
|
280
|
+
cell_content = line
|
|
246
281
|
|
|
247
282
|
# Save previous cell if exists
|
|
248
283
|
if current_cell_lines:
|
|
@@ -260,16 +295,13 @@ class TableParser:
|
|
|
260
295
|
conditionals_after_row, expected_columns, rows
|
|
261
296
|
)
|
|
262
297
|
|
|
263
|
-
#
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
# Check for inline cell type specifier (a|text, s|text, etc.)
|
|
267
|
-
# Cell type specifiers are single characters followed by |
|
|
268
|
-
if len(cell_content) > 0 and cell_content[0] in 'ashdmev' and len(cell_content) > 1 and cell_content[1] == '|':
|
|
298
|
+
# Check for cell type specifier (a|, s|, etc.)
|
|
299
|
+
# Type specifiers are single characters followed by |
|
|
300
|
+
if len(cell_content) > 1 and cell_content[0] in 'ashdmev' and cell_content[1] == '|':
|
|
269
301
|
# Track if this is an AsciiDoc cell (a|)
|
|
270
302
|
if cell_content[0] == 'a':
|
|
271
303
|
in_asciidoc_cell = True
|
|
272
|
-
cell_content = cell_content[2:] # Remove type specifier and
|
|
304
|
+
cell_content = cell_content[2:] # Remove type specifier and |
|
|
273
305
|
|
|
274
306
|
cell_content = cell_content.strip()
|
|
275
307
|
|
|
@@ -350,6 +382,8 @@ class TableParser:
|
|
|
350
382
|
"""
|
|
351
383
|
Determine if a table is a callout explanation table.
|
|
352
384
|
A callout table has two columns: callout number and explanation.
|
|
385
|
+
Accepts both callout format (<1>) and plain numbers (1).
|
|
386
|
+
Skips header rows if present.
|
|
353
387
|
"""
|
|
354
388
|
if not table.rows:
|
|
355
389
|
return False
|
|
@@ -358,15 +392,23 @@ class TableParser:
|
|
|
358
392
|
if not all(len(row.cells) == 2 for row in table.rows):
|
|
359
393
|
return False
|
|
360
394
|
|
|
361
|
-
#
|
|
362
|
-
|
|
395
|
+
# Determine if there's a header row and skip it
|
|
396
|
+
has_header = self._has_header_row(table)
|
|
397
|
+
data_rows = table.rows[1:] if has_header else table.rows
|
|
398
|
+
|
|
399
|
+
if not data_rows:
|
|
400
|
+
return False
|
|
401
|
+
|
|
402
|
+
# Check if first cell of each data row is a callout number (either <1> or 1)
|
|
403
|
+
for row in data_rows:
|
|
363
404
|
first_cell = row.cells[0]
|
|
364
405
|
if not first_cell.content:
|
|
365
406
|
return False
|
|
366
407
|
|
|
367
|
-
# First line of first cell should be a callout number
|
|
408
|
+
# First line of first cell should be a callout number or plain number
|
|
368
409
|
first_line = first_cell.content[0].strip()
|
|
369
|
-
|
|
410
|
+
is_match, _ = self._is_callout_or_number(first_line)
|
|
411
|
+
if not is_match:
|
|
370
412
|
return False
|
|
371
413
|
|
|
372
414
|
return True
|
|
@@ -375,23 +417,36 @@ class TableParser:
|
|
|
375
417
|
"""
|
|
376
418
|
Check if table has a header row.
|
|
377
419
|
Common header patterns: "Item", "Value", "Description", "Column", etc.
|
|
420
|
+
|
|
421
|
+
A row is a header if:
|
|
422
|
+
- It does NOT start with a callout number (<1> or 1)
|
|
423
|
+
- It contains common header keywords in the cells
|
|
378
424
|
"""
|
|
379
425
|
if not table.rows:
|
|
380
426
|
return False
|
|
381
427
|
|
|
382
428
|
first_row = table.rows[0]
|
|
383
|
-
if not first_row.cells:
|
|
429
|
+
if not first_row.cells or len(first_row.cells) < 2:
|
|
384
430
|
return False
|
|
385
431
|
|
|
432
|
+
# If first cell is a callout number, this is NOT a header
|
|
433
|
+
first_cell = first_row.cells[0]
|
|
434
|
+
if first_cell.content:
|
|
435
|
+
first_cell_text = first_cell.content[0].strip()
|
|
436
|
+
is_callout, _ = self._is_callout_or_number(first_cell_text)
|
|
437
|
+
if is_callout:
|
|
438
|
+
return False
|
|
439
|
+
|
|
386
440
|
# Collect text from all cells in first row
|
|
387
441
|
header_text = ' '.join(
|
|
388
442
|
cell.content[0] if cell.content else ''
|
|
389
443
|
for cell in first_row.cells
|
|
390
444
|
).lower()
|
|
391
445
|
|
|
392
|
-
# Check for common header keywords
|
|
446
|
+
# Check for common header keywords (as whole words)
|
|
393
447
|
header_keywords = ['item', 'description', 'value', 'column', 'parameter', 'field', 'name']
|
|
394
|
-
|
|
448
|
+
import re
|
|
449
|
+
return any(re.search(r'\b' + re.escape(keyword) + r'\b', header_text) for keyword in header_keywords)
|
|
395
450
|
|
|
396
451
|
def is_3column_callout_table(self, table: AsciiDocTable) -> bool:
|
|
397
452
|
"""
|
|
@@ -417,19 +472,52 @@ class TableParser:
|
|
|
417
472
|
if not all(len(row.cells) == 3 for row in data_rows):
|
|
418
473
|
return False
|
|
419
474
|
|
|
420
|
-
# Check if first cell of each data row contains a plain number (1, 2, 3...)
|
|
475
|
+
# Check if first cell of each data row contains a callout or plain number (1, 2, 3... or <1>, <2>...)
|
|
421
476
|
for row in data_rows:
|
|
422
477
|
first_cell = row.cells[0]
|
|
423
478
|
if not first_cell.content:
|
|
424
479
|
return False
|
|
425
480
|
|
|
426
|
-
# First line of first cell should be a number
|
|
481
|
+
# First line of first cell should be a callout number or plain number
|
|
427
482
|
first_line = first_cell.content[0].strip()
|
|
428
|
-
|
|
483
|
+
is_match, _ = self._is_callout_or_number(first_line)
|
|
484
|
+
if not is_match:
|
|
429
485
|
return False
|
|
430
486
|
|
|
431
487
|
return True
|
|
432
488
|
|
|
489
|
+
def get_table_callout_numbers(self, table: AsciiDocTable) -> List[int]:
|
|
490
|
+
"""
|
|
491
|
+
Extract just the callout numbers from a table (in order, with duplicates).
|
|
492
|
+
Used for validation and diagnostics.
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
List of callout numbers in the order they appear in the table.
|
|
496
|
+
Preserves duplicates to help identify table errors.
|
|
497
|
+
"""
|
|
498
|
+
callout_numbers = []
|
|
499
|
+
|
|
500
|
+
# Determine if there's a header row and skip it
|
|
501
|
+
has_header = self._has_header_row(table)
|
|
502
|
+
data_rows = table.rows[1:] if has_header else table.rows
|
|
503
|
+
|
|
504
|
+
for row in data_rows:
|
|
505
|
+
# Handle both 2-column and 3-column tables
|
|
506
|
+
if len(row.cells) < 2:
|
|
507
|
+
continue
|
|
508
|
+
|
|
509
|
+
first_cell = row.cells[0]
|
|
510
|
+
if not first_cell.content:
|
|
511
|
+
continue
|
|
512
|
+
|
|
513
|
+
# Extract callout number (supports both <1> and 1 formats)
|
|
514
|
+
first_line = first_cell.content[0].strip()
|
|
515
|
+
is_match, callout_num = self._is_callout_or_number(first_line)
|
|
516
|
+
if is_match:
|
|
517
|
+
callout_numbers.append(callout_num)
|
|
518
|
+
|
|
519
|
+
return callout_numbers
|
|
520
|
+
|
|
433
521
|
def extract_callout_explanations_from_table(self, table: AsciiDocTable) -> Dict[int, Tuple[List[str], List[str]]]:
|
|
434
522
|
"""
|
|
435
523
|
Extract callout explanations from a table.
|
|
@@ -437,24 +525,32 @@ class TableParser:
|
|
|
437
525
|
|
|
438
526
|
The conditionals list includes any ifdef/ifndef/endif statements that should
|
|
439
527
|
be preserved when converting the table to other formats.
|
|
528
|
+
|
|
529
|
+
Accepts both callout format (<1>) and plain numbers (1).
|
|
530
|
+
Skips header rows if present.
|
|
531
|
+
|
|
532
|
+
Note: If table contains duplicate callout numbers, the last one wins.
|
|
533
|
+
Use get_table_callout_numbers() to detect duplicates.
|
|
440
534
|
"""
|
|
441
535
|
explanations = {}
|
|
442
536
|
|
|
443
|
-
|
|
537
|
+
# Determine if there's a header row and skip it
|
|
538
|
+
has_header = self._has_header_row(table)
|
|
539
|
+
data_rows = table.rows[1:] if has_header else table.rows
|
|
540
|
+
|
|
541
|
+
for row in data_rows:
|
|
444
542
|
if len(row.cells) != 2:
|
|
445
543
|
continue
|
|
446
544
|
|
|
447
545
|
callout_cell = row.cells[0]
|
|
448
546
|
explanation_cell = row.cells[1]
|
|
449
547
|
|
|
450
|
-
# Extract callout number
|
|
548
|
+
# Extract callout number (supports both <1> and 1 formats)
|
|
451
549
|
first_line = callout_cell.content[0].strip()
|
|
452
|
-
|
|
453
|
-
if not
|
|
550
|
+
is_match, callout_num = self._is_callout_or_number(first_line)
|
|
551
|
+
if not is_match:
|
|
454
552
|
continue
|
|
455
553
|
|
|
456
|
-
callout_num = int(match.group(1))
|
|
457
|
-
|
|
458
554
|
# Collect explanation lines, preserving blank lines and conditionals inline
|
|
459
555
|
# Blank lines will need to become continuation markers (+) in definition lists
|
|
460
556
|
explanation_lines = []
|
|
@@ -478,12 +574,14 @@ class TableParser:
|
|
|
478
574
|
Returns dict mapping callout number to tuple of (value_lines, description_lines, conditionals).
|
|
479
575
|
|
|
480
576
|
Format: Item | Value | Description
|
|
481
|
-
- Item: Number (1, 2, 3...) corresponding to callout number
|
|
577
|
+
- Item: Number (1, 2, 3...) or callout (<1>, <2>...) corresponding to callout number
|
|
482
578
|
- Value: The code/value being explained
|
|
483
579
|
- Description: Explanation text
|
|
484
580
|
|
|
485
581
|
The conditionals list includes any ifdef/ifndef/endif statements that should
|
|
486
582
|
be preserved when converting the table to other formats.
|
|
583
|
+
|
|
584
|
+
Accepts both callout format (<1>) and plain numbers (1).
|
|
487
585
|
"""
|
|
488
586
|
explanations = {}
|
|
489
587
|
|
|
@@ -499,16 +597,15 @@ class TableParser:
|
|
|
499
597
|
value_cell = row.cells[1]
|
|
500
598
|
desc_cell = row.cells[2]
|
|
501
599
|
|
|
502
|
-
# Extract item number (maps to callout number)
|
|
600
|
+
# Extract item number (maps to callout number) - supports both <1> and 1 formats
|
|
503
601
|
if not item_cell.content:
|
|
504
602
|
continue
|
|
505
603
|
|
|
506
604
|
item_num_str = item_cell.content[0].strip()
|
|
507
|
-
|
|
605
|
+
is_match, callout_num = self._is_callout_or_number(item_num_str)
|
|
606
|
+
if not is_match:
|
|
508
607
|
continue
|
|
509
608
|
|
|
510
|
-
callout_num = int(item_num_str)
|
|
511
|
-
|
|
512
609
|
# Collect value lines (column 2), preserving all content including conditionals
|
|
513
610
|
value_lines = []
|
|
514
611
|
for line in value_cell.content:
|
|
@@ -539,8 +636,12 @@ class TableParser:
|
|
|
539
636
|
Returns:
|
|
540
637
|
AsciiDocTable if a callout table is found, None otherwise
|
|
541
638
|
"""
|
|
542
|
-
# Skip
|
|
639
|
+
# Skip the closing delimiter of the code block (----, ...., etc.)
|
|
543
640
|
i = code_block_end + 1
|
|
641
|
+
if i < len(lines) and lines[i].strip() in ['----', '....', '====']:
|
|
642
|
+
i += 1
|
|
643
|
+
|
|
644
|
+
# Skip blank lines and continuation markers after code block
|
|
544
645
|
while i < len(lines) and (not lines[i].strip() or lines[i].strip() == '+'):
|
|
545
646
|
i += 1
|
|
546
647
|
|
|
@@ -562,12 +663,26 @@ class TableParser:
|
|
|
562
663
|
|
|
563
664
|
# Check for table delimiter
|
|
564
665
|
if self.TABLE_DELIMITER.match(line):
|
|
565
|
-
# Found a table,
|
|
666
|
+
# Found a table, extract attributes and title
|
|
667
|
+
attributes = ""
|
|
668
|
+
title = ""
|
|
566
669
|
start_line = j
|
|
670
|
+
|
|
671
|
+
# Check line before delimiter for attributes [cols="..."]
|
|
567
672
|
if j > 0 and self.TABLE_START.match(lines[j - 1]):
|
|
673
|
+
attributes = lines[j - 1]
|
|
674
|
+
start_line = j - 1
|
|
675
|
+
|
|
676
|
+
# Check line before attributes for title .Title
|
|
677
|
+
if j > 1 and lines[j - 2].strip().startswith('.') and not lines[j - 2].strip().startswith('..'):
|
|
678
|
+
title = lines[j - 2].strip()
|
|
679
|
+
start_line = j - 2
|
|
680
|
+
elif j > 0 and lines[j - 1].strip().startswith('.') and not lines[j - 1].strip().startswith('..'):
|
|
681
|
+
# Title directly before delimiter (no attributes)
|
|
682
|
+
title = lines[j - 1].strip()
|
|
568
683
|
start_line = j - 1
|
|
569
684
|
|
|
570
|
-
table = self._parse_table(lines, start_line, j)
|
|
685
|
+
table = self._parse_table(lines, start_line, j, title)
|
|
571
686
|
if table and (self.is_callout_table(table) or self.is_3column_callout_table(table)):
|
|
572
687
|
return table
|
|
573
688
|
|
|
@@ -20,6 +20,9 @@ from callout_lib import (
|
|
|
20
20
|
CodeBlock,
|
|
21
21
|
)
|
|
22
22
|
|
|
23
|
+
# Import version
|
|
24
|
+
from doc_utils.version import __version__
|
|
25
|
+
|
|
23
26
|
|
|
24
27
|
# Colors for output
|
|
25
28
|
class Colors:
|
|
@@ -307,7 +310,20 @@ class InteractiveCalloutConverter:
|
|
|
307
310
|
explanations, explanation_end = self.detector.extract_callout_explanations(new_lines, block.end_line)
|
|
308
311
|
|
|
309
312
|
if not explanations:
|
|
310
|
-
|
|
313
|
+
# Get callout numbers for warning message
|
|
314
|
+
all_callout_nums = []
|
|
315
|
+
for group in callout_groups:
|
|
316
|
+
all_callout_nums.extend(group.callout_numbers)
|
|
317
|
+
|
|
318
|
+
warning_msg = (
|
|
319
|
+
f"WARNING: {input_file.name} line {block.start_line + 1}: "
|
|
320
|
+
f"Code block has callouts {sorted(set(all_callout_nums))} but no explanations found after it. "
|
|
321
|
+
f"This may indicate: explanations are shared with another code block, "
|
|
322
|
+
f"explanations are in an unexpected location, or documentation error (missing explanations). "
|
|
323
|
+
f"Consider reviewing this block manually."
|
|
324
|
+
)
|
|
325
|
+
print_colored(warning_msg, Colors.YELLOW)
|
|
326
|
+
self.warnings.append(warning_msg)
|
|
311
327
|
continue
|
|
312
328
|
|
|
313
329
|
# Validate
|
|
@@ -332,9 +348,9 @@ class InteractiveCalloutConverter:
|
|
|
332
348
|
else:
|
|
333
349
|
converted_content = self.detector.remove_callouts_from_code(block.content)
|
|
334
350
|
if format_choice == 'bullets':
|
|
335
|
-
output_list = BulletListConverter.convert(callout_groups, explanations)
|
|
351
|
+
output_list = BulletListConverter.convert(callout_groups, explanations, self.detector.last_table_title)
|
|
336
352
|
else: # deflist
|
|
337
|
-
output_list = DefListConverter.convert(callout_groups, explanations)
|
|
353
|
+
output_list = DefListConverter.convert(callout_groups, explanations, self.detector.last_table_title)
|
|
338
354
|
|
|
339
355
|
# Replace in document
|
|
340
356
|
has_source_prefix = self.detector.CODE_BLOCK_START.match(new_lines[block.start_line])
|
|
@@ -431,6 +447,11 @@ Examples:
|
|
|
431
447
|
"""
|
|
432
448
|
)
|
|
433
449
|
|
|
450
|
+
parser.add_argument(
|
|
451
|
+
'--version',
|
|
452
|
+
action='version',
|
|
453
|
+
version=f'%(prog)s {__version__}'
|
|
454
|
+
)
|
|
434
455
|
parser.add_argument(
|
|
435
456
|
'path',
|
|
436
457
|
nargs='?',
|
|
@@ -521,6 +542,8 @@ Examples:
|
|
|
521
542
|
print_colored(f"\n⚠ {len(converter.warnings)} Warning(s):", Colors.YELLOW)
|
|
522
543
|
for warning in converter.warnings:
|
|
523
544
|
print_colored(f" {warning}", Colors.YELLOW)
|
|
545
|
+
print()
|
|
546
|
+
print_colored("Suggestion: Fix the callout mismatches in the files above and rerun this command.", Colors.YELLOW)
|
|
524
547
|
|
|
525
548
|
if args.dry_run and files_modified > 0:
|
|
526
549
|
print_colored("\nDRY RUN - No files were modified", Colors.YELLOW)
|