rolfedh-doc-utils 0.1.29__py3-none-any.whl → 0.1.31__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 +37 -2
- callout_lib/detector.py +55 -21
- callout_lib/table_parser.py +299 -87
- convert_callouts_interactive.py +18 -3
- convert_callouts_to_deflist.py +137 -14
- doc_utils/version.py +1 -1
- doc_utils/warnings_report.py +237 -0
- {rolfedh_doc_utils-0.1.29.dist-info → rolfedh_doc_utils-0.1.31.dist-info}/METADATA +1 -1
- {rolfedh_doc_utils-0.1.29.dist-info → rolfedh_doc_utils-0.1.31.dist-info}/RECORD +14 -13
- {rolfedh_doc_utils-0.1.29.dist-info → rolfedh_doc_utils-0.1.31.dist-info}/WHEEL +0 -0
- {rolfedh_doc_utils-0.1.29.dist-info → rolfedh_doc_utils-0.1.31.dist-info}/entry_points.txt +0 -0
- {rolfedh_doc_utils-0.1.29.dist-info → rolfedh_doc_utils-0.1.31.dist-info}/licenses/LICENSE +0 -0
- {rolfedh_doc_utils-0.1.29.dist-info → rolfedh_doc_utils-0.1.31.dist-info}/top_level.txt +0 -0
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,64 @@ 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)
|
|
80
|
+
|
|
81
|
+
def _finalize_row_if_complete(self, current_row_cells, conditionals_before_row,
|
|
82
|
+
conditionals_after_row, expected_columns, rows):
|
|
83
|
+
"""
|
|
84
|
+
Check if we have enough cells for a complete row, and if so, save it.
|
|
85
|
+
|
|
86
|
+
Returns: (new_current_row_cells, new_conditionals_before, new_conditionals_after)
|
|
87
|
+
"""
|
|
88
|
+
if expected_columns > 0 and len(current_row_cells) >= expected_columns:
|
|
89
|
+
# Row is complete - save it
|
|
90
|
+
rows.append(TableRow(
|
|
91
|
+
cells=current_row_cells.copy(),
|
|
92
|
+
conditionals_before=conditionals_before_row.copy(),
|
|
93
|
+
conditionals_after=conditionals_after_row.copy()
|
|
94
|
+
))
|
|
95
|
+
return [], [], [] # Reset for next row
|
|
96
|
+
|
|
97
|
+
# Row not complete yet
|
|
98
|
+
return current_row_cells, conditionals_before_row, conditionals_after_row
|
|
99
|
+
|
|
100
|
+
def _parse_column_count(self, attributes: str) -> int:
|
|
101
|
+
"""
|
|
102
|
+
Parse the cols attribute to determine number of columns.
|
|
103
|
+
|
|
104
|
+
Example: '[cols="1,7a"]' returns 2
|
|
105
|
+
'[cols="1,2,3"]' returns 3
|
|
106
|
+
"""
|
|
107
|
+
import re
|
|
108
|
+
# Match cols="..." or cols='...'
|
|
109
|
+
match = re.search(r'cols=["\']([^"\']+)["\']', attributes)
|
|
110
|
+
if not match:
|
|
111
|
+
return 0 # Unknown column count
|
|
112
|
+
|
|
113
|
+
cols_spec = match.group(1)
|
|
114
|
+
# Count comma-separated values
|
|
115
|
+
# Handle formats like: "1,2", "1a,2a", "1,2,3", etc.
|
|
116
|
+
columns = cols_spec.split(',')
|
|
117
|
+
return len(columns)
|
|
59
118
|
|
|
60
119
|
def find_tables(self, lines: List[str]) -> List[AsciiDocTable]:
|
|
61
120
|
"""Find all tables in the document."""
|
|
@@ -65,16 +124,27 @@ class TableParser:
|
|
|
65
124
|
while i < len(lines):
|
|
66
125
|
# Look for table delimiter
|
|
67
126
|
if self.TABLE_DELIMITER.match(lines[i]):
|
|
68
|
-
# Check
|
|
127
|
+
# Check for attributes and title before the table
|
|
69
128
|
attributes = ""
|
|
129
|
+
title = ""
|
|
70
130
|
start_line = i
|
|
71
131
|
|
|
132
|
+
# Check line before delimiter for attributes [cols="..."]
|
|
72
133
|
if i > 0 and self.TABLE_START.match(lines[i - 1]):
|
|
73
134
|
attributes = lines[i - 1]
|
|
74
135
|
start_line = i - 1
|
|
75
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
|
+
|
|
76
146
|
# Parse table content
|
|
77
|
-
table = self._parse_table(lines, start_line, i)
|
|
147
|
+
table = self._parse_table(lines, start_line, i, title)
|
|
78
148
|
if table:
|
|
79
149
|
tables.append(table)
|
|
80
150
|
i = table.end_line + 1
|
|
@@ -83,11 +153,13 @@ class TableParser:
|
|
|
83
153
|
|
|
84
154
|
return tables
|
|
85
155
|
|
|
86
|
-
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]:
|
|
87
157
|
"""
|
|
88
158
|
Parse a single table starting at the delimiter.
|
|
89
159
|
|
|
90
160
|
AsciiDoc table format:
|
|
161
|
+
.Optional title
|
|
162
|
+
[optional attributes]
|
|
91
163
|
|===
|
|
92
164
|
|Cell1
|
|
93
165
|
|Cell2
|
|
@@ -96,12 +168,28 @@ class TableParser:
|
|
|
96
168
|
|Cell4
|
|
97
169
|
|===
|
|
98
170
|
"""
|
|
171
|
+
# Get attributes and parse column count
|
|
172
|
+
attributes = ""
|
|
173
|
+
if start_line < delimiter_line:
|
|
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]
|
|
183
|
+
|
|
184
|
+
expected_columns = self._parse_column_count(attributes)
|
|
185
|
+
|
|
99
186
|
i = delimiter_line + 1
|
|
100
187
|
rows = []
|
|
101
188
|
current_row_cells = []
|
|
102
189
|
current_cell_lines = []
|
|
103
190
|
conditionals_before_row = []
|
|
104
191
|
conditionals_after_row = []
|
|
192
|
+
in_asciidoc_cell = False # Track if we're in an a| (AsciiDoc) cell
|
|
105
193
|
|
|
106
194
|
while i < len(lines):
|
|
107
195
|
line = lines[i]
|
|
@@ -124,36 +212,40 @@ class TableParser:
|
|
|
124
212
|
conditionals_after=conditionals_after_row.copy()
|
|
125
213
|
))
|
|
126
214
|
|
|
127
|
-
# Get attributes if present
|
|
128
|
-
attributes = ""
|
|
129
|
-
if start_line < delimiter_line:
|
|
130
|
-
attributes = lines[start_line]
|
|
131
|
-
|
|
215
|
+
# Get attributes if present (already extracted above)
|
|
132
216
|
return AsciiDocTable(
|
|
133
217
|
start_line=start_line,
|
|
134
218
|
end_line=i,
|
|
135
219
|
attributes=attributes,
|
|
136
|
-
rows=rows
|
|
220
|
+
rows=rows,
|
|
221
|
+
title=title
|
|
137
222
|
)
|
|
138
223
|
|
|
139
224
|
# Check for conditional directives
|
|
140
225
|
if self.IFDEF_PATTERN.match(line) or self.ENDIF_PATTERN.match(line):
|
|
141
|
-
|
|
226
|
+
# If we're building a cell (current_cell_lines is not empty) OR
|
|
227
|
+
# we're in an AsciiDoc cell, add conditional to cell content
|
|
228
|
+
if current_cell_lines or in_asciidoc_cell:
|
|
229
|
+
# Inside a cell - conditional is part of cell content
|
|
230
|
+
current_cell_lines.append(line)
|
|
231
|
+
elif current_row_cells:
|
|
232
|
+
# Between cells in the same row
|
|
233
|
+
conditionals_after_row.append(line)
|
|
234
|
+
else:
|
|
142
235
|
# Conditional before any cells in this row
|
|
143
236
|
conditionals_before_row.append(line)
|
|
144
|
-
else:
|
|
145
|
-
# Conditional after cells started - treat as part of current context
|
|
146
|
-
if current_cell_lines:
|
|
147
|
-
# Inside a cell
|
|
148
|
-
current_cell_lines.append(line)
|
|
149
|
-
else:
|
|
150
|
-
# Between cells in the same row
|
|
151
|
-
conditionals_after_row.append(line)
|
|
152
237
|
i += 1
|
|
153
238
|
continue
|
|
154
239
|
|
|
155
|
-
# Blank line
|
|
240
|
+
# Blank line handling
|
|
156
241
|
if not line.strip():
|
|
242
|
+
# In AsciiDoc cells (a|), blank lines are part of cell content
|
|
243
|
+
if in_asciidoc_cell:
|
|
244
|
+
current_cell_lines.append(line)
|
|
245
|
+
i += 1
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
# Otherwise, blank line separates rows
|
|
157
249
|
# Save pending cell if exists
|
|
158
250
|
if current_cell_lines:
|
|
159
251
|
current_row_cells.append(TableCell(
|
|
@@ -161,6 +253,7 @@ class TableParser:
|
|
|
161
253
|
conditionals=[]
|
|
162
254
|
))
|
|
163
255
|
current_cell_lines = []
|
|
256
|
+
in_asciidoc_cell = False
|
|
164
257
|
|
|
165
258
|
# Save row if we have cells
|
|
166
259
|
if current_row_cells:
|
|
@@ -176,8 +269,16 @@ class TableParser:
|
|
|
176
269
|
i += 1
|
|
177
270
|
continue
|
|
178
271
|
|
|
179
|
-
# Check for cell separator (|)
|
|
272
|
+
# Check for cell separator (|) or cell type specifier (a|, s|, etc.)
|
|
180
273
|
if self.CELL_SEPARATOR.match(line):
|
|
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
|
|
281
|
+
|
|
181
282
|
# Save previous cell if exists
|
|
182
283
|
if current_cell_lines:
|
|
183
284
|
current_row_cells.append(TableCell(
|
|
@@ -185,9 +286,24 @@ class TableParser:
|
|
|
185
286
|
conditionals=[]
|
|
186
287
|
))
|
|
187
288
|
current_cell_lines = []
|
|
289
|
+
in_asciidoc_cell = False # Reset for next cell
|
|
290
|
+
|
|
291
|
+
# Check if row is complete (have enough cells based on cols attribute)
|
|
292
|
+
current_row_cells, conditionals_before_row, conditionals_after_row = \
|
|
293
|
+
self._finalize_row_if_complete(
|
|
294
|
+
current_row_cells, conditionals_before_row,
|
|
295
|
+
conditionals_after_row, expected_columns, rows
|
|
296
|
+
)
|
|
188
297
|
|
|
189
|
-
#
|
|
190
|
-
|
|
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] == '|':
|
|
301
|
+
# Track if this is an AsciiDoc cell (a|)
|
|
302
|
+
if cell_content[0] == 'a':
|
|
303
|
+
in_asciidoc_cell = True
|
|
304
|
+
cell_content = cell_content[2:] # Remove type specifier and |
|
|
305
|
+
|
|
306
|
+
cell_content = cell_content.strip()
|
|
191
307
|
|
|
192
308
|
# Check if there are multiple cells on the same line (e.g., |Cell1 |Cell2 |Cell3)
|
|
193
309
|
if '|' in cell_content:
|
|
@@ -220,6 +336,39 @@ class TableParser:
|
|
|
220
336
|
i += 1
|
|
221
337
|
continue
|
|
222
338
|
|
|
339
|
+
# Check for cell type specifier on its own line (e.g., "a|", "s|", "h|")
|
|
340
|
+
# This is actually a cell SEPARATOR with type specifier
|
|
341
|
+
# Example:
|
|
342
|
+
# |<1> ← Cell 1
|
|
343
|
+
# a| ← Start cell 2 with type 'a' (AsciiDoc)
|
|
344
|
+
# content... ← Cell 2 content
|
|
345
|
+
stripped_line = line.strip()
|
|
346
|
+
if (len(stripped_line) == 2 and
|
|
347
|
+
stripped_line[0] in 'ashdmev' and
|
|
348
|
+
stripped_line[1] == '|' and
|
|
349
|
+
(current_cell_lines or current_row_cells)):
|
|
350
|
+
# Save previous cell if we have one
|
|
351
|
+
if current_cell_lines:
|
|
352
|
+
current_row_cells.append(TableCell(
|
|
353
|
+
content=current_cell_lines.copy(),
|
|
354
|
+
conditionals=[]
|
|
355
|
+
))
|
|
356
|
+
current_cell_lines = []
|
|
357
|
+
|
|
358
|
+
# Check if row is complete
|
|
359
|
+
current_row_cells, conditionals_before_row, conditionals_after_row = \
|
|
360
|
+
self._finalize_row_if_complete(
|
|
361
|
+
current_row_cells, conditionals_before_row,
|
|
362
|
+
conditionals_after_row, expected_columns, rows
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
# Set cell type for the NEW cell we're starting
|
|
366
|
+
if stripped_line[0] == 'a':
|
|
367
|
+
in_asciidoc_cell = True
|
|
368
|
+
# Start collecting content for the new cell (no content on this line)
|
|
369
|
+
i += 1
|
|
370
|
+
continue
|
|
371
|
+
|
|
223
372
|
# Regular content line (continuation of current cell)
|
|
224
373
|
if current_cell_lines or current_row_cells:
|
|
225
374
|
current_cell_lines.append(line)
|
|
@@ -233,6 +382,8 @@ class TableParser:
|
|
|
233
382
|
"""
|
|
234
383
|
Determine if a table is a callout explanation table.
|
|
235
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.
|
|
236
387
|
"""
|
|
237
388
|
if not table.rows:
|
|
238
389
|
return False
|
|
@@ -241,15 +392,23 @@ class TableParser:
|
|
|
241
392
|
if not all(len(row.cells) == 2 for row in table.rows):
|
|
242
393
|
return False
|
|
243
394
|
|
|
244
|
-
#
|
|
245
|
-
|
|
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:
|
|
246
404
|
first_cell = row.cells[0]
|
|
247
405
|
if not first_cell.content:
|
|
248
406
|
return False
|
|
249
407
|
|
|
250
|
-
# First line of first cell should be a callout number
|
|
408
|
+
# First line of first cell should be a callout number or plain number
|
|
251
409
|
first_line = first_cell.content[0].strip()
|
|
252
|
-
|
|
410
|
+
is_match, _ = self._is_callout_or_number(first_line)
|
|
411
|
+
if not is_match:
|
|
253
412
|
return False
|
|
254
413
|
|
|
255
414
|
return True
|
|
@@ -258,23 +417,36 @@ class TableParser:
|
|
|
258
417
|
"""
|
|
259
418
|
Check if table has a header row.
|
|
260
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
|
|
261
424
|
"""
|
|
262
425
|
if not table.rows:
|
|
263
426
|
return False
|
|
264
427
|
|
|
265
428
|
first_row = table.rows[0]
|
|
266
|
-
if not first_row.cells:
|
|
429
|
+
if not first_row.cells or len(first_row.cells) < 2:
|
|
267
430
|
return False
|
|
268
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
|
+
|
|
269
440
|
# Collect text from all cells in first row
|
|
270
441
|
header_text = ' '.join(
|
|
271
442
|
cell.content[0] if cell.content else ''
|
|
272
443
|
for cell in first_row.cells
|
|
273
444
|
).lower()
|
|
274
445
|
|
|
275
|
-
# Check for common header keywords
|
|
446
|
+
# Check for common header keywords (as whole words)
|
|
276
447
|
header_keywords = ['item', 'description', 'value', 'column', 'parameter', 'field', 'name']
|
|
277
|
-
|
|
448
|
+
import re
|
|
449
|
+
return any(re.search(r'\b' + re.escape(keyword) + r'\b', header_text) for keyword in header_keywords)
|
|
278
450
|
|
|
279
451
|
def is_3column_callout_table(self, table: AsciiDocTable) -> bool:
|
|
280
452
|
"""
|
|
@@ -300,19 +472,52 @@ class TableParser:
|
|
|
300
472
|
if not all(len(row.cells) == 3 for row in data_rows):
|
|
301
473
|
return False
|
|
302
474
|
|
|
303
|
-
# 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>...)
|
|
304
476
|
for row in data_rows:
|
|
305
477
|
first_cell = row.cells[0]
|
|
306
478
|
if not first_cell.content:
|
|
307
479
|
return False
|
|
308
480
|
|
|
309
|
-
# First line of first cell should be a number
|
|
481
|
+
# First line of first cell should be a callout number or plain number
|
|
310
482
|
first_line = first_cell.content[0].strip()
|
|
311
|
-
|
|
483
|
+
is_match, _ = self._is_callout_or_number(first_line)
|
|
484
|
+
if not is_match:
|
|
312
485
|
return False
|
|
313
486
|
|
|
314
487
|
return True
|
|
315
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
|
+
|
|
316
521
|
def extract_callout_explanations_from_table(self, table: AsciiDocTable) -> Dict[int, Tuple[List[str], List[str]]]:
|
|
317
522
|
"""
|
|
318
523
|
Extract callout explanations from a table.
|
|
@@ -320,43 +525,46 @@ class TableParser:
|
|
|
320
525
|
|
|
321
526
|
The conditionals list includes any ifdef/ifndef/endif statements that should
|
|
322
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.
|
|
323
534
|
"""
|
|
324
535
|
explanations = {}
|
|
325
536
|
|
|
326
|
-
|
|
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:
|
|
327
542
|
if len(row.cells) != 2:
|
|
328
543
|
continue
|
|
329
544
|
|
|
330
545
|
callout_cell = row.cells[0]
|
|
331
546
|
explanation_cell = row.cells[1]
|
|
332
547
|
|
|
333
|
-
# Extract callout number
|
|
548
|
+
# Extract callout number (supports both <1> and 1 formats)
|
|
334
549
|
first_line = callout_cell.content[0].strip()
|
|
335
|
-
|
|
336
|
-
if not
|
|
550
|
+
is_match, callout_num = self._is_callout_or_number(first_line)
|
|
551
|
+
if not is_match:
|
|
337
552
|
continue
|
|
338
553
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
# Collect explanation lines
|
|
554
|
+
# Collect explanation lines, preserving blank lines and conditionals inline
|
|
555
|
+
# Blank lines will need to become continuation markers (+) in definition lists
|
|
342
556
|
explanation_lines = []
|
|
343
557
|
for line in explanation_cell.content:
|
|
344
|
-
#
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
# Collect all conditionals for this row
|
|
349
|
-
all_conditionals = []
|
|
350
|
-
all_conditionals.extend(row.conditionals_before)
|
|
351
|
-
|
|
352
|
-
# Extract conditionals from explanation cell
|
|
353
|
-
for line in explanation_cell.content:
|
|
354
|
-
if self.IFDEF_PATTERN.match(line) or self.ENDIF_PATTERN.match(line):
|
|
355
|
-
all_conditionals.append(line)
|
|
558
|
+
# Preserve ALL lines including conditionals and blank lines
|
|
559
|
+
# Empty lines will be marked as '' which signals need for continuation marker
|
|
560
|
+
explanation_lines.append(line)
|
|
356
561
|
|
|
357
|
-
|
|
562
|
+
# Collect conditionals that appear before/after the row
|
|
563
|
+
row_conditionals = []
|
|
564
|
+
row_conditionals.extend(row.conditionals_before)
|
|
565
|
+
row_conditionals.extend(row.conditionals_after)
|
|
358
566
|
|
|
359
|
-
explanations[callout_num] = (explanation_lines,
|
|
567
|
+
explanations[callout_num] = (explanation_lines, row_conditionals)
|
|
360
568
|
|
|
361
569
|
return explanations
|
|
362
570
|
|
|
@@ -366,12 +574,14 @@ class TableParser:
|
|
|
366
574
|
Returns dict mapping callout number to tuple of (value_lines, description_lines, conditionals).
|
|
367
575
|
|
|
368
576
|
Format: Item | Value | Description
|
|
369
|
-
- Item: Number (1, 2, 3...) corresponding to callout number
|
|
577
|
+
- Item: Number (1, 2, 3...) or callout (<1>, <2>...) corresponding to callout number
|
|
370
578
|
- Value: The code/value being explained
|
|
371
579
|
- Description: Explanation text
|
|
372
580
|
|
|
373
581
|
The conditionals list includes any ifdef/ifndef/endif statements that should
|
|
374
582
|
be preserved when converting the table to other formats.
|
|
583
|
+
|
|
584
|
+
Accepts both callout format (<1>) and plain numbers (1).
|
|
375
585
|
"""
|
|
376
586
|
explanations = {}
|
|
377
587
|
|
|
@@ -387,47 +597,31 @@ class TableParser:
|
|
|
387
597
|
value_cell = row.cells[1]
|
|
388
598
|
desc_cell = row.cells[2]
|
|
389
599
|
|
|
390
|
-
# Extract item number (maps to callout number)
|
|
600
|
+
# Extract item number (maps to callout number) - supports both <1> and 1 formats
|
|
391
601
|
if not item_cell.content:
|
|
392
602
|
continue
|
|
393
603
|
|
|
394
604
|
item_num_str = item_cell.content[0].strip()
|
|
395
|
-
|
|
605
|
+
is_match, callout_num = self._is_callout_or_number(item_num_str)
|
|
606
|
+
if not is_match:
|
|
396
607
|
continue
|
|
397
608
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
# Collect value lines (column 2)
|
|
609
|
+
# Collect value lines (column 2), preserving all content including conditionals
|
|
401
610
|
value_lines = []
|
|
402
611
|
for line in value_cell.content:
|
|
403
|
-
|
|
404
|
-
if not (self.IFDEF_PATTERN.match(line) or self.ENDIF_PATTERN.match(line)):
|
|
405
|
-
value_lines.append(line)
|
|
612
|
+
value_lines.append(line)
|
|
406
613
|
|
|
407
|
-
# Collect description lines (column 3)
|
|
614
|
+
# Collect description lines (column 3), preserving all content including conditionals
|
|
408
615
|
description_lines = []
|
|
409
616
|
for line in desc_cell.content:
|
|
410
|
-
|
|
411
|
-
if not (self.IFDEF_PATTERN.match(line) or self.ENDIF_PATTERN.match(line)):
|
|
412
|
-
description_lines.append(line)
|
|
413
|
-
|
|
414
|
-
# Collect all conditionals for this row
|
|
415
|
-
all_conditionals = []
|
|
416
|
-
all_conditionals.extend(row.conditionals_before)
|
|
417
|
-
|
|
418
|
-
# Extract conditionals from value cell
|
|
419
|
-
for line in value_cell.content:
|
|
420
|
-
if self.IFDEF_PATTERN.match(line) or self.ENDIF_PATTERN.match(line):
|
|
421
|
-
all_conditionals.append(line)
|
|
422
|
-
|
|
423
|
-
# Extract conditionals from description cell
|
|
424
|
-
for line in desc_cell.content:
|
|
425
|
-
if self.IFDEF_PATTERN.match(line) or self.ENDIF_PATTERN.match(line):
|
|
426
|
-
all_conditionals.append(line)
|
|
617
|
+
description_lines.append(line)
|
|
427
618
|
|
|
428
|
-
|
|
619
|
+
# Collect conditionals that appear before/after the row
|
|
620
|
+
row_conditionals = []
|
|
621
|
+
row_conditionals.extend(row.conditionals_before)
|
|
622
|
+
row_conditionals.extend(row.conditionals_after)
|
|
429
623
|
|
|
430
|
-
explanations[callout_num] = (value_lines, description_lines,
|
|
624
|
+
explanations[callout_num] = (value_lines, description_lines, row_conditionals)
|
|
431
625
|
|
|
432
626
|
return explanations
|
|
433
627
|
|
|
@@ -442,8 +636,12 @@ class TableParser:
|
|
|
442
636
|
Returns:
|
|
443
637
|
AsciiDocTable if a callout table is found, None otherwise
|
|
444
638
|
"""
|
|
445
|
-
# Skip
|
|
639
|
+
# Skip the closing delimiter of the code block (----, ...., etc.)
|
|
446
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
|
|
447
645
|
while i < len(lines) and (not lines[i].strip() or lines[i].strip() == '+'):
|
|
448
646
|
i += 1
|
|
449
647
|
|
|
@@ -465,12 +663,26 @@ class TableParser:
|
|
|
465
663
|
|
|
466
664
|
# Check for table delimiter
|
|
467
665
|
if self.TABLE_DELIMITER.match(line):
|
|
468
|
-
# Found a table,
|
|
666
|
+
# Found a table, extract attributes and title
|
|
667
|
+
attributes = ""
|
|
668
|
+
title = ""
|
|
469
669
|
start_line = j
|
|
670
|
+
|
|
671
|
+
# Check line before delimiter for attributes [cols="..."]
|
|
470
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()
|
|
471
683
|
start_line = j - 1
|
|
472
684
|
|
|
473
|
-
table = self._parse_table(lines, start_line, j)
|
|
685
|
+
table = self._parse_table(lines, start_line, j, title)
|
|
474
686
|
if table and (self.is_callout_table(table) or self.is_3column_callout_table(table)):
|
|
475
687
|
return table
|
|
476
688
|
|
convert_callouts_interactive.py
CHANGED
|
@@ -307,7 +307,20 @@ class InteractiveCalloutConverter:
|
|
|
307
307
|
explanations, explanation_end = self.detector.extract_callout_explanations(new_lines, block.end_line)
|
|
308
308
|
|
|
309
309
|
if not explanations:
|
|
310
|
-
|
|
310
|
+
# Get callout numbers for warning message
|
|
311
|
+
all_callout_nums = []
|
|
312
|
+
for group in callout_groups:
|
|
313
|
+
all_callout_nums.extend(group.callout_numbers)
|
|
314
|
+
|
|
315
|
+
warning_msg = (
|
|
316
|
+
f"WARNING: {input_file.name} line {block.start_line + 1}: "
|
|
317
|
+
f"Code block has callouts {sorted(set(all_callout_nums))} but no explanations found after it. "
|
|
318
|
+
f"This may indicate: explanations are shared with another code block, "
|
|
319
|
+
f"explanations are in an unexpected location, or documentation error (missing explanations). "
|
|
320
|
+
f"Consider reviewing this block manually."
|
|
321
|
+
)
|
|
322
|
+
print_colored(warning_msg, Colors.YELLOW)
|
|
323
|
+
self.warnings.append(warning_msg)
|
|
311
324
|
continue
|
|
312
325
|
|
|
313
326
|
# Validate
|
|
@@ -332,9 +345,9 @@ class InteractiveCalloutConverter:
|
|
|
332
345
|
else:
|
|
333
346
|
converted_content = self.detector.remove_callouts_from_code(block.content)
|
|
334
347
|
if format_choice == 'bullets':
|
|
335
|
-
output_list = BulletListConverter.convert(callout_groups, explanations)
|
|
348
|
+
output_list = BulletListConverter.convert(callout_groups, explanations, self.detector.last_table_title)
|
|
336
349
|
else: # deflist
|
|
337
|
-
output_list = DefListConverter.convert(callout_groups, explanations)
|
|
350
|
+
output_list = DefListConverter.convert(callout_groups, explanations, self.detector.last_table_title)
|
|
338
351
|
|
|
339
352
|
# Replace in document
|
|
340
353
|
has_source_prefix = self.detector.CODE_BLOCK_START.match(new_lines[block.start_line])
|
|
@@ -521,6 +534,8 @@ Examples:
|
|
|
521
534
|
print_colored(f"\n⚠ {len(converter.warnings)} Warning(s):", Colors.YELLOW)
|
|
522
535
|
for warning in converter.warnings:
|
|
523
536
|
print_colored(f" {warning}", Colors.YELLOW)
|
|
537
|
+
print()
|
|
538
|
+
print_colored("Suggestion: Fix the callout mismatches in the files above and rerun this command.", Colors.YELLOW)
|
|
524
539
|
|
|
525
540
|
if args.dry_run and files_modified > 0:
|
|
526
541
|
print_colored("\nDRY RUN - No files were modified", Colors.YELLOW)
|