rolfedh-doc-utils 0.1.30__py3-none-any.whl → 0.1.32__py3-none-any.whl
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.
- callout_lib/converter_bullets.py +10 -2
- callout_lib/converter_deflist.py +10 -2
- callout_lib/detector.py +38 -8
- callout_lib/table_parser.py +170 -55
- convert_callouts_interactive.py +26 -3
- convert_callouts_to_deflist.py +145 -14
- doc_utils/version.py +1 -1
- doc_utils/warnings_report.py +237 -0
- {rolfedh_doc_utils-0.1.30.dist-info → rolfedh_doc_utils-0.1.32.dist-info}/METADATA +1 -1
- {rolfedh_doc_utils-0.1.30.dist-info → rolfedh_doc_utils-0.1.32.dist-info}/RECORD +14 -13
- {rolfedh_doc_utils-0.1.30.dist-info → rolfedh_doc_utils-0.1.32.dist-info}/WHEEL +0 -0
- {rolfedh_doc_utils-0.1.30.dist-info → rolfedh_doc_utils-0.1.32.dist-info}/entry_points.txt +0 -0
- {rolfedh_doc_utils-0.1.30.dist-info → rolfedh_doc_utils-0.1.32.dist-info}/licenses/LICENSE +0 -0
- {rolfedh_doc_utils-0.1.30.dist-info → rolfedh_doc_utils-0.1.32.dist-info}/top_level.txt +0 -0
callout_lib/converter_bullets.py
CHANGED
|
@@ -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:
|
callout_lib/converter_deflist.py
CHANGED
|
@@ -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:
|
callout_lib/detector.py
CHANGED
|
@@ -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)
|
callout_lib/table_parser.py
CHANGED
|
@@ -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
|
|
convert_callouts_interactive.py
CHANGED
|
@@ -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)
|
convert_callouts_to_deflist.py
CHANGED
|
@@ -22,6 +22,12 @@ from callout_lib import (
|
|
|
22
22
|
CommentConverter,
|
|
23
23
|
)
|
|
24
24
|
|
|
25
|
+
# Import warnings report generator
|
|
26
|
+
from doc_utils.warnings_report import generate_warnings_report
|
|
27
|
+
|
|
28
|
+
# Import version
|
|
29
|
+
from doc_utils.version import __version__
|
|
30
|
+
|
|
25
31
|
|
|
26
32
|
# Colors for output
|
|
27
33
|
class Colors:
|
|
@@ -40,11 +46,12 @@ class CalloutConverter:
|
|
|
40
46
|
"""Converts callout-style documentation to various formats."""
|
|
41
47
|
|
|
42
48
|
def __init__(self, dry_run: bool = False, verbose: bool = False, output_format: str = 'deflist',
|
|
43
|
-
max_comment_length: int = 120):
|
|
49
|
+
max_comment_length: int = 120, force: bool = False):
|
|
44
50
|
self.dry_run = dry_run
|
|
45
51
|
self.verbose = verbose
|
|
46
52
|
self.output_format = output_format # 'deflist', 'bullets', or 'comments'
|
|
47
53
|
self.max_comment_length = max_comment_length # Max length for inline comments
|
|
54
|
+
self.force = force # Force strip callouts even with warnings
|
|
48
55
|
self.changes_made = 0
|
|
49
56
|
self.warnings = [] # Collect warnings for summary
|
|
50
57
|
self.long_comment_warnings = [] # Warnings for comments exceeding max length
|
|
@@ -103,11 +110,50 @@ class CalloutConverter:
|
|
|
103
110
|
|
|
104
111
|
if not explanations:
|
|
105
112
|
self.log(f"No explanations found after block at line {block.start_line + 1}")
|
|
106
|
-
|
|
113
|
+
# Warn user about code blocks with callouts but no explanations
|
|
114
|
+
warning_msg = (
|
|
115
|
+
f"WARNING: {input_file.name} line {block.start_line + 1}: "
|
|
116
|
+
f"Code block has callouts {sorted(set(all_callout_nums))} but no explanations found after it. "
|
|
117
|
+
f"This may indicate: explanations are shared with another code block, "
|
|
118
|
+
f"explanations are in an unexpected location, or documentation error (missing explanations). "
|
|
119
|
+
f"Consider reviewing this block manually."
|
|
120
|
+
)
|
|
121
|
+
print_colored(warning_msg, Colors.YELLOW)
|
|
122
|
+
self.warnings.append(warning_msg)
|
|
123
|
+
|
|
124
|
+
# In force mode, strip callouts anyway
|
|
125
|
+
if not self.force:
|
|
126
|
+
continue
|
|
127
|
+
else:
|
|
128
|
+
self.log(f"FORCE MODE: Stripping callouts from block at line {block.start_line + 1} despite missing explanations")
|
|
129
|
+
|
|
130
|
+
# Just strip callouts without creating explanation list
|
|
131
|
+
converted_content = self.detector.remove_callouts_from_code(block.content)
|
|
132
|
+
|
|
133
|
+
# Replace in document
|
|
134
|
+
has_source_prefix = self.detector.CODE_BLOCK_START.match(new_lines[block.start_line])
|
|
135
|
+
if has_source_prefix:
|
|
136
|
+
content_start = block.start_line + 2 # After [source] and ----
|
|
137
|
+
else:
|
|
138
|
+
content_start = block.start_line + 1 # After ---- only
|
|
139
|
+
content_end = block.end_line
|
|
140
|
+
|
|
141
|
+
# Build new section with just code (no explanations)
|
|
142
|
+
new_section = (
|
|
143
|
+
new_lines[:content_start] +
|
|
144
|
+
converted_content +
|
|
145
|
+
[new_lines[content_end]] + # Keep closing delimiter
|
|
146
|
+
new_lines[content_end + 1:] # Keep rest of file
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
new_lines = new_section
|
|
150
|
+
conversions += 1
|
|
151
|
+
self.changes_made += 1
|
|
152
|
+
continue
|
|
107
153
|
|
|
108
154
|
# Validate callouts match
|
|
109
155
|
is_valid, code_nums, explanation_nums = self.detector.validate_callouts(callout_groups, explanations)
|
|
110
|
-
if not is_valid:
|
|
156
|
+
if not is_valid and explanations: # Only validate if we have explanations
|
|
111
157
|
# Format warning message with file and line numbers
|
|
112
158
|
line_range = f"{block.start_line + 1}-{block.end_line + 1}"
|
|
113
159
|
warning_msg = (
|
|
@@ -116,7 +162,12 @@ class CalloutConverter:
|
|
|
116
162
|
)
|
|
117
163
|
print_colored(warning_msg, Colors.YELLOW)
|
|
118
164
|
self.warnings.append(warning_msg)
|
|
119
|
-
|
|
165
|
+
|
|
166
|
+
# In force mode, convert anyway
|
|
167
|
+
if not self.force:
|
|
168
|
+
continue
|
|
169
|
+
else:
|
|
170
|
+
self.log(f"FORCE MODE: Converting block at line {block.start_line + 1} despite callout mismatch")
|
|
120
171
|
|
|
121
172
|
self.log(f"Converting block at line {block.start_line + 1}")
|
|
122
173
|
|
|
@@ -144,7 +195,7 @@ class CalloutConverter:
|
|
|
144
195
|
# Fall back to definition list
|
|
145
196
|
self.log(f"Falling back to definition list for block at line {block.start_line + 1}")
|
|
146
197
|
converted_content = self.detector.remove_callouts_from_code(block.content)
|
|
147
|
-
output_list = DefListConverter.convert(callout_groups, explanations)
|
|
198
|
+
output_list = DefListConverter.convert(callout_groups, explanations, self.detector.last_table_title)
|
|
148
199
|
use_deflist_fallback = True
|
|
149
200
|
else:
|
|
150
201
|
output_list = [] # No separate list after code block for comments
|
|
@@ -153,9 +204,9 @@ class CalloutConverter:
|
|
|
153
204
|
converted_content = self.detector.remove_callouts_from_code(block.content)
|
|
154
205
|
|
|
155
206
|
if self.output_format == 'bullets':
|
|
156
|
-
output_list = BulletListConverter.convert(callout_groups, explanations)
|
|
207
|
+
output_list = BulletListConverter.convert(callout_groups, explanations, self.detector.last_table_title)
|
|
157
208
|
else: # default to 'deflist'
|
|
158
|
-
output_list = DefListConverter.convert(callout_groups, explanations)
|
|
209
|
+
output_list = DefListConverter.convert(callout_groups, explanations, self.detector.last_table_title)
|
|
159
210
|
|
|
160
211
|
# Replace in document
|
|
161
212
|
# Check if block has [source] prefix
|
|
@@ -170,23 +221,40 @@ class CalloutConverter:
|
|
|
170
221
|
# For deflist/bullets format, remove old explanations and add new list
|
|
171
222
|
if self.output_format == 'comments' and not use_deflist_fallback:
|
|
172
223
|
# Remove old callout explanations (list or table format)
|
|
224
|
+
# Find where explanations/table actually starts to preserve content in between
|
|
225
|
+
if self.detector.last_table:
|
|
226
|
+
explanation_start_line = self.detector.last_table.start_line
|
|
227
|
+
else:
|
|
228
|
+
# List format: skip blank lines after code block
|
|
229
|
+
explanation_start_line = block.end_line + 1
|
|
230
|
+
while explanation_start_line < len(new_lines) and not new_lines[explanation_start_line].strip():
|
|
231
|
+
explanation_start_line += 1
|
|
232
|
+
|
|
173
233
|
new_section = (
|
|
174
234
|
new_lines[:content_start] +
|
|
175
235
|
converted_content +
|
|
176
236
|
[new_lines[content_end]] + # Keep closing delimiter
|
|
237
|
+
new_lines[content_end + 1:explanation_start_line] + # Preserve content between code and explanations
|
|
177
238
|
new_lines[explanation_end + 1:] # Skip explanations/table, keep rest
|
|
178
239
|
)
|
|
179
240
|
else:
|
|
180
241
|
# Remove old callout explanations and add new list
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
242
|
+
# Find where explanations/table actually starts
|
|
243
|
+
if self.detector.last_table:
|
|
244
|
+
# Table format: preserve content between code block and table start
|
|
245
|
+
explanation_start_line = self.detector.last_table.start_line
|
|
246
|
+
else:
|
|
247
|
+
# List format: skip blank lines after code block
|
|
248
|
+
explanation_start_line = block.end_line + 1
|
|
249
|
+
while explanation_start_line < len(new_lines) and not new_lines[explanation_start_line].strip():
|
|
250
|
+
explanation_start_line += 1
|
|
184
251
|
|
|
185
252
|
# Build the new section
|
|
186
253
|
new_section = (
|
|
187
254
|
new_lines[:content_start] +
|
|
188
255
|
converted_content +
|
|
189
256
|
[new_lines[content_end]] + # Keep closing delimiter
|
|
257
|
+
new_lines[content_end + 1:explanation_start_line] + # Preserve content between code and explanations
|
|
190
258
|
output_list +
|
|
191
259
|
new_lines[explanation_end + 1:]
|
|
192
260
|
)
|
|
@@ -330,6 +398,11 @@ Example transformation (deflist format):
|
|
|
330
398
|
"""
|
|
331
399
|
)
|
|
332
400
|
|
|
401
|
+
parser.add_argument(
|
|
402
|
+
'--version',
|
|
403
|
+
action='version',
|
|
404
|
+
version=f'%(prog)s {__version__}'
|
|
405
|
+
)
|
|
333
406
|
parser.add_argument(
|
|
334
407
|
'path',
|
|
335
408
|
nargs='?',
|
|
@@ -377,6 +450,30 @@ Example transformation (deflist format):
|
|
|
377
450
|
type=Path,
|
|
378
451
|
help='Path to file containing directories/files to exclude, one per line'
|
|
379
452
|
)
|
|
453
|
+
parser.add_argument(
|
|
454
|
+
'--warnings-report',
|
|
455
|
+
dest='warnings_report',
|
|
456
|
+
action='store_true',
|
|
457
|
+
default=True,
|
|
458
|
+
help='Generate warnings report file (default: enabled)'
|
|
459
|
+
)
|
|
460
|
+
parser.add_argument(
|
|
461
|
+
'--no-warnings-report',
|
|
462
|
+
dest='warnings_report',
|
|
463
|
+
action='store_false',
|
|
464
|
+
help='Disable warnings report file generation'
|
|
465
|
+
)
|
|
466
|
+
parser.add_argument(
|
|
467
|
+
'--warnings-file',
|
|
468
|
+
type=Path,
|
|
469
|
+
default=Path('callout-warnings-report.adoc'),
|
|
470
|
+
help='Path for warnings report file (default: callout-warnings-report.adoc)'
|
|
471
|
+
)
|
|
472
|
+
parser.add_argument(
|
|
473
|
+
'--force',
|
|
474
|
+
action='store_true',
|
|
475
|
+
help='Force strip callouts from code blocks even with warnings (USE WITH CAUTION: only after reviewing and fixing callout issues)'
|
|
476
|
+
)
|
|
380
477
|
|
|
381
478
|
args = parser.parse_args()
|
|
382
479
|
|
|
@@ -414,9 +511,24 @@ Example transformation (deflist format):
|
|
|
414
511
|
|
|
415
512
|
print(f"Found {len(adoc_files)} AsciiDoc file(s) to process")
|
|
416
513
|
|
|
514
|
+
# If force mode is enabled, show warning and ask for confirmation
|
|
515
|
+
if args.force and not args.dry_run:
|
|
516
|
+
print_colored("\n⚠️ FORCE MODE ENABLED ⚠️", Colors.YELLOW)
|
|
517
|
+
print_colored("This will strip callouts from code blocks even when warnings are present.", Colors.YELLOW)
|
|
518
|
+
print_colored("You should only use this option AFTER:", Colors.YELLOW)
|
|
519
|
+
print_colored(" 1. Reviewing all warnings in the warnings report", Colors.YELLOW)
|
|
520
|
+
print_colored(" 2. Manually fixing callout issues where appropriate", Colors.YELLOW)
|
|
521
|
+
print_colored(" 3. Confirming that remaining warnings are acceptable", Colors.YELLOW)
|
|
522
|
+
print()
|
|
523
|
+
response = input("Are you sure you want to proceed with force mode? (yes/no): ").strip().lower()
|
|
524
|
+
if response not in ['yes', 'y']:
|
|
525
|
+
print_colored("Operation cancelled.", Colors.YELLOW)
|
|
526
|
+
sys.exit(0)
|
|
527
|
+
print()
|
|
528
|
+
|
|
417
529
|
# Create converter
|
|
418
530
|
converter = CalloutConverter(dry_run=args.dry_run, verbose=args.verbose, output_format=args.format,
|
|
419
|
-
max_comment_length=args.max_comment_length)
|
|
531
|
+
max_comment_length=args.max_comment_length, force=args.force)
|
|
420
532
|
|
|
421
533
|
# Process each file
|
|
422
534
|
files_processed = 0
|
|
@@ -459,9 +571,28 @@ Example transformation (deflist format):
|
|
|
459
571
|
|
|
460
572
|
# Display warning summary if any warnings were collected
|
|
461
573
|
if converter.warnings:
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
574
|
+
# Generate warnings report if enabled
|
|
575
|
+
if args.warnings_report:
|
|
576
|
+
try:
|
|
577
|
+
generate_warnings_report(converter.warnings, args.warnings_file)
|
|
578
|
+
print_colored(f"\n⚠️ {len(converter.warnings)} Warning(s) - See {args.warnings_file} for details", Colors.YELLOW)
|
|
579
|
+
print()
|
|
580
|
+
print_colored(f"Suggestion: Review and fix the callout issues listed in {args.warnings_file}, then rerun this command.", Colors.YELLOW)
|
|
581
|
+
except Exception as e:
|
|
582
|
+
print_colored(f"\n⚠️ {len(converter.warnings)} Warning(s):", Colors.YELLOW)
|
|
583
|
+
print_colored(f"Error generating warnings report: {e}", Colors.RED)
|
|
584
|
+
# Fall back to displaying warnings in console
|
|
585
|
+
for warning in converter.warnings:
|
|
586
|
+
print_colored(f" {warning}", Colors.YELLOW)
|
|
587
|
+
print()
|
|
588
|
+
print_colored("Suggestion: Fix the callout issues listed above and rerun this command.", Colors.YELLOW)
|
|
589
|
+
else:
|
|
590
|
+
# Console-only output (legacy behavior)
|
|
591
|
+
print_colored(f"\n⚠️ {len(converter.warnings)} Warning(s):", Colors.YELLOW)
|
|
592
|
+
for warning in converter.warnings:
|
|
593
|
+
print_colored(f" {warning}", Colors.YELLOW)
|
|
594
|
+
print()
|
|
595
|
+
print_colored("Suggestion: Fix the callout issues listed above and rerun this command.", Colors.YELLOW)
|
|
465
596
|
print()
|
|
466
597
|
|
|
467
598
|
if args.dry_run and files_modified > 0:
|
doc_utils/version.py
CHANGED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generate AsciiDoc warnings report for callout conversion issues.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import List, Dict, Set
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class WarningInfo:
|
|
11
|
+
"""Information about a specific warning."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, warning_type: str, file_name: str, line_info: str,
|
|
14
|
+
code_nums: List[int] = None, explanation_nums: List[int] = None):
|
|
15
|
+
self.warning_type = warning_type # 'mismatch' or 'missing'
|
|
16
|
+
self.file_name = file_name
|
|
17
|
+
self.line_info = line_info # e.g., "211" or "55-72"
|
|
18
|
+
self.code_nums = code_nums or []
|
|
19
|
+
self.explanation_nums = explanation_nums or []
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def parse_warning_message(warning_msg: str) -> WarningInfo:
|
|
23
|
+
"""
|
|
24
|
+
Parse a warning message to extract structured information.
|
|
25
|
+
|
|
26
|
+
Examples:
|
|
27
|
+
- "WARNING: file.adoc lines 55-72: Callout mismatch: code has [1, 2], explanations have [1, 3]"
|
|
28
|
+
- "WARNING: file.adoc line 211: Code block has callouts [1, 2, 3, 4] but no explanations found..."
|
|
29
|
+
"""
|
|
30
|
+
import re
|
|
31
|
+
|
|
32
|
+
# Extract file name and line info
|
|
33
|
+
match = re.match(r'WARNING: (.+?) lines? ([\d-]+):', warning_msg)
|
|
34
|
+
if not match:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
file_name = match.group(1)
|
|
38
|
+
line_info = match.group(2)
|
|
39
|
+
|
|
40
|
+
# Determine warning type and extract callout numbers
|
|
41
|
+
if 'Callout mismatch' in warning_msg:
|
|
42
|
+
# Parse: "code has [1, 2], explanations have [1, 3]"
|
|
43
|
+
code_match = re.search(r'code has \[([^\]]+)\]', warning_msg)
|
|
44
|
+
exp_match = re.search(r'explanations have \[([^\]]+)\]', warning_msg)
|
|
45
|
+
|
|
46
|
+
code_nums = []
|
|
47
|
+
exp_nums = []
|
|
48
|
+
|
|
49
|
+
if code_match:
|
|
50
|
+
code_nums = [int(n.strip()) for n in code_match.group(1).split(',')]
|
|
51
|
+
if exp_match:
|
|
52
|
+
exp_nums = [int(n.strip()) for n in exp_match.group(1).split(',')]
|
|
53
|
+
|
|
54
|
+
return WarningInfo('mismatch', file_name, line_info, code_nums, exp_nums)
|
|
55
|
+
|
|
56
|
+
elif 'but no explanations found' in warning_msg:
|
|
57
|
+
# Parse: "Code block has callouts [1, 2, 3, 4] but no explanations found"
|
|
58
|
+
callouts_match = re.search(r'has callouts \[([^\]]+)\]', warning_msg)
|
|
59
|
+
|
|
60
|
+
code_nums = []
|
|
61
|
+
if callouts_match:
|
|
62
|
+
code_nums = [int(n.strip()) for n in callouts_match.group(1).split(',')]
|
|
63
|
+
|
|
64
|
+
return WarningInfo('missing', file_name, line_info, code_nums, [])
|
|
65
|
+
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def analyze_mismatch(code_nums: List[int], exp_nums: List[int]) -> List[str]:
|
|
70
|
+
"""
|
|
71
|
+
Analyze what's wrong with a callout mismatch.
|
|
72
|
+
|
|
73
|
+
Returns a list of issue descriptions.
|
|
74
|
+
"""
|
|
75
|
+
issues = []
|
|
76
|
+
code_set = set(code_nums)
|
|
77
|
+
exp_set = set(exp_nums)
|
|
78
|
+
|
|
79
|
+
# Check for duplicates in explanations
|
|
80
|
+
exp_counts = {}
|
|
81
|
+
for num in exp_nums:
|
|
82
|
+
exp_counts[num] = exp_counts.get(num, 0) + 1
|
|
83
|
+
|
|
84
|
+
duplicates = [num for num, count in exp_counts.items() if count > 1]
|
|
85
|
+
if duplicates:
|
|
86
|
+
for dup in duplicates:
|
|
87
|
+
count = exp_counts[dup]
|
|
88
|
+
issues.append(f"Duplicate callout: {dup} (appears {count} times in explanations)")
|
|
89
|
+
|
|
90
|
+
# Check for missing callouts (in code but not in explanations)
|
|
91
|
+
missing_in_exp = code_set - exp_set
|
|
92
|
+
if missing_in_exp:
|
|
93
|
+
for num in sorted(missing_in_exp):
|
|
94
|
+
issues.append(f"Missing callout: {num} (in code but not in explanations)")
|
|
95
|
+
|
|
96
|
+
# Check for extra callouts (in explanations but not in code)
|
|
97
|
+
extra_in_exp = exp_set - code_set
|
|
98
|
+
if extra_in_exp:
|
|
99
|
+
for num in sorted(extra_in_exp):
|
|
100
|
+
issues.append(f"Extra callout: {num} (in explanations but not in code)")
|
|
101
|
+
|
|
102
|
+
# Check for off-by-one errors
|
|
103
|
+
if code_nums and exp_nums:
|
|
104
|
+
code_start = min(code_nums)
|
|
105
|
+
exp_start = min(exp_nums)
|
|
106
|
+
if code_start != exp_start and not (missing_in_exp or extra_in_exp or duplicates):
|
|
107
|
+
issues.append(f"Off-by-one error (code starts at {code_start}, explanations start at {exp_start})")
|
|
108
|
+
|
|
109
|
+
return issues
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def generate_warnings_report(warnings: List[str], output_path: Path = None) -> str:
|
|
113
|
+
"""
|
|
114
|
+
Generate an AsciiDoc warnings report from warning messages.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
warnings: List of warning message strings
|
|
118
|
+
output_path: Path to write report file (if None, returns content only)
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
The report content as a string
|
|
122
|
+
"""
|
|
123
|
+
if not warnings:
|
|
124
|
+
return ""
|
|
125
|
+
|
|
126
|
+
# Parse all warnings
|
|
127
|
+
parsed_warnings = []
|
|
128
|
+
for warning in warnings:
|
|
129
|
+
parsed = parse_warning_message(warning)
|
|
130
|
+
if parsed:
|
|
131
|
+
parsed_warnings.append(parsed)
|
|
132
|
+
|
|
133
|
+
if not parsed_warnings:
|
|
134
|
+
return ""
|
|
135
|
+
|
|
136
|
+
# Group warnings by type
|
|
137
|
+
mismatch_warnings = [w for w in parsed_warnings if w.warning_type == 'mismatch']
|
|
138
|
+
missing_warnings = [w for w in parsed_warnings if w.warning_type == 'missing']
|
|
139
|
+
|
|
140
|
+
# Generate report content
|
|
141
|
+
lines = []
|
|
142
|
+
lines.append("= Callout Conversion Warnings Report")
|
|
143
|
+
lines.append(":toc:")
|
|
144
|
+
lines.append("")
|
|
145
|
+
lines.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
146
|
+
lines.append("")
|
|
147
|
+
|
|
148
|
+
# Summary
|
|
149
|
+
lines.append("== Summary")
|
|
150
|
+
lines.append("")
|
|
151
|
+
lines.append(f"Total warnings: {len(parsed_warnings)}")
|
|
152
|
+
if mismatch_warnings:
|
|
153
|
+
lines.append(f"- Callout mismatches: {len(mismatch_warnings)}")
|
|
154
|
+
if missing_warnings:
|
|
155
|
+
lines.append(f"- Missing explanations: {len(missing_warnings)}")
|
|
156
|
+
lines.append("")
|
|
157
|
+
lines.append("== Recommended Actions")
|
|
158
|
+
lines.append("")
|
|
159
|
+
lines.append("1. Review each warning below and fix callout issues where appropriate")
|
|
160
|
+
lines.append("2. For callout mismatches: Ensure code callouts match explanation numbers")
|
|
161
|
+
lines.append("3. For missing explanations: Check if explanations are shared with another block or missing")
|
|
162
|
+
lines.append("4. After fixing issues, rerun the conversion command")
|
|
163
|
+
lines.append("")
|
|
164
|
+
lines.append("== Force Mode Option")
|
|
165
|
+
lines.append("")
|
|
166
|
+
lines.append("CAUTION: Use this option sparingly and only after reviewing all warnings.")
|
|
167
|
+
lines.append("")
|
|
168
|
+
lines.append("If you've reviewed all warnings and confirmed that remaining issues are acceptable,")
|
|
169
|
+
lines.append("you can use the `--force` option to strip callouts from code blocks despite warnings:")
|
|
170
|
+
lines.append("")
|
|
171
|
+
lines.append("[source,bash]")
|
|
172
|
+
lines.append("----")
|
|
173
|
+
lines.append("convert-callouts-to-deflist --force modules/")
|
|
174
|
+
lines.append("----")
|
|
175
|
+
lines.append("")
|
|
176
|
+
lines.append("Force mode will:")
|
|
177
|
+
lines.append("")
|
|
178
|
+
lines.append("- Strip callouts from blocks with missing explanations (without creating explanation lists)")
|
|
179
|
+
lines.append("- Convert blocks with callout mismatches using available explanations")
|
|
180
|
+
lines.append("- Require confirmation before proceeding (unless in dry-run mode)")
|
|
181
|
+
lines.append("")
|
|
182
|
+
lines.append("IMPORTANT: Always work in a git branch and review changes with `git diff` before committing.")
|
|
183
|
+
lines.append("")
|
|
184
|
+
|
|
185
|
+
# Callout Mismatch section
|
|
186
|
+
if mismatch_warnings:
|
|
187
|
+
lines.append("== Callout Mismatch Warnings")
|
|
188
|
+
lines.append("")
|
|
189
|
+
lines.append("Callout numbers in code don't match explanation numbers.")
|
|
190
|
+
lines.append("")
|
|
191
|
+
|
|
192
|
+
for warning in mismatch_warnings:
|
|
193
|
+
lines.append(f"=== {warning.file_name}")
|
|
194
|
+
lines.append("")
|
|
195
|
+
lines.append(f"*Lines {warning.line_info}*")
|
|
196
|
+
lines.append("")
|
|
197
|
+
lines.append(f"Code has:: {warning.code_nums}")
|
|
198
|
+
lines.append(f"Explanations have:: {warning.explanation_nums}")
|
|
199
|
+
lines.append("")
|
|
200
|
+
|
|
201
|
+
issues = analyze_mismatch(warning.code_nums, warning.explanation_nums)
|
|
202
|
+
if issues:
|
|
203
|
+
lines.append("Issues detected::")
|
|
204
|
+
for issue in issues:
|
|
205
|
+
lines.append(f"- {issue}")
|
|
206
|
+
lines.append("")
|
|
207
|
+
|
|
208
|
+
# Missing Explanations section
|
|
209
|
+
if missing_warnings:
|
|
210
|
+
lines.append("== Missing Explanations Warnings")
|
|
211
|
+
lines.append("")
|
|
212
|
+
lines.append("Code blocks with callouts but no explanations found after them.")
|
|
213
|
+
lines.append("")
|
|
214
|
+
|
|
215
|
+
for warning in missing_warnings:
|
|
216
|
+
lines.append(f"=== {warning.file_name}")
|
|
217
|
+
lines.append("")
|
|
218
|
+
lines.append(f"*Line {warning.line_info}*")
|
|
219
|
+
lines.append("")
|
|
220
|
+
lines.append(f"Callouts in code:: {warning.code_nums}")
|
|
221
|
+
lines.append("")
|
|
222
|
+
lines.append("Possible causes::")
|
|
223
|
+
lines.append("- Explanations shared with another code block (e.g., in conditional sections)")
|
|
224
|
+
lines.append("- Explanations in unexpected location")
|
|
225
|
+
lines.append("- Documentation error (missing explanations)")
|
|
226
|
+
lines.append("")
|
|
227
|
+
lines.append("Action:: Review this block manually")
|
|
228
|
+
lines.append("")
|
|
229
|
+
|
|
230
|
+
content = '\n'.join(lines)
|
|
231
|
+
|
|
232
|
+
# Write to file if path provided
|
|
233
|
+
if output_path:
|
|
234
|
+
with open(output_path, 'w', encoding='utf-8') as f:
|
|
235
|
+
f.write(content)
|
|
236
|
+
|
|
237
|
+
return content
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
archive_unused_files.py,sha256=OJZrkqn70hiOXED218jMYPFNFWnsDpjsCYOmBRxYnHU,2274
|
|
2
2
|
archive_unused_images.py,sha256=fZeyEZtTd72Gbd3YBXTy5xoshAAM9qb4qFPMjhHL1Fg,1864
|
|
3
3
|
check_scannability.py,sha256=O6ROr-e624jVPvPpASpsWo0gTfuCFpA2mTSX61BjAEI,5478
|
|
4
|
-
convert_callouts_interactive.py,sha256=
|
|
5
|
-
convert_callouts_to_deflist.py,sha256=
|
|
4
|
+
convert_callouts_interactive.py,sha256=9mmXEIbJo8G2Drj0uMWHFsF59zwJtNPgR2MNsisELLE,22160
|
|
5
|
+
convert_callouts_to_deflist.py,sha256=zbQeig2d69gHJPVEiV9jPUg2IzYFPjVzj1GIUacRk7Q,24895
|
|
6
6
|
doc_utils_cli.py,sha256=J3CE7cTDDCRGkhAknYejNWHhk5t9YFGt27WDVfR98Xk,5111
|
|
7
7
|
extract_link_attributes.py,sha256=wR2SmR2la-jR6DzDbas2PoNONgRZ4dZ6aqwzkwEv8Gs,3516
|
|
8
8
|
find_unused_attributes.py,sha256=77CxFdm72wj6SO81w-auMdDjnvF83jWy_qaM7DsAtBw,4263
|
|
@@ -10,11 +10,11 @@ format_asciidoc_spacing.py,sha256=nmWpw2dgwhd81LXyznq0rT8w6Z7cNRyGtPJGRyKFRdc,42
|
|
|
10
10
|
replace_link_attributes.py,sha256=Cpc4E-j9j-4_y0LOstAKYOPl02Ln_2bGNIeqp3ZVCdA,7624
|
|
11
11
|
validate_links.py,sha256=lWuK8sgfiFdfcUdSVAt_5U9JHVde_oa6peSUlBQtsac,6145
|
|
12
12
|
callout_lib/__init__.py,sha256=8B82N_z4D1LaZVYgd5jZR53QAabtgPzADOyGlnvihj0,665
|
|
13
|
-
callout_lib/converter_bullets.py,sha256=
|
|
13
|
+
callout_lib/converter_bullets.py,sha256=nfH0hz4p8qNM2F-MhtBjwH-lUYcNf2m1sdJebRlCxoo,4405
|
|
14
14
|
callout_lib/converter_comments.py,sha256=do0dH8uOyNFpn5CDEzR0jYYCMIPP3oPFM8cEB-Fp22c,9767
|
|
15
|
-
callout_lib/converter_deflist.py,sha256=
|
|
16
|
-
callout_lib/detector.py,sha256=
|
|
17
|
-
callout_lib/table_parser.py,sha256=
|
|
15
|
+
callout_lib/converter_deflist.py,sha256=LphSVdvCAcH1k7ysiBRQcrfaXRs48lmygSijYXVpu40,4942
|
|
16
|
+
callout_lib/detector.py,sha256=oU36eaSz-damdylPuOftTwNU5ZjVf8GMJ44txcFAFOM,15474
|
|
17
|
+
callout_lib/table_parser.py,sha256=ZucisADE8RDAk5HtIrttaPgBi6Hf8ZUpw7KzfbcmEjc,31450
|
|
18
18
|
doc_utils/__init__.py,sha256=qqZR3lohzkP63soymrEZPBGzzk6-nFzi4_tSffjmu_0,74
|
|
19
19
|
doc_utils/extract_link_attributes.py,sha256=U0EvPZReJQigNfbT-icBsVT6Li64hYki5W7MQz6qqbc,22743
|
|
20
20
|
doc_utils/file_utils.py,sha256=fpTh3xx759sF8sNocdn_arsP3KAv8XA6cTQTAVIZiZg,4247
|
|
@@ -27,11 +27,12 @@ doc_utils/unused_adoc.py,sha256=2cbqcYr1os2EhETUU928BlPRlsZVSdI00qaMhqjSIqQ,5263
|
|
|
27
27
|
doc_utils/unused_attributes.py,sha256=OHyAdaBD7aNo357B0SLBN5NC_jNY5TWXMwgtfJNh3X8,7621
|
|
28
28
|
doc_utils/unused_images.py,sha256=nqn36Bbrmon2KlGlcaruNjJJvTQ8_9H0WU9GvCW7rW8,1456
|
|
29
29
|
doc_utils/validate_links.py,sha256=iBGXnwdeLlgIT3fo3v01ApT5k0X2FtctsvkrE6E3VMk,19610
|
|
30
|
-
doc_utils/version.py,sha256=
|
|
30
|
+
doc_utils/version.py,sha256=0NJYwHS5xU9zjMDJzmJazrS_NbnmIeQhFMCZS7ZUNvQ,203
|
|
31
31
|
doc_utils/version_check.py,sha256=-31Y6AN0KGi_CUCAVOOhf6bPO3r7SQIXPxxeffLAF0w,7535
|
|
32
|
-
|
|
33
|
-
rolfedh_doc_utils-0.1.
|
|
34
|
-
rolfedh_doc_utils-0.1.
|
|
35
|
-
rolfedh_doc_utils-0.1.
|
|
36
|
-
rolfedh_doc_utils-0.1.
|
|
37
|
-
rolfedh_doc_utils-0.1.
|
|
32
|
+
doc_utils/warnings_report.py,sha256=20yfwqBjOprfFhQwCujbcsvjJCbHHhmH84uAujm-y-o,8877
|
|
33
|
+
rolfedh_doc_utils-0.1.32.dist-info/licenses/LICENSE,sha256=vLxtwMVOJA_hEy8b77niTkdmQI9kNJskXHq0dBS36e0,1075
|
|
34
|
+
rolfedh_doc_utils-0.1.32.dist-info/METADATA,sha256=PVHT2JPkbSx-8AY7xGOYqI-RHT-2I2YTmS41EZmsma8,8325
|
|
35
|
+
rolfedh_doc_utils-0.1.32.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
36
|
+
rolfedh_doc_utils-0.1.32.dist-info/entry_points.txt,sha256=vL_LlLKOiurRzchrq8iRUQG19Xi9lSAFVZGjO-xyErk,577
|
|
37
|
+
rolfedh_doc_utils-0.1.32.dist-info/top_level.txt,sha256=J4xtr3zoyCip27b3GnticFVZoyz5HHtgGqHQ-SZONCA,265
|
|
38
|
+
rolfedh_doc_utils-0.1.32.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|