rolfedh-doc-utils 0.1.25__py3-none-any.whl → 0.1.27__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/detector.py +93 -0
- callout_lib/table_parser.py +581 -0
- convert_callouts_to_deflist.py +5 -4
- {rolfedh_doc_utils-0.1.25.dist-info → rolfedh_doc_utils-0.1.27.dist-info}/METADATA +1 -1
- {rolfedh_doc_utils-0.1.25.dist-info → rolfedh_doc_utils-0.1.27.dist-info}/RECORD +9 -8
- {rolfedh_doc_utils-0.1.25.dist-info → rolfedh_doc_utils-0.1.27.dist-info}/WHEEL +0 -0
- {rolfedh_doc_utils-0.1.25.dist-info → rolfedh_doc_utils-0.1.27.dist-info}/entry_points.txt +0 -0
- {rolfedh_doc_utils-0.1.25.dist-info → rolfedh_doc_utils-0.1.27.dist-info}/licenses/LICENSE +0 -0
- {rolfedh_doc_utils-0.1.25.dist-info → rolfedh_doc_utils-0.1.27.dist-info}/top_level.txt +0 -0
callout_lib/detector.py
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
Callout Detection Module
|
|
3
3
|
|
|
4
4
|
Detects code blocks with callouts and extracts callout information from AsciiDoc files.
|
|
5
|
+
Supports both list-format and table-format callout explanations.
|
|
5
6
|
"""
|
|
6
7
|
|
|
7
8
|
import re
|
|
8
9
|
from typing import List, Dict, Tuple, Optional
|
|
9
10
|
from dataclasses import dataclass
|
|
11
|
+
from .table_parser import TableParser
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
@dataclass
|
|
@@ -50,6 +52,10 @@ class CalloutDetector:
|
|
|
50
52
|
# Excludes heredoc syntax (<<) and comparison operators
|
|
51
53
|
USER_VALUE_PATTERN = re.compile(r'(?<!<)<([a-zA-Z][^>]*)>')
|
|
52
54
|
|
|
55
|
+
def __init__(self):
|
|
56
|
+
"""Initialize detector with table parser."""
|
|
57
|
+
self.table_parser = TableParser()
|
|
58
|
+
|
|
53
59
|
def find_code_blocks(self, lines: List[str]) -> List[CodeBlock]:
|
|
54
60
|
"""Find all code blocks in the document."""
|
|
55
61
|
blocks = []
|
|
@@ -151,8 +157,95 @@ class CalloutDetector:
|
|
|
151
157
|
def extract_callout_explanations(self, lines: List[str], start_line: int) -> Tuple[Dict[int, Callout], int]:
|
|
152
158
|
"""
|
|
153
159
|
Extract callout explanations following a code block.
|
|
160
|
+
Supports list-format (<1> text), 2-column table, and 3-column table formats.
|
|
154
161
|
Returns dict of callouts and the line number where explanations end.
|
|
155
162
|
"""
|
|
163
|
+
# First, try to find a table-format callout explanation
|
|
164
|
+
table = self.table_parser.find_callout_table_after_code_block(lines, start_line)
|
|
165
|
+
if table:
|
|
166
|
+
# Check if it's a 3-column table (Item | Value | Description)
|
|
167
|
+
if self.table_parser.is_3column_callout_table(table):
|
|
168
|
+
return self._extract_from_3column_table(table)
|
|
169
|
+
# Check if it's a 2-column table (<callout> | explanation)
|
|
170
|
+
elif self.table_parser.is_callout_table(table):
|
|
171
|
+
return self._extract_from_table(table)
|
|
172
|
+
|
|
173
|
+
# Fall back to list-format extraction
|
|
174
|
+
return self._extract_from_list(lines, start_line)
|
|
175
|
+
|
|
176
|
+
def _extract_from_table(self, table) -> Tuple[Dict[int, Callout], int]:
|
|
177
|
+
"""Extract callout explanations from a table format."""
|
|
178
|
+
explanations = {}
|
|
179
|
+
table_data = self.table_parser.extract_callout_explanations_from_table(table)
|
|
180
|
+
|
|
181
|
+
for callout_num, (explanation_lines, conditionals) in table_data.items():
|
|
182
|
+
# Combine explanation lines with conditionals preserved
|
|
183
|
+
all_lines = []
|
|
184
|
+
for line in explanation_lines:
|
|
185
|
+
all_lines.append(line)
|
|
186
|
+
|
|
187
|
+
# Add conditionals as separate lines (they'll be preserved in output)
|
|
188
|
+
all_lines.extend(conditionals)
|
|
189
|
+
|
|
190
|
+
# Check if marked as optional
|
|
191
|
+
is_optional = False
|
|
192
|
+
if all_lines and (all_lines[0].lower().startswith('optional.') or
|
|
193
|
+
all_lines[0].lower().startswith('optional:')):
|
|
194
|
+
is_optional = True
|
|
195
|
+
all_lines[0] = all_lines[0][9:].strip()
|
|
196
|
+
elif all_lines and ('(Optional)' in all_lines[0] or '(optional)' in all_lines[0]):
|
|
197
|
+
is_optional = True
|
|
198
|
+
all_lines[0] = re.sub(r'\s*\(optional\)\s*', ' ', all_lines[0], flags=re.IGNORECASE).strip()
|
|
199
|
+
|
|
200
|
+
explanations[callout_num] = Callout(callout_num, all_lines, is_optional)
|
|
201
|
+
|
|
202
|
+
return explanations, table.end_line
|
|
203
|
+
|
|
204
|
+
def _extract_from_3column_table(self, table) -> Tuple[Dict[int, Callout], int]:
|
|
205
|
+
"""
|
|
206
|
+
Extract callout explanations from a 3-column table format.
|
|
207
|
+
Format: Item (number) | Value | Description
|
|
208
|
+
"""
|
|
209
|
+
explanations = {}
|
|
210
|
+
table_data = self.table_parser.extract_3column_callout_explanations(table)
|
|
211
|
+
|
|
212
|
+
for callout_num, (value_lines, description_lines, conditionals) in table_data.items():
|
|
213
|
+
# Combine value and description into explanation lines
|
|
214
|
+
# Strategy: Include value as context, then description
|
|
215
|
+
all_lines = []
|
|
216
|
+
|
|
217
|
+
# Add value lines with context
|
|
218
|
+
if value_lines:
|
|
219
|
+
# Format: "Refers to `value`. Description..."
|
|
220
|
+
value_text = value_lines[0] if value_lines else ""
|
|
221
|
+
# If value is code-like (contains backticks or special chars), keep it formatted
|
|
222
|
+
if value_text:
|
|
223
|
+
all_lines.append(f"Refers to {value_text}.")
|
|
224
|
+
|
|
225
|
+
# Add additional value lines if multi-line
|
|
226
|
+
for line in value_lines[1:]:
|
|
227
|
+
all_lines.append(line)
|
|
228
|
+
|
|
229
|
+
# Add description lines
|
|
230
|
+
all_lines.extend(description_lines)
|
|
231
|
+
|
|
232
|
+
# Add conditionals as separate lines (they'll be preserved in output)
|
|
233
|
+
all_lines.extend(conditionals)
|
|
234
|
+
|
|
235
|
+
# Check if marked as optional
|
|
236
|
+
is_optional = False
|
|
237
|
+
if all_lines and (all_lines[0].lower().startswith('optional.') or
|
|
238
|
+
all_lines[0].lower().startswith('optional:') or
|
|
239
|
+
'optional' in all_lines[0].lower()[:50]): # Check first 50 chars
|
|
240
|
+
is_optional = True
|
|
241
|
+
# Don't remove "optional" text - it's part of the description
|
|
242
|
+
|
|
243
|
+
explanations[callout_num] = Callout(callout_num, all_lines, is_optional)
|
|
244
|
+
|
|
245
|
+
return explanations, table.end_line
|
|
246
|
+
|
|
247
|
+
def _extract_from_list(self, lines: List[str], start_line: int) -> Tuple[Dict[int, Callout], int]:
|
|
248
|
+
"""Extract callout explanations from list format (<1> text)."""
|
|
156
249
|
explanations = {}
|
|
157
250
|
i = start_line + 1 # Start after the closing delimiter
|
|
158
251
|
|
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AsciiDoc Table Parser Module
|
|
3
|
+
|
|
4
|
+
Parses AsciiDoc tables and extracts structured data. Designed to be reusable
|
|
5
|
+
for various table conversion tasks (not just callout explanations).
|
|
6
|
+
|
|
7
|
+
Handles:
|
|
8
|
+
- Two-column tables with callout numbers and explanations
|
|
9
|
+
- Conditional statements (ifdef, ifndef, endif) within table cells
|
|
10
|
+
- Multi-line table cells
|
|
11
|
+
- Table attributes and formatting
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from typing import List, Dict, Tuple, Optional
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class TableCell:
|
|
21
|
+
"""Represents a single table cell with its content and any conditional blocks."""
|
|
22
|
+
content: List[str] # Lines of content in the cell
|
|
23
|
+
conditionals: List[str] # Any ifdef/ifndef/endif lines associated with this cell
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class TableRow:
|
|
28
|
+
"""Represents a table row with cells."""
|
|
29
|
+
cells: List[TableCell]
|
|
30
|
+
conditionals_before: List[str] # Conditional statements before this row
|
|
31
|
+
conditionals_after: List[str] # Conditional statements after this row
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class AsciiDocTable:
|
|
36
|
+
"""Represents a complete AsciiDoc table."""
|
|
37
|
+
start_line: int
|
|
38
|
+
end_line: int
|
|
39
|
+
attributes: str # Table attributes like [cols="1,3"]
|
|
40
|
+
rows: List[TableRow]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TableParser:
|
|
44
|
+
"""Parses AsciiDoc tables and extracts structured data."""
|
|
45
|
+
|
|
46
|
+
# Pattern for table start delimiter with optional attributes
|
|
47
|
+
TABLE_START = re.compile(r'^\[.*?\]$')
|
|
48
|
+
TABLE_DELIMITER = re.compile(r'^\|===\s*$')
|
|
49
|
+
|
|
50
|
+
# Pattern for table cell separator
|
|
51
|
+
CELL_SEPARATOR = re.compile(r'^\|')
|
|
52
|
+
|
|
53
|
+
# Pattern for conditional directives
|
|
54
|
+
IFDEF_PATTERN = re.compile(r'^(ifdef::|ifndef::).+\[\]\s*$')
|
|
55
|
+
ENDIF_PATTERN = re.compile(r'^endif::\[\]\s*$')
|
|
56
|
+
|
|
57
|
+
# Pattern for callout number (used for callout table detection)
|
|
58
|
+
CALLOUT_NUMBER = re.compile(r'^<(\d+)>\s*$')
|
|
59
|
+
|
|
60
|
+
def find_tables(self, lines: List[str]) -> List[AsciiDocTable]:
|
|
61
|
+
"""Find all tables in the document."""
|
|
62
|
+
tables = []
|
|
63
|
+
i = 0
|
|
64
|
+
|
|
65
|
+
while i < len(lines):
|
|
66
|
+
# Look for table delimiter
|
|
67
|
+
if self.TABLE_DELIMITER.match(lines[i]):
|
|
68
|
+
# Check if there are attributes on the line before
|
|
69
|
+
attributes = ""
|
|
70
|
+
start_line = i
|
|
71
|
+
|
|
72
|
+
if i > 0 and self.TABLE_START.match(lines[i - 1]):
|
|
73
|
+
attributes = lines[i - 1]
|
|
74
|
+
start_line = i - 1
|
|
75
|
+
|
|
76
|
+
# Parse table content
|
|
77
|
+
table = self._parse_table(lines, start_line, i)
|
|
78
|
+
if table:
|
|
79
|
+
tables.append(table)
|
|
80
|
+
i = table.end_line + 1
|
|
81
|
+
continue
|
|
82
|
+
i += 1
|
|
83
|
+
|
|
84
|
+
return tables
|
|
85
|
+
|
|
86
|
+
def _parse_table(self, lines: List[str], start_line: int, delimiter_line: int) -> Optional[AsciiDocTable]:
|
|
87
|
+
"""
|
|
88
|
+
Parse a single table starting at the delimiter.
|
|
89
|
+
|
|
90
|
+
AsciiDoc table format:
|
|
91
|
+
|===
|
|
92
|
+
|Cell1
|
|
93
|
+
|Cell2
|
|
94
|
+
(blank line separates rows)
|
|
95
|
+
|Cell3
|
|
96
|
+
|Cell4
|
|
97
|
+
|===
|
|
98
|
+
"""
|
|
99
|
+
i = delimiter_line + 1
|
|
100
|
+
rows = []
|
|
101
|
+
current_row_cells = []
|
|
102
|
+
current_cell_lines = []
|
|
103
|
+
conditionals_before_row = []
|
|
104
|
+
conditionals_after_row = []
|
|
105
|
+
|
|
106
|
+
while i < len(lines):
|
|
107
|
+
line = lines[i]
|
|
108
|
+
|
|
109
|
+
# Check for table end
|
|
110
|
+
if self.TABLE_DELIMITER.match(line):
|
|
111
|
+
# Save any pending cell
|
|
112
|
+
if current_cell_lines:
|
|
113
|
+
current_row_cells.append(TableCell(
|
|
114
|
+
content=current_cell_lines.copy(),
|
|
115
|
+
conditionals=[]
|
|
116
|
+
))
|
|
117
|
+
current_cell_lines = []
|
|
118
|
+
|
|
119
|
+
# Save any pending row
|
|
120
|
+
if current_row_cells:
|
|
121
|
+
rows.append(TableRow(
|
|
122
|
+
cells=current_row_cells.copy(),
|
|
123
|
+
conditionals_before=conditionals_before_row.copy(),
|
|
124
|
+
conditionals_after=conditionals_after_row.copy()
|
|
125
|
+
))
|
|
126
|
+
|
|
127
|
+
# Get attributes if present
|
|
128
|
+
attributes = ""
|
|
129
|
+
if start_line < delimiter_line:
|
|
130
|
+
attributes = lines[start_line]
|
|
131
|
+
|
|
132
|
+
return AsciiDocTable(
|
|
133
|
+
start_line=start_line,
|
|
134
|
+
end_line=i,
|
|
135
|
+
attributes=attributes,
|
|
136
|
+
rows=rows
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Check for conditional directives
|
|
140
|
+
if self.IFDEF_PATTERN.match(line) or self.ENDIF_PATTERN.match(line):
|
|
141
|
+
if not current_row_cells:
|
|
142
|
+
# Conditional before any cells in this row
|
|
143
|
+
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
|
+
i += 1
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
# Blank line separates rows
|
|
156
|
+
if not line.strip():
|
|
157
|
+
# Save pending cell if exists
|
|
158
|
+
if current_cell_lines:
|
|
159
|
+
current_row_cells.append(TableCell(
|
|
160
|
+
content=current_cell_lines.copy(),
|
|
161
|
+
conditionals=[]
|
|
162
|
+
))
|
|
163
|
+
current_cell_lines = []
|
|
164
|
+
|
|
165
|
+
# Save row if we have cells
|
|
166
|
+
if current_row_cells:
|
|
167
|
+
rows.append(TableRow(
|
|
168
|
+
cells=current_row_cells.copy(),
|
|
169
|
+
conditionals_before=conditionals_before_row.copy(),
|
|
170
|
+
conditionals_after=conditionals_after_row.copy()
|
|
171
|
+
))
|
|
172
|
+
current_row_cells = []
|
|
173
|
+
conditionals_before_row = []
|
|
174
|
+
conditionals_after_row = []
|
|
175
|
+
|
|
176
|
+
i += 1
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
# Check for cell separator (|)
|
|
180
|
+
if self.CELL_SEPARATOR.match(line):
|
|
181
|
+
# Save previous cell if exists
|
|
182
|
+
if current_cell_lines:
|
|
183
|
+
current_row_cells.append(TableCell(
|
|
184
|
+
content=current_cell_lines.copy(),
|
|
185
|
+
conditionals=[]
|
|
186
|
+
))
|
|
187
|
+
current_cell_lines = []
|
|
188
|
+
|
|
189
|
+
# Extract cell content from this line (text after |)
|
|
190
|
+
cell_content = line[1:].strip() # Remove leading |
|
|
191
|
+
|
|
192
|
+
# Check if there are multiple cells on the same line (e.g., |Cell1 |Cell2 |Cell3)
|
|
193
|
+
if '|' in cell_content:
|
|
194
|
+
# Split by | to get multiple cells
|
|
195
|
+
parts = cell_content.split('|')
|
|
196
|
+
for part in parts:
|
|
197
|
+
part = part.strip()
|
|
198
|
+
if part: # Skip empty parts
|
|
199
|
+
current_row_cells.append(TableCell(
|
|
200
|
+
content=[part],
|
|
201
|
+
conditionals=[]
|
|
202
|
+
))
|
|
203
|
+
else:
|
|
204
|
+
# Single cell on this line
|
|
205
|
+
if cell_content:
|
|
206
|
+
current_cell_lines.append(cell_content)
|
|
207
|
+
# If empty, just start a new cell with no content yet
|
|
208
|
+
|
|
209
|
+
i += 1
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
# Regular content line (continuation of current cell)
|
|
213
|
+
if current_cell_lines or current_row_cells:
|
|
214
|
+
current_cell_lines.append(line)
|
|
215
|
+
|
|
216
|
+
i += 1
|
|
217
|
+
|
|
218
|
+
# Return None if we didn't find a proper table end
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
def is_callout_table(self, table: AsciiDocTable) -> bool:
|
|
222
|
+
"""
|
|
223
|
+
Determine if a table is a callout explanation table.
|
|
224
|
+
A callout table has two columns: callout number and explanation.
|
|
225
|
+
"""
|
|
226
|
+
if not table.rows:
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
# Check if all rows have exactly 2 cells
|
|
230
|
+
if not all(len(row.cells) == 2 for row in table.rows):
|
|
231
|
+
return False
|
|
232
|
+
|
|
233
|
+
# Check if first cell of each row is a callout number
|
|
234
|
+
for row in table.rows:
|
|
235
|
+
first_cell = row.cells[0]
|
|
236
|
+
if not first_cell.content:
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
# First line of first cell should be a callout number
|
|
240
|
+
first_line = first_cell.content[0].strip()
|
|
241
|
+
if not self.CALLOUT_NUMBER.match(first_line):
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
return True
|
|
245
|
+
|
|
246
|
+
def _has_header_row(self, table: AsciiDocTable) -> bool:
|
|
247
|
+
"""
|
|
248
|
+
Check if table has a header row.
|
|
249
|
+
Common header patterns: "Item", "Value", "Description", "Column", etc.
|
|
250
|
+
"""
|
|
251
|
+
if not table.rows:
|
|
252
|
+
return False
|
|
253
|
+
|
|
254
|
+
first_row = table.rows[0]
|
|
255
|
+
if not first_row.cells:
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
# Collect text from all cells in first row
|
|
259
|
+
header_text = ' '.join(
|
|
260
|
+
cell.content[0] if cell.content else ''
|
|
261
|
+
for cell in first_row.cells
|
|
262
|
+
).lower()
|
|
263
|
+
|
|
264
|
+
# Check for common header keywords
|
|
265
|
+
header_keywords = ['item', 'description', 'value', 'column', 'parameter', 'field', 'name']
|
|
266
|
+
return any(keyword in header_text for keyword in header_keywords)
|
|
267
|
+
|
|
268
|
+
def is_3column_callout_table(self, table: AsciiDocTable) -> bool:
|
|
269
|
+
"""
|
|
270
|
+
Determine if a table is a 3-column callout explanation table.
|
|
271
|
+
Format: Item (number) | Value | Description
|
|
272
|
+
|
|
273
|
+
This format is used in some documentation (e.g., Debezium) where:
|
|
274
|
+
- Column 1: Item number (1, 2, 3...) corresponding to callout numbers
|
|
275
|
+
- Column 2: The value/code being explained
|
|
276
|
+
- Column 3: Description/explanation text
|
|
277
|
+
"""
|
|
278
|
+
if not table.rows:
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
# Determine if there's a header row
|
|
282
|
+
has_header = self._has_header_row(table)
|
|
283
|
+
data_rows = table.rows[1:] if has_header else table.rows
|
|
284
|
+
|
|
285
|
+
if not data_rows:
|
|
286
|
+
return False
|
|
287
|
+
|
|
288
|
+
# Check if all data rows have exactly 3 cells
|
|
289
|
+
if not all(len(row.cells) == 3 for row in data_rows):
|
|
290
|
+
return False
|
|
291
|
+
|
|
292
|
+
# Check if first cell of each data row contains a plain number (1, 2, 3...)
|
|
293
|
+
for row in data_rows:
|
|
294
|
+
first_cell = row.cells[0]
|
|
295
|
+
if not first_cell.content:
|
|
296
|
+
return False
|
|
297
|
+
|
|
298
|
+
# First line of first cell should be a number
|
|
299
|
+
first_line = first_cell.content[0].strip()
|
|
300
|
+
if not first_line.isdigit():
|
|
301
|
+
return False
|
|
302
|
+
|
|
303
|
+
return True
|
|
304
|
+
|
|
305
|
+
def extract_callout_explanations_from_table(self, table: AsciiDocTable) -> Dict[int, Tuple[List[str], List[str]]]:
|
|
306
|
+
"""
|
|
307
|
+
Extract callout explanations from a table.
|
|
308
|
+
Returns dict mapping callout number to tuple of (explanation_lines, conditionals).
|
|
309
|
+
|
|
310
|
+
The conditionals list includes any ifdef/ifndef/endif statements that should
|
|
311
|
+
be preserved when converting the table to other formats.
|
|
312
|
+
"""
|
|
313
|
+
explanations = {}
|
|
314
|
+
|
|
315
|
+
for row in table.rows:
|
|
316
|
+
if len(row.cells) != 2:
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
callout_cell = row.cells[0]
|
|
320
|
+
explanation_cell = row.cells[1]
|
|
321
|
+
|
|
322
|
+
# Extract callout number
|
|
323
|
+
first_line = callout_cell.content[0].strip()
|
|
324
|
+
match = self.CALLOUT_NUMBER.match(first_line)
|
|
325
|
+
if not match:
|
|
326
|
+
continue
|
|
327
|
+
|
|
328
|
+
callout_num = int(match.group(1))
|
|
329
|
+
|
|
330
|
+
# Collect explanation lines
|
|
331
|
+
explanation_lines = []
|
|
332
|
+
for line in explanation_cell.content:
|
|
333
|
+
# Skip conditional directives in explanation (preserve them separately)
|
|
334
|
+
if not (self.IFDEF_PATTERN.match(line) or self.ENDIF_PATTERN.match(line)):
|
|
335
|
+
explanation_lines.append(line)
|
|
336
|
+
|
|
337
|
+
# Collect all conditionals for this row
|
|
338
|
+
all_conditionals = []
|
|
339
|
+
all_conditionals.extend(row.conditionals_before)
|
|
340
|
+
|
|
341
|
+
# Extract conditionals from explanation cell
|
|
342
|
+
for line in explanation_cell.content:
|
|
343
|
+
if self.IFDEF_PATTERN.match(line) or self.ENDIF_PATTERN.match(line):
|
|
344
|
+
all_conditionals.append(line)
|
|
345
|
+
|
|
346
|
+
all_conditionals.extend(row.conditionals_after)
|
|
347
|
+
|
|
348
|
+
explanations[callout_num] = (explanation_lines, all_conditionals)
|
|
349
|
+
|
|
350
|
+
return explanations
|
|
351
|
+
|
|
352
|
+
def extract_3column_callout_explanations(self, table: AsciiDocTable) -> Dict[int, Tuple[List[str], List[str], List[str]]]:
|
|
353
|
+
"""
|
|
354
|
+
Extract callout explanations from a 3-column table.
|
|
355
|
+
Returns dict mapping callout number to tuple of (value_lines, description_lines, conditionals).
|
|
356
|
+
|
|
357
|
+
Format: Item | Value | Description
|
|
358
|
+
- Item: Number (1, 2, 3...) corresponding to callout number
|
|
359
|
+
- Value: The code/value being explained
|
|
360
|
+
- Description: Explanation text
|
|
361
|
+
|
|
362
|
+
The conditionals list includes any ifdef/ifndef/endif statements that should
|
|
363
|
+
be preserved when converting the table to other formats.
|
|
364
|
+
"""
|
|
365
|
+
explanations = {}
|
|
366
|
+
|
|
367
|
+
# Determine if there's a header row and skip it
|
|
368
|
+
has_header = self._has_header_row(table)
|
|
369
|
+
data_rows = table.rows[1:] if has_header else table.rows
|
|
370
|
+
|
|
371
|
+
for row in data_rows:
|
|
372
|
+
if len(row.cells) != 3:
|
|
373
|
+
continue
|
|
374
|
+
|
|
375
|
+
item_cell = row.cells[0]
|
|
376
|
+
value_cell = row.cells[1]
|
|
377
|
+
desc_cell = row.cells[2]
|
|
378
|
+
|
|
379
|
+
# Extract item number (maps to callout number)
|
|
380
|
+
if not item_cell.content:
|
|
381
|
+
continue
|
|
382
|
+
|
|
383
|
+
item_num_str = item_cell.content[0].strip()
|
|
384
|
+
if not item_num_str.isdigit():
|
|
385
|
+
continue
|
|
386
|
+
|
|
387
|
+
callout_num = int(item_num_str)
|
|
388
|
+
|
|
389
|
+
# Collect value lines (column 2)
|
|
390
|
+
value_lines = []
|
|
391
|
+
for line in value_cell.content:
|
|
392
|
+
# Skip conditional directives in value (preserve them separately)
|
|
393
|
+
if not (self.IFDEF_PATTERN.match(line) or self.ENDIF_PATTERN.match(line)):
|
|
394
|
+
value_lines.append(line)
|
|
395
|
+
|
|
396
|
+
# Collect description lines (column 3)
|
|
397
|
+
description_lines = []
|
|
398
|
+
for line in desc_cell.content:
|
|
399
|
+
# Skip conditional directives in description (preserve them separately)
|
|
400
|
+
if not (self.IFDEF_PATTERN.match(line) or self.ENDIF_PATTERN.match(line)):
|
|
401
|
+
description_lines.append(line)
|
|
402
|
+
|
|
403
|
+
# Collect all conditionals for this row
|
|
404
|
+
all_conditionals = []
|
|
405
|
+
all_conditionals.extend(row.conditionals_before)
|
|
406
|
+
|
|
407
|
+
# Extract conditionals from value cell
|
|
408
|
+
for line in value_cell.content:
|
|
409
|
+
if self.IFDEF_PATTERN.match(line) or self.ENDIF_PATTERN.match(line):
|
|
410
|
+
all_conditionals.append(line)
|
|
411
|
+
|
|
412
|
+
# Extract conditionals from description cell
|
|
413
|
+
for line in desc_cell.content:
|
|
414
|
+
if self.IFDEF_PATTERN.match(line) or self.ENDIF_PATTERN.match(line):
|
|
415
|
+
all_conditionals.append(line)
|
|
416
|
+
|
|
417
|
+
all_conditionals.extend(row.conditionals_after)
|
|
418
|
+
|
|
419
|
+
explanations[callout_num] = (value_lines, description_lines, all_conditionals)
|
|
420
|
+
|
|
421
|
+
return explanations
|
|
422
|
+
|
|
423
|
+
def find_callout_table_after_code_block(self, lines: List[str], code_block_end: int) -> Optional[AsciiDocTable]:
|
|
424
|
+
"""
|
|
425
|
+
Find a callout explanation table that appears after a code block.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
lines: All lines in the document
|
|
429
|
+
code_block_end: Line number where the code block ends
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
AsciiDocTable if a callout table is found, None otherwise
|
|
433
|
+
"""
|
|
434
|
+
# Skip blank lines and continuation markers after code block
|
|
435
|
+
i = code_block_end + 1
|
|
436
|
+
while i < len(lines) and (not lines[i].strip() or lines[i].strip() == '+'):
|
|
437
|
+
i += 1
|
|
438
|
+
|
|
439
|
+
# Look for a table starting within the next few lines
|
|
440
|
+
# (allowing for possible text between code block and table)
|
|
441
|
+
search_limit = min(i + 10, len(lines))
|
|
442
|
+
|
|
443
|
+
for j in range(i, search_limit):
|
|
444
|
+
line = lines[j]
|
|
445
|
+
|
|
446
|
+
# If we encounter a list-format callout explanation, stop
|
|
447
|
+
# (list format takes precedence over table format further away)
|
|
448
|
+
if self.CALLOUT_NUMBER.match(line.strip()):
|
|
449
|
+
return None
|
|
450
|
+
|
|
451
|
+
# If we encounter another code block start, stop
|
|
452
|
+
if line.strip() in ['----', '....'] or line.strip().startswith('[source'):
|
|
453
|
+
return None
|
|
454
|
+
|
|
455
|
+
# Check for table delimiter
|
|
456
|
+
if self.TABLE_DELIMITER.match(line):
|
|
457
|
+
# Found a table, parse it
|
|
458
|
+
start_line = j
|
|
459
|
+
if j > 0 and self.TABLE_START.match(lines[j - 1]):
|
|
460
|
+
start_line = j - 1
|
|
461
|
+
|
|
462
|
+
table = self._parse_table(lines, start_line, j)
|
|
463
|
+
if table and (self.is_callout_table(table) or self.is_3column_callout_table(table)):
|
|
464
|
+
return table
|
|
465
|
+
|
|
466
|
+
# If we found a table but it's not a callout table, stop searching
|
|
467
|
+
break
|
|
468
|
+
|
|
469
|
+
return None
|
|
470
|
+
|
|
471
|
+
def convert_table_to_deflist(self, table: AsciiDocTable, preserve_conditionals: bool = True) -> List[str]:
|
|
472
|
+
"""
|
|
473
|
+
Convert a two-column table to an AsciiDoc definition list.
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
table: The table to convert
|
|
477
|
+
preserve_conditionals: Whether to preserve ifdef/ifndef/endif statements
|
|
478
|
+
|
|
479
|
+
Returns:
|
|
480
|
+
List of lines representing the definition list
|
|
481
|
+
"""
|
|
482
|
+
output = []
|
|
483
|
+
|
|
484
|
+
for row in table.rows:
|
|
485
|
+
if len(row.cells) != 2:
|
|
486
|
+
continue
|
|
487
|
+
|
|
488
|
+
# Add conditionals before row
|
|
489
|
+
if preserve_conditionals and row.conditionals_before:
|
|
490
|
+
output.extend(row.conditionals_before)
|
|
491
|
+
|
|
492
|
+
# First cell is the term
|
|
493
|
+
term_lines = row.cells[0].content
|
|
494
|
+
if term_lines:
|
|
495
|
+
output.append(term_lines[0])
|
|
496
|
+
|
|
497
|
+
# Second cell is the definition
|
|
498
|
+
definition_lines = row.cells[1].content
|
|
499
|
+
if definition_lines:
|
|
500
|
+
# Filter out conditionals if needed
|
|
501
|
+
if preserve_conditionals:
|
|
502
|
+
for line in definition_lines:
|
|
503
|
+
if self.IFDEF_PATTERN.match(line) or self.ENDIF_PATTERN.match(line):
|
|
504
|
+
output.append(line)
|
|
505
|
+
else:
|
|
506
|
+
output.append(f" {line}")
|
|
507
|
+
else:
|
|
508
|
+
for line in definition_lines:
|
|
509
|
+
if not (self.IFDEF_PATTERN.match(line) or self.ENDIF_PATTERN.match(line)):
|
|
510
|
+
output.append(f" {line}")
|
|
511
|
+
|
|
512
|
+
# Add conditionals after row
|
|
513
|
+
if preserve_conditionals and row.conditionals_after:
|
|
514
|
+
output.extend(row.conditionals_after)
|
|
515
|
+
|
|
516
|
+
# Add blank line between entries
|
|
517
|
+
output.append("")
|
|
518
|
+
|
|
519
|
+
# Remove trailing blank line
|
|
520
|
+
if output and not output[-1].strip():
|
|
521
|
+
output.pop()
|
|
522
|
+
|
|
523
|
+
return output
|
|
524
|
+
|
|
525
|
+
def convert_table_to_bullets(self, table: AsciiDocTable, preserve_conditionals: bool = True) -> List[str]:
|
|
526
|
+
"""
|
|
527
|
+
Convert a two-column table to a bulleted list.
|
|
528
|
+
|
|
529
|
+
Args:
|
|
530
|
+
table: The table to convert
|
|
531
|
+
preserve_conditionals: Whether to preserve ifdef/ifndef/endif statements
|
|
532
|
+
|
|
533
|
+
Returns:
|
|
534
|
+
List of lines representing the bulleted list
|
|
535
|
+
"""
|
|
536
|
+
output = []
|
|
537
|
+
|
|
538
|
+
for row in table.rows:
|
|
539
|
+
if len(row.cells) != 2:
|
|
540
|
+
continue
|
|
541
|
+
|
|
542
|
+
# Add conditionals before row
|
|
543
|
+
if preserve_conditionals and row.conditionals_before:
|
|
544
|
+
output.extend(row.conditionals_before)
|
|
545
|
+
|
|
546
|
+
# Get the term (first cell)
|
|
547
|
+
term_lines = row.cells[0].content
|
|
548
|
+
term = term_lines[0] if term_lines else ""
|
|
549
|
+
|
|
550
|
+
# Get the definition (second cell)
|
|
551
|
+
definition_lines = row.cells[1].content
|
|
552
|
+
|
|
553
|
+
# Filter conditionals from definition if needed
|
|
554
|
+
filtered_def_lines = []
|
|
555
|
+
inline_conditionals = []
|
|
556
|
+
|
|
557
|
+
for line in definition_lines:
|
|
558
|
+
if self.IFDEF_PATTERN.match(line) or self.ENDIF_PATTERN.match(line):
|
|
559
|
+
if preserve_conditionals:
|
|
560
|
+
inline_conditionals.append(line)
|
|
561
|
+
else:
|
|
562
|
+
filtered_def_lines.append(line)
|
|
563
|
+
|
|
564
|
+
# Create bullet item
|
|
565
|
+
if filtered_def_lines:
|
|
566
|
+
first_line = filtered_def_lines[0]
|
|
567
|
+
output.append(f"* *{term}*: {first_line}")
|
|
568
|
+
|
|
569
|
+
# Add continuation lines with proper indentation
|
|
570
|
+
for line in filtered_def_lines[1:]:
|
|
571
|
+
output.append(f" {line}")
|
|
572
|
+
|
|
573
|
+
# Add inline conditionals if present
|
|
574
|
+
if preserve_conditionals and inline_conditionals:
|
|
575
|
+
output.extend(inline_conditionals)
|
|
576
|
+
|
|
577
|
+
# Add conditionals after row
|
|
578
|
+
if preserve_conditionals and row.conditionals_after:
|
|
579
|
+
output.extend(row.conditionals_after)
|
|
580
|
+
|
|
581
|
+
return output
|
convert_callouts_to_deflist.py
CHANGED
|
@@ -166,14 +166,15 @@ class CalloutConverter:
|
|
|
166
166
|
content_start = block.start_line + 1 # After ---- only
|
|
167
167
|
content_end = block.end_line
|
|
168
168
|
|
|
169
|
-
# For comments format (without fallback),
|
|
170
|
-
# For deflist/bullets format,
|
|
169
|
+
# For comments format (without fallback), remove explanations but don't add new list
|
|
170
|
+
# For deflist/bullets format, remove old explanations and add new list
|
|
171
171
|
if self.output_format == 'comments' and not use_deflist_fallback:
|
|
172
|
-
#
|
|
172
|
+
# Remove old callout explanations (list or table format)
|
|
173
173
|
new_section = (
|
|
174
174
|
new_lines[:content_start] +
|
|
175
175
|
converted_content +
|
|
176
|
-
new_lines[content_end
|
|
176
|
+
[new_lines[content_end]] + # Keep closing delimiter
|
|
177
|
+
new_lines[explanation_end + 1:] # Skip explanations/table, keep rest
|
|
177
178
|
)
|
|
178
179
|
else:
|
|
179
180
|
# Remove old callout explanations and add new list
|
|
@@ -2,7 +2,7 @@ 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
4
|
convert_callouts_interactive.py,sha256=hoDKff3jqyJiGZ3IqjcWF7AXM4XUQE-vVg2NpJYECs4,21066
|
|
5
|
-
convert_callouts_to_deflist.py,sha256=
|
|
5
|
+
convert_callouts_to_deflist.py,sha256=MfNbbTzaODFIK6jdPdCoMCe27KqMjJFjdoIiGazm978,17852
|
|
6
6
|
doc_utils_cli.py,sha256=dsMYrkAriYdZUF0_cSPh5DAPrJMPiecuY26xN-p0UJ0,4911
|
|
7
7
|
extract_link_attributes.py,sha256=wR2SmR2la-jR6DzDbas2PoNONgRZ4dZ6aqwzkwEv8Gs,3516
|
|
8
8
|
find_unused_attributes.py,sha256=77CxFdm72wj6SO81w-auMdDjnvF83jWy_qaM7DsAtBw,4263
|
|
@@ -13,7 +13,8 @@ callout_lib/__init__.py,sha256=8B82N_z4D1LaZVYgd5jZR53QAabtgPzADOyGlnvihj0,665
|
|
|
13
13
|
callout_lib/converter_bullets.py,sha256=8P92QZ0PylrKRE7V-D3TVZCDH0ct3GRIonc7W5AK5uU,3898
|
|
14
14
|
callout_lib/converter_comments.py,sha256=do0dH8uOyNFpn5CDEzR0jYYCMIPP3oPFM8cEB-Fp22c,9767
|
|
15
15
|
callout_lib/converter_deflist.py,sha256=mQ17Y8gJLv0MzzJscpUL7ujuDRyEVcrb9lcPdUNkIX4,3117
|
|
16
|
-
callout_lib/detector.py,sha256=
|
|
16
|
+
callout_lib/detector.py,sha256=yPikWkoZybd5GjooEv8xYd-iLQbHP09cNCp7HrcqWCA,13200
|
|
17
|
+
callout_lib/table_parser.py,sha256=CaFOphhwONF4zQWeVVGZbB-rwF0Ety7R6My3V6ETdig,21119
|
|
17
18
|
doc_utils/__init__.py,sha256=qqZR3lohzkP63soymrEZPBGzzk6-nFzi4_tSffjmu_0,74
|
|
18
19
|
doc_utils/extract_link_attributes.py,sha256=U0EvPZReJQigNfbT-icBsVT6Li64hYki5W7MQz6qqbc,22743
|
|
19
20
|
doc_utils/file_utils.py,sha256=fpTh3xx759sF8sNocdn_arsP3KAv8XA6cTQTAVIZiZg,4247
|
|
@@ -28,9 +29,9 @@ doc_utils/unused_images.py,sha256=nqn36Bbrmon2KlGlcaruNjJJvTQ8_9H0WU9GvCW7rW8,14
|
|
|
28
29
|
doc_utils/validate_links.py,sha256=iBGXnwdeLlgIT3fo3v01ApT5k0X2FtctsvkrE6E3VMk,19610
|
|
29
30
|
doc_utils/version.py,sha256=5Uc0sAUOkXA6R_PvDGjw2MBYptEKdav5XmeRqukMTo0,203
|
|
30
31
|
doc_utils/version_check.py,sha256=eHJnZmBTbdhhY2fJQW9KnnyD0rWEvCZpMg6oSr0fOmI,7090
|
|
31
|
-
rolfedh_doc_utils-0.1.
|
|
32
|
-
rolfedh_doc_utils-0.1.
|
|
33
|
-
rolfedh_doc_utils-0.1.
|
|
34
|
-
rolfedh_doc_utils-0.1.
|
|
35
|
-
rolfedh_doc_utils-0.1.
|
|
36
|
-
rolfedh_doc_utils-0.1.
|
|
32
|
+
rolfedh_doc_utils-0.1.27.dist-info/licenses/LICENSE,sha256=vLxtwMVOJA_hEy8b77niTkdmQI9kNJskXHq0dBS36e0,1075
|
|
33
|
+
rolfedh_doc_utils-0.1.27.dist-info/METADATA,sha256=Kk9CDC-zzU6sDESsDr_zumWLMycacohrbLUq7_IWHKs,8325
|
|
34
|
+
rolfedh_doc_utils-0.1.27.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
35
|
+
rolfedh_doc_utils-0.1.27.dist-info/entry_points.txt,sha256=vL_LlLKOiurRzchrq8iRUQG19Xi9lSAFVZGjO-xyErk,577
|
|
36
|
+
rolfedh_doc_utils-0.1.27.dist-info/top_level.txt,sha256=J4xtr3zoyCip27b3GnticFVZoyz5HHtgGqHQ-SZONCA,265
|
|
37
|
+
rolfedh_doc_utils-0.1.27.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|