rolfedh-doc-utils 0.1.24__py3-none-any.whl → 0.1.26__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.
@@ -0,0 +1,265 @@
1
+ """
2
+ Callout Detection Module
3
+
4
+ Detects code blocks with callouts and extracts callout information from AsciiDoc files.
5
+ Supports both list-format and table-format callout explanations.
6
+ """
7
+
8
+ import re
9
+ from typing import List, Dict, Tuple, Optional
10
+ from dataclasses import dataclass
11
+ from .table_parser import TableParser
12
+
13
+
14
+ @dataclass
15
+ class Callout:
16
+ """Represents a callout with its number and explanation text."""
17
+ number: int
18
+ lines: List[str] # List of lines to preserve formatting
19
+ is_optional: bool = False
20
+
21
+
22
+ @dataclass
23
+ class CalloutGroup:
24
+ """Represents one or more callouts that share the same code line."""
25
+ code_line: str # The actual code line (without callouts)
26
+ callout_numbers: List[int] # List of callout numbers on this line
27
+
28
+
29
+ @dataclass
30
+ class CodeBlock:
31
+ """Represents a code block with its content and metadata."""
32
+ start_line: int
33
+ end_line: int
34
+ delimiter: str
35
+ content: List[str]
36
+ language: Optional[str] = None
37
+
38
+
39
+ class CalloutDetector:
40
+ """Detects and extracts callout information from AsciiDoc code blocks."""
41
+
42
+ # Pattern for code block start: [source,language] or [source] with optional attributes
43
+ CODE_BLOCK_START = re.compile(r'^\[source(?:,\s*(\w+))?(?:[,\s]+[^\]]+)?\]')
44
+
45
+ # Pattern for callout number in code block (can appear multiple times per line)
46
+ CALLOUT_IN_CODE = re.compile(r'<(\d+)>')
47
+
48
+ # Pattern for callout explanation line: <1> Explanation text
49
+ CALLOUT_EXPLANATION = re.compile(r'^<(\d+)>\s+(.+)$')
50
+
51
+ # Pattern to detect user-replaceable values in angle brackets
52
+ # Excludes heredoc syntax (<<) and comparison operators
53
+ USER_VALUE_PATTERN = re.compile(r'(?<!<)<([a-zA-Z][^>]*)>')
54
+
55
+ def __init__(self):
56
+ """Initialize detector with table parser."""
57
+ self.table_parser = TableParser()
58
+
59
+ def find_code_blocks(self, lines: List[str]) -> List[CodeBlock]:
60
+ """Find all code blocks in the document."""
61
+ blocks = []
62
+ i = 0
63
+
64
+ while i < len(lines):
65
+ # Check for [source] prefix first
66
+ match = self.CODE_BLOCK_START.match(lines[i])
67
+ if match:
68
+ language = match.group(1)
69
+ start = i
70
+ i += 1
71
+
72
+ # Find the delimiter line (---- or ....)
73
+ if i < len(lines) and lines[i].strip() in ['----', '....']:
74
+ delimiter = lines[i].strip()
75
+ i += 1
76
+ content_start = i
77
+
78
+ # Find the closing delimiter
79
+ while i < len(lines):
80
+ if lines[i].strip() == delimiter:
81
+ content = lines[content_start:i]
82
+ blocks.append(CodeBlock(
83
+ start_line=start,
84
+ end_line=i,
85
+ delimiter=delimiter,
86
+ content=content,
87
+ language=language
88
+ ))
89
+ break
90
+ i += 1
91
+ # Check for plain delimited blocks without [source] prefix
92
+ elif lines[i].strip() in ['----', '....']:
93
+ delimiter = lines[i].strip()
94
+ start = i
95
+ i += 1
96
+ content_start = i
97
+
98
+ # Find the closing delimiter
99
+ while i < len(lines):
100
+ if lines[i].strip() == delimiter:
101
+ content = lines[content_start:i]
102
+ # Only add if block contains callouts
103
+ if any(self.CALLOUT_IN_CODE.search(line) for line in content):
104
+ blocks.append(CodeBlock(
105
+ start_line=start,
106
+ end_line=i,
107
+ delimiter=delimiter,
108
+ content=content,
109
+ language=None
110
+ ))
111
+ break
112
+ i += 1
113
+ i += 1
114
+
115
+ return blocks
116
+
117
+ def extract_callouts_from_code(self, content: List[str]) -> List[CalloutGroup]:
118
+ """
119
+ Extract callout numbers from code block content.
120
+ Returns list of CalloutGroups, where each group contains:
121
+ - The code line (with user-replaceable value if found, or full line)
122
+ - List of callout numbers on that line
123
+
124
+ Multiple callouts on the same line are grouped together to be merged
125
+ in the definition list.
126
+ """
127
+ groups = []
128
+
129
+ for line in content:
130
+ # Look for all callout numbers on this line
131
+ callout_matches = list(self.CALLOUT_IN_CODE.finditer(line))
132
+ if callout_matches:
133
+ # Remove all callouts from the line to get the actual code
134
+ line_without_callouts = self.CALLOUT_IN_CODE.sub('', line).strip()
135
+
136
+ # Find all angle-bracket enclosed values
137
+ user_values = self.USER_VALUE_PATTERN.findall(line_without_callouts)
138
+
139
+ # Determine what to use as the code line term
140
+ if user_values:
141
+ # Use the rightmost (closest to the callout) user value
142
+ code_line = user_values[-1]
143
+ else:
144
+ # No angle-bracket value found - use the actual code line
145
+ code_line = line_without_callouts
146
+
147
+ # Collect all callout numbers on this line
148
+ callout_nums = [int(m.group(1)) for m in callout_matches]
149
+
150
+ groups.append(CalloutGroup(
151
+ code_line=code_line,
152
+ callout_numbers=callout_nums
153
+ ))
154
+
155
+ return groups
156
+
157
+ def extract_callout_explanations(self, lines: List[str], start_line: int) -> Tuple[Dict[int, Callout], int]:
158
+ """
159
+ Extract callout explanations following a code block.
160
+ Supports both list-format (<1> text) and table-format explanations.
161
+ Returns dict of callouts and the line number where explanations end.
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
+ return self._extract_from_table(table)
167
+
168
+ # Fall back to list-format extraction
169
+ return self._extract_from_list(lines, start_line)
170
+
171
+ def _extract_from_table(self, table) -> Tuple[Dict[int, Callout], int]:
172
+ """Extract callout explanations from a table format."""
173
+ explanations = {}
174
+ table_data = self.table_parser.extract_callout_explanations_from_table(table)
175
+
176
+ for callout_num, (explanation_lines, conditionals) in table_data.items():
177
+ # Combine explanation lines with conditionals preserved
178
+ all_lines = []
179
+ for line in explanation_lines:
180
+ all_lines.append(line)
181
+
182
+ # Add conditionals as separate lines (they'll be preserved in output)
183
+ all_lines.extend(conditionals)
184
+
185
+ # Check if marked as optional
186
+ is_optional = False
187
+ if all_lines and (all_lines[0].lower().startswith('optional.') or
188
+ all_lines[0].lower().startswith('optional:')):
189
+ is_optional = True
190
+ all_lines[0] = all_lines[0][9:].strip()
191
+ elif all_lines and ('(Optional)' in all_lines[0] or '(optional)' in all_lines[0]):
192
+ is_optional = True
193
+ all_lines[0] = re.sub(r'\s*\(optional\)\s*', ' ', all_lines[0], flags=re.IGNORECASE).strip()
194
+
195
+ explanations[callout_num] = Callout(callout_num, all_lines, is_optional)
196
+
197
+ return explanations, table.end_line
198
+
199
+ def _extract_from_list(self, lines: List[str], start_line: int) -> Tuple[Dict[int, Callout], int]:
200
+ """Extract callout explanations from list format (<1> text)."""
201
+ explanations = {}
202
+ i = start_line + 1 # Start after the closing delimiter
203
+
204
+ # Skip blank lines and continuation markers (+)
205
+ while i < len(lines) and (not lines[i].strip() or lines[i].strip() == '+'):
206
+ i += 1
207
+
208
+ # Collect consecutive callout explanation lines
209
+ while i < len(lines):
210
+ match = self.CALLOUT_EXPLANATION.match(lines[i])
211
+ if match:
212
+ num = int(match.group(1))
213
+ first_line = match.group(2).strip()
214
+ explanation_lines = [first_line]
215
+ i += 1
216
+
217
+ # Collect continuation lines (lines that don't start with a new callout)
218
+ # Continue until we hit a blank line, a new callout, or certain patterns
219
+ while i < len(lines):
220
+ line = lines[i]
221
+ # Stop if we hit a blank line, new callout, or list start marker
222
+ if not line.strip() or self.CALLOUT_EXPLANATION.match(line) or line.startswith('[start='):
223
+ break
224
+ # Add continuation line preserving original formatting
225
+ explanation_lines.append(line)
226
+ i += 1
227
+
228
+ # Check if marked as optional (only in first line)
229
+ is_optional = False
230
+ if first_line.lower().startswith('optional.') or first_line.lower().startswith('optional:'):
231
+ is_optional = True
232
+ # Remove "Optional." or "Optional:" from first line
233
+ explanation_lines[0] = first_line[9:].strip()
234
+ elif '(Optional)' in first_line or '(optional)' in first_line:
235
+ is_optional = True
236
+ explanation_lines[0] = re.sub(r'\s*\(optional\)\s*', ' ', first_line, flags=re.IGNORECASE).strip()
237
+
238
+ explanations[num] = Callout(num, explanation_lines, is_optional)
239
+ else:
240
+ break
241
+
242
+ return explanations, i - 1
243
+
244
+ def remove_callouts_from_code(self, content: List[str]) -> List[str]:
245
+ """Remove callout numbers from code block content (handles multiple callouts per line)."""
246
+ cleaned = []
247
+ for line in content:
248
+ # Remove all callout numbers and trailing whitespace
249
+ cleaned.append(self.CALLOUT_IN_CODE.sub('', line).rstrip())
250
+ return cleaned
251
+
252
+ def validate_callouts(self, callout_groups: List[CalloutGroup], explanations: Dict[int, Callout]) -> Tuple[bool, set, set]:
253
+ """
254
+ Validate that callout numbers in code match explanation numbers.
255
+ Returns tuple of (is_valid, code_nums, explanation_nums).
256
+ """
257
+ # Extract all callout numbers from groups
258
+ code_nums = set()
259
+ for group in callout_groups:
260
+ code_nums.update(group.callout_numbers)
261
+
262
+ explanation_nums = set(explanations.keys())
263
+
264
+ is_valid = code_nums == explanation_nums
265
+ return is_valid, code_nums, explanation_nums
@@ -0,0 +1,437 @@
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
+ if cell_content:
192
+ current_cell_lines.append(cell_content)
193
+ # If empty, just start a new cell with no content yet
194
+
195
+ i += 1
196
+ continue
197
+
198
+ # Regular content line (continuation of current cell)
199
+ if current_cell_lines or current_row_cells:
200
+ current_cell_lines.append(line)
201
+
202
+ i += 1
203
+
204
+ # Return None if we didn't find a proper table end
205
+ return None
206
+
207
+ def is_callout_table(self, table: AsciiDocTable) -> bool:
208
+ """
209
+ Determine if a table is a callout explanation table.
210
+ A callout table has two columns: callout number and explanation.
211
+ """
212
+ if not table.rows:
213
+ return False
214
+
215
+ # Check if all rows have exactly 2 cells
216
+ if not all(len(row.cells) == 2 for row in table.rows):
217
+ return False
218
+
219
+ # Check if first cell of each row is a callout number
220
+ for row in table.rows:
221
+ first_cell = row.cells[0]
222
+ if not first_cell.content:
223
+ return False
224
+
225
+ # First line of first cell should be a callout number
226
+ first_line = first_cell.content[0].strip()
227
+ if not self.CALLOUT_NUMBER.match(first_line):
228
+ return False
229
+
230
+ return True
231
+
232
+ def extract_callout_explanations_from_table(self, table: AsciiDocTable) -> Dict[int, Tuple[List[str], List[str]]]:
233
+ """
234
+ Extract callout explanations from a table.
235
+ Returns dict mapping callout number to tuple of (explanation_lines, conditionals).
236
+
237
+ The conditionals list includes any ifdef/ifndef/endif statements that should
238
+ be preserved when converting the table to other formats.
239
+ """
240
+ explanations = {}
241
+
242
+ for row in table.rows:
243
+ if len(row.cells) != 2:
244
+ continue
245
+
246
+ callout_cell = row.cells[0]
247
+ explanation_cell = row.cells[1]
248
+
249
+ # Extract callout number
250
+ first_line = callout_cell.content[0].strip()
251
+ match = self.CALLOUT_NUMBER.match(first_line)
252
+ if not match:
253
+ continue
254
+
255
+ callout_num = int(match.group(1))
256
+
257
+ # Collect explanation lines
258
+ explanation_lines = []
259
+ for line in explanation_cell.content:
260
+ # Skip conditional directives in explanation (preserve them separately)
261
+ if not (self.IFDEF_PATTERN.match(line) or self.ENDIF_PATTERN.match(line)):
262
+ explanation_lines.append(line)
263
+
264
+ # Collect all conditionals for this row
265
+ all_conditionals = []
266
+ all_conditionals.extend(row.conditionals_before)
267
+
268
+ # Extract conditionals from explanation cell
269
+ for line in explanation_cell.content:
270
+ if self.IFDEF_PATTERN.match(line) or self.ENDIF_PATTERN.match(line):
271
+ all_conditionals.append(line)
272
+
273
+ all_conditionals.extend(row.conditionals_after)
274
+
275
+ explanations[callout_num] = (explanation_lines, all_conditionals)
276
+
277
+ return explanations
278
+
279
+ def find_callout_table_after_code_block(self, lines: List[str], code_block_end: int) -> Optional[AsciiDocTable]:
280
+ """
281
+ Find a callout explanation table that appears after a code block.
282
+
283
+ Args:
284
+ lines: All lines in the document
285
+ code_block_end: Line number where the code block ends
286
+
287
+ Returns:
288
+ AsciiDocTable if a callout table is found, None otherwise
289
+ """
290
+ # Skip blank lines and continuation markers after code block
291
+ i = code_block_end + 1
292
+ while i < len(lines) and (not lines[i].strip() or lines[i].strip() == '+'):
293
+ i += 1
294
+
295
+ # Look for a table starting within the next few lines
296
+ # (allowing for possible text between code block and table)
297
+ search_limit = min(i + 10, len(lines))
298
+
299
+ for j in range(i, search_limit):
300
+ line = lines[j]
301
+
302
+ # If we encounter a list-format callout explanation, stop
303
+ # (list format takes precedence over table format further away)
304
+ if self.CALLOUT_NUMBER.match(line.strip()):
305
+ return None
306
+
307
+ # If we encounter another code block start, stop
308
+ if line.strip() in ['----', '....'] or line.strip().startswith('[source'):
309
+ return None
310
+
311
+ # Check for table delimiter
312
+ if self.TABLE_DELIMITER.match(line):
313
+ # Found a table, parse it
314
+ start_line = j
315
+ if j > 0 and self.TABLE_START.match(lines[j - 1]):
316
+ start_line = j - 1
317
+
318
+ table = self._parse_table(lines, start_line, j)
319
+ if table and self.is_callout_table(table):
320
+ return table
321
+
322
+ # If we found a table but it's not a callout table, stop searching
323
+ break
324
+
325
+ return None
326
+
327
+ def convert_table_to_deflist(self, table: AsciiDocTable, preserve_conditionals: bool = True) -> List[str]:
328
+ """
329
+ Convert a two-column table to an AsciiDoc definition list.
330
+
331
+ Args:
332
+ table: The table to convert
333
+ preserve_conditionals: Whether to preserve ifdef/ifndef/endif statements
334
+
335
+ Returns:
336
+ List of lines representing the definition list
337
+ """
338
+ output = []
339
+
340
+ for row in table.rows:
341
+ if len(row.cells) != 2:
342
+ continue
343
+
344
+ # Add conditionals before row
345
+ if preserve_conditionals and row.conditionals_before:
346
+ output.extend(row.conditionals_before)
347
+
348
+ # First cell is the term
349
+ term_lines = row.cells[0].content
350
+ if term_lines:
351
+ output.append(term_lines[0])
352
+
353
+ # Second cell is the definition
354
+ definition_lines = row.cells[1].content
355
+ if definition_lines:
356
+ # Filter out conditionals if needed
357
+ if preserve_conditionals:
358
+ for line in definition_lines:
359
+ if self.IFDEF_PATTERN.match(line) or self.ENDIF_PATTERN.match(line):
360
+ output.append(line)
361
+ else:
362
+ output.append(f" {line}")
363
+ else:
364
+ for line in definition_lines:
365
+ if not (self.IFDEF_PATTERN.match(line) or self.ENDIF_PATTERN.match(line)):
366
+ output.append(f" {line}")
367
+
368
+ # Add conditionals after row
369
+ if preserve_conditionals and row.conditionals_after:
370
+ output.extend(row.conditionals_after)
371
+
372
+ # Add blank line between entries
373
+ output.append("")
374
+
375
+ # Remove trailing blank line
376
+ if output and not output[-1].strip():
377
+ output.pop()
378
+
379
+ return output
380
+
381
+ def convert_table_to_bullets(self, table: AsciiDocTable, preserve_conditionals: bool = True) -> List[str]:
382
+ """
383
+ Convert a two-column table to a bulleted list.
384
+
385
+ Args:
386
+ table: The table to convert
387
+ preserve_conditionals: Whether to preserve ifdef/ifndef/endif statements
388
+
389
+ Returns:
390
+ List of lines representing the bulleted list
391
+ """
392
+ output = []
393
+
394
+ for row in table.rows:
395
+ if len(row.cells) != 2:
396
+ continue
397
+
398
+ # Add conditionals before row
399
+ if preserve_conditionals and row.conditionals_before:
400
+ output.extend(row.conditionals_before)
401
+
402
+ # Get the term (first cell)
403
+ term_lines = row.cells[0].content
404
+ term = term_lines[0] if term_lines else ""
405
+
406
+ # Get the definition (second cell)
407
+ definition_lines = row.cells[1].content
408
+
409
+ # Filter conditionals from definition if needed
410
+ filtered_def_lines = []
411
+ inline_conditionals = []
412
+
413
+ for line in definition_lines:
414
+ if self.IFDEF_PATTERN.match(line) or self.ENDIF_PATTERN.match(line):
415
+ if preserve_conditionals:
416
+ inline_conditionals.append(line)
417
+ else:
418
+ filtered_def_lines.append(line)
419
+
420
+ # Create bullet item
421
+ if filtered_def_lines:
422
+ first_line = filtered_def_lines[0]
423
+ output.append(f"* *{term}*: {first_line}")
424
+
425
+ # Add continuation lines with proper indentation
426
+ for line in filtered_def_lines[1:]:
427
+ output.append(f" {line}")
428
+
429
+ # Add inline conditionals if present
430
+ if preserve_conditionals and inline_conditionals:
431
+ output.extend(inline_conditionals)
432
+
433
+ # Add conditionals after row
434
+ if preserve_conditionals and row.conditionals_after:
435
+ output.extend(row.conditionals_after)
436
+
437
+ return output