rolfedh-doc-utils 0.1.30__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.
@@ -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
- lines = [''] # Start with blank line before list
43
+ # Convert table title to lead-in sentence if present
44
+ if table_title:
45
+ # Remove leading dot and trailing period if present
46
+ title_text = table_title.lstrip('.').rstrip('.')
47
+ lines = [f'\n{title_text}:'] # Use colon for bulleted list lead-in
48
+ else:
49
+ lines = [''] # Start with blank line before list
42
50
 
43
51
  # Process each group (which may contain one or more callouts)
44
52
  for group in callout_groups:
@@ -16,7 +16,7 @@ class DefListConverter:
16
16
  USER_VALUE_PATTERN = re.compile(r'(?<!<)<([a-zA-Z][^>]*)>')
17
17
 
18
18
  @staticmethod
19
- def convert(callout_groups: List[CalloutGroup], explanations: Dict[int, Callout]) -> List[str]:
19
+ def convert(callout_groups: List[CalloutGroup], explanations: Dict[int, Callout], table_title: str = "") -> List[str]:
20
20
  """
21
21
  Create definition list from callout groups and explanations.
22
22
 
@@ -29,11 +29,19 @@ class DefListConverter:
29
29
  Args:
30
30
  callout_groups: List of CalloutGroup objects from code block
31
31
  explanations: Dict mapping callout numbers to Callout objects
32
+ table_title: Optional table title (e.g., ".Descriptions of delete event")
33
+ Will be converted to lead-in sentence (e.g., "Descriptions of delete event, where:")
32
34
 
33
35
  Returns:
34
36
  List of strings representing the definition list
35
37
  """
36
- lines = ['\nwhere:']
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, set, set]:
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, code_nums, explanation_nums).
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 all callout numbers from groups
319
- code_nums = set()
338
+ # Extract unique callout numbers from code groups
339
+ code_nums_set = set()
320
340
  for group in callout_groups:
321
- code_nums.update(group.callout_numbers)
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
- explanation_nums = set(explanations.keys())
353
+ # Validation compares unique numbers only
354
+ is_valid = code_nums_set == explanation_nums_set
324
355
 
325
- is_valid = code_nums == explanation_nums
326
- return is_valid, code_nums, explanation_nums
356
+ return is_valid, sorted(code_nums_set), sorted(explanation_nums_list)
@@ -38,6 +38,7 @@ class AsciiDocTable:
38
38
  end_line: int
39
39
  attributes: str # Table attributes like [cols="1,3"]
40
40
  rows: List[TableRow]
41
+ title: str = "" # Block title like ".Table description"
41
42
 
42
43
 
43
44
  class TableParser:
@@ -47,8 +48,8 @@ class TableParser:
47
48
  TABLE_START = re.compile(r'^\[.*?\]$')
48
49
  TABLE_DELIMITER = re.compile(r'^\|===\s*$')
49
50
 
50
- # Pattern for table cell separator
51
- CELL_SEPARATOR = re.compile(r'^\|')
51
+ # Pattern for table cell separator (| or cell type specifier like a|, s|, etc.)
52
+ CELL_SEPARATOR = re.compile(r'^(\||[ashdmev]\|)')
52
53
 
53
54
  # Pattern for conditional directives
54
55
  IFDEF_PATTERN = re.compile(r'^(ifdef::|ifndef::).+\[\]\s*$')
@@ -56,6 +57,26 @@ class TableParser:
56
57
 
57
58
  # Pattern for callout number (used for callout table detection)
58
59
  CALLOUT_NUMBER = re.compile(r'^<(\d+)>\s*$')
60
+ PLAIN_NUMBER = re.compile(r'^(\d+)\s*$')
61
+
62
+ def _is_callout_or_number(self, text: str) -> tuple[bool, int]:
63
+ """
64
+ Check if text is a callout number (<1>) or plain number (1).
65
+ Returns (is_match, number) or (False, 0) if no match.
66
+ """
67
+ text = text.strip()
68
+
69
+ # Try callout format first: <1>
70
+ match = self.CALLOUT_NUMBER.match(text)
71
+ if match:
72
+ return (True, int(match.group(1)))
73
+
74
+ # Try plain number format: 1
75
+ match = self.PLAIN_NUMBER.match(text)
76
+ if match:
77
+ return (True, int(match.group(1)))
78
+
79
+ return (False, 0)
59
80
 
60
81
  def _finalize_row_if_complete(self, current_row_cells, conditionals_before_row,
61
82
  conditionals_after_row, expected_columns, rows):
@@ -103,16 +124,27 @@ class TableParser:
103
124
  while i < len(lines):
104
125
  # Look for table delimiter
105
126
  if self.TABLE_DELIMITER.match(lines[i]):
106
- # Check if there are attributes on the line before
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
- attributes = lines[start_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]
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
- # Check if this is a cell type specifier on its own line (e.g., "a|" or "s|")
236
- cell_content = line[1:].strip() # Remove leading | and whitespace
237
-
238
- # If line is just "a|", "s|", "h|", etc. (cell type specifier alone)
239
- if len(cell_content) == 2 and cell_content[0] in 'ashdmev' and cell_content[1] == '|':
240
- # This is a cell type specifier on its own line
241
- if cell_content[0] == 'a':
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
- # Extract cell content from this line (text after |)
264
- cell_content = line[1:] # Remove leading |
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 second |
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
- # Check if first cell of each row is a callout number
362
- for row in table.rows:
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
- if not self.CALLOUT_NUMBER.match(first_line):
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
- return any(keyword in header_text for keyword in header_keywords)
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
- if not first_line.isdigit():
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
- for row in table.rows:
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
- match = self.CALLOUT_NUMBER.match(first_line)
453
- if not match:
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
- if not item_num_str.isdigit():
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 blank lines and continuation markers after code block
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, parse it
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
 
@@ -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
- print_colored(f"Warning: No explanations found for block at line {block.start_line + 1}", Colors.YELLOW)
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)
@@ -22,6 +22,9 @@ 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
+
25
28
 
26
29
  # Colors for output
27
30
  class Colors:
@@ -40,11 +43,12 @@ class CalloutConverter:
40
43
  """Converts callout-style documentation to various formats."""
41
44
 
42
45
  def __init__(self, dry_run: bool = False, verbose: bool = False, output_format: str = 'deflist',
43
- max_comment_length: int = 120):
46
+ max_comment_length: int = 120, force: bool = False):
44
47
  self.dry_run = dry_run
45
48
  self.verbose = verbose
46
49
  self.output_format = output_format # 'deflist', 'bullets', or 'comments'
47
50
  self.max_comment_length = max_comment_length # Max length for inline comments
51
+ self.force = force # Force strip callouts even with warnings
48
52
  self.changes_made = 0
49
53
  self.warnings = [] # Collect warnings for summary
50
54
  self.long_comment_warnings = [] # Warnings for comments exceeding max length
@@ -103,11 +107,50 @@ class CalloutConverter:
103
107
 
104
108
  if not explanations:
105
109
  self.log(f"No explanations found after block at line {block.start_line + 1}")
106
- continue
110
+ # Warn user about code blocks with callouts but no explanations
111
+ warning_msg = (
112
+ f"WARNING: {input_file.name} line {block.start_line + 1}: "
113
+ f"Code block has callouts {sorted(set(all_callout_nums))} but no explanations found after it. "
114
+ f"This may indicate: explanations are shared with another code block, "
115
+ f"explanations are in an unexpected location, or documentation error (missing explanations). "
116
+ f"Consider reviewing this block manually."
117
+ )
118
+ print_colored(warning_msg, Colors.YELLOW)
119
+ self.warnings.append(warning_msg)
120
+
121
+ # In force mode, strip callouts anyway
122
+ if not self.force:
123
+ continue
124
+ else:
125
+ self.log(f"FORCE MODE: Stripping callouts from block at line {block.start_line + 1} despite missing explanations")
126
+
127
+ # Just strip callouts without creating explanation list
128
+ converted_content = self.detector.remove_callouts_from_code(block.content)
129
+
130
+ # Replace in document
131
+ has_source_prefix = self.detector.CODE_BLOCK_START.match(new_lines[block.start_line])
132
+ if has_source_prefix:
133
+ content_start = block.start_line + 2 # After [source] and ----
134
+ else:
135
+ content_start = block.start_line + 1 # After ---- only
136
+ content_end = block.end_line
137
+
138
+ # Build new section with just code (no explanations)
139
+ new_section = (
140
+ new_lines[:content_start] +
141
+ converted_content +
142
+ [new_lines[content_end]] + # Keep closing delimiter
143
+ new_lines[content_end + 1:] # Keep rest of file
144
+ )
145
+
146
+ new_lines = new_section
147
+ conversions += 1
148
+ self.changes_made += 1
149
+ continue
107
150
 
108
151
  # Validate callouts match
109
152
  is_valid, code_nums, explanation_nums = self.detector.validate_callouts(callout_groups, explanations)
110
- if not is_valid:
153
+ if not is_valid and explanations: # Only validate if we have explanations
111
154
  # Format warning message with file and line numbers
112
155
  line_range = f"{block.start_line + 1}-{block.end_line + 1}"
113
156
  warning_msg = (
@@ -116,7 +159,12 @@ class CalloutConverter:
116
159
  )
117
160
  print_colored(warning_msg, Colors.YELLOW)
118
161
  self.warnings.append(warning_msg)
119
- continue
162
+
163
+ # In force mode, convert anyway
164
+ if not self.force:
165
+ continue
166
+ else:
167
+ self.log(f"FORCE MODE: Converting block at line {block.start_line + 1} despite callout mismatch")
120
168
 
121
169
  self.log(f"Converting block at line {block.start_line + 1}")
122
170
 
@@ -144,7 +192,7 @@ class CalloutConverter:
144
192
  # Fall back to definition list
145
193
  self.log(f"Falling back to definition list for block at line {block.start_line + 1}")
146
194
  converted_content = self.detector.remove_callouts_from_code(block.content)
147
- output_list = DefListConverter.convert(callout_groups, explanations)
195
+ output_list = DefListConverter.convert(callout_groups, explanations, self.detector.last_table_title)
148
196
  use_deflist_fallback = True
149
197
  else:
150
198
  output_list = [] # No separate list after code block for comments
@@ -153,9 +201,9 @@ class CalloutConverter:
153
201
  converted_content = self.detector.remove_callouts_from_code(block.content)
154
202
 
155
203
  if self.output_format == 'bullets':
156
- output_list = BulletListConverter.convert(callout_groups, explanations)
204
+ output_list = BulletListConverter.convert(callout_groups, explanations, self.detector.last_table_title)
157
205
  else: # default to 'deflist'
158
- output_list = DefListConverter.convert(callout_groups, explanations)
206
+ output_list = DefListConverter.convert(callout_groups, explanations, self.detector.last_table_title)
159
207
 
160
208
  # Replace in document
161
209
  # Check if block has [source] prefix
@@ -170,23 +218,40 @@ class CalloutConverter:
170
218
  # For deflist/bullets format, remove old explanations and add new list
171
219
  if self.output_format == 'comments' and not use_deflist_fallback:
172
220
  # Remove old callout explanations (list or table format)
221
+ # Find where explanations/table actually starts to preserve content in between
222
+ if self.detector.last_table:
223
+ explanation_start_line = self.detector.last_table.start_line
224
+ else:
225
+ # List format: skip blank lines after code block
226
+ explanation_start_line = block.end_line + 1
227
+ while explanation_start_line < len(new_lines) and not new_lines[explanation_start_line].strip():
228
+ explanation_start_line += 1
229
+
173
230
  new_section = (
174
231
  new_lines[:content_start] +
175
232
  converted_content +
176
233
  [new_lines[content_end]] + # Keep closing delimiter
234
+ new_lines[content_end + 1:explanation_start_line] + # Preserve content between code and explanations
177
235
  new_lines[explanation_end + 1:] # Skip explanations/table, keep rest
178
236
  )
179
237
  else:
180
238
  # Remove old callout explanations and add new list
181
- explanation_start = block.end_line + 1
182
- while explanation_start < len(new_lines) and not new_lines[explanation_start].strip():
183
- explanation_start += 1
239
+ # Find where explanations/table actually starts
240
+ if self.detector.last_table:
241
+ # Table format: preserve content between code block and table start
242
+ explanation_start_line = self.detector.last_table.start_line
243
+ else:
244
+ # List format: skip blank lines after code block
245
+ explanation_start_line = block.end_line + 1
246
+ while explanation_start_line < len(new_lines) and not new_lines[explanation_start_line].strip():
247
+ explanation_start_line += 1
184
248
 
185
249
  # Build the new section
186
250
  new_section = (
187
251
  new_lines[:content_start] +
188
252
  converted_content +
189
253
  [new_lines[content_end]] + # Keep closing delimiter
254
+ new_lines[content_end + 1:explanation_start_line] + # Preserve content between code and explanations
190
255
  output_list +
191
256
  new_lines[explanation_end + 1:]
192
257
  )
@@ -377,6 +442,30 @@ Example transformation (deflist format):
377
442
  type=Path,
378
443
  help='Path to file containing directories/files to exclude, one per line'
379
444
  )
445
+ parser.add_argument(
446
+ '--warnings-report',
447
+ dest='warnings_report',
448
+ action='store_true',
449
+ default=True,
450
+ help='Generate warnings report file (default: enabled)'
451
+ )
452
+ parser.add_argument(
453
+ '--no-warnings-report',
454
+ dest='warnings_report',
455
+ action='store_false',
456
+ help='Disable warnings report file generation'
457
+ )
458
+ parser.add_argument(
459
+ '--warnings-file',
460
+ type=Path,
461
+ default=Path('callout-warnings-report.adoc'),
462
+ help='Path for warnings report file (default: callout-warnings-report.adoc)'
463
+ )
464
+ parser.add_argument(
465
+ '--force',
466
+ action='store_true',
467
+ help='Force strip callouts from code blocks even with warnings (USE WITH CAUTION: only after reviewing and fixing callout issues)'
468
+ )
380
469
 
381
470
  args = parser.parse_args()
382
471
 
@@ -414,9 +503,24 @@ Example transformation (deflist format):
414
503
 
415
504
  print(f"Found {len(adoc_files)} AsciiDoc file(s) to process")
416
505
 
506
+ # If force mode is enabled, show warning and ask for confirmation
507
+ if args.force and not args.dry_run:
508
+ print_colored("\n⚠️ FORCE MODE ENABLED ⚠️", Colors.YELLOW)
509
+ print_colored("This will strip callouts from code blocks even when warnings are present.", Colors.YELLOW)
510
+ print_colored("You should only use this option AFTER:", Colors.YELLOW)
511
+ print_colored(" 1. Reviewing all warnings in the warnings report", Colors.YELLOW)
512
+ print_colored(" 2. Manually fixing callout issues where appropriate", Colors.YELLOW)
513
+ print_colored(" 3. Confirming that remaining warnings are acceptable", Colors.YELLOW)
514
+ print()
515
+ response = input("Are you sure you want to proceed with force mode? (yes/no): ").strip().lower()
516
+ if response not in ['yes', 'y']:
517
+ print_colored("Operation cancelled.", Colors.YELLOW)
518
+ sys.exit(0)
519
+ print()
520
+
417
521
  # Create converter
418
522
  converter = CalloutConverter(dry_run=args.dry_run, verbose=args.verbose, output_format=args.format,
419
- max_comment_length=args.max_comment_length)
523
+ max_comment_length=args.max_comment_length, force=args.force)
420
524
 
421
525
  # Process each file
422
526
  files_processed = 0
@@ -459,9 +563,28 @@ Example transformation (deflist format):
459
563
 
460
564
  # Display warning summary if any warnings were collected
461
565
  if converter.warnings:
462
- print_colored(f"\n⚠️ {len(converter.warnings)} Warning(s):", Colors.YELLOW)
463
- for warning in converter.warnings:
464
- print_colored(f" {warning}", Colors.YELLOW)
566
+ # Generate warnings report if enabled
567
+ if args.warnings_report:
568
+ try:
569
+ generate_warnings_report(converter.warnings, args.warnings_file)
570
+ print_colored(f"\n⚠️ {len(converter.warnings)} Warning(s) - See {args.warnings_file} for details", Colors.YELLOW)
571
+ print()
572
+ print_colored(f"Suggestion: Review and fix the callout issues listed in {args.warnings_file}, then rerun this command.", Colors.YELLOW)
573
+ except Exception as e:
574
+ print_colored(f"\n⚠️ {len(converter.warnings)} Warning(s):", Colors.YELLOW)
575
+ print_colored(f"Error generating warnings report: {e}", Colors.RED)
576
+ # Fall back to displaying warnings in console
577
+ for warning in converter.warnings:
578
+ print_colored(f" {warning}", Colors.YELLOW)
579
+ print()
580
+ print_colored("Suggestion: Fix the callout issues listed above and rerun this command.", Colors.YELLOW)
581
+ else:
582
+ # Console-only output (legacy behavior)
583
+ print_colored(f"\n⚠️ {len(converter.warnings)} Warning(s):", Colors.YELLOW)
584
+ for warning in converter.warnings:
585
+ print_colored(f" {warning}", Colors.YELLOW)
586
+ print()
587
+ print_colored("Suggestion: Fix the callout issues listed above and rerun this command.", Colors.YELLOW)
465
588
  print()
466
589
 
467
590
  if args.dry_run and files_modified > 0:
@@ -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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rolfedh-doc-utils
3
- Version: 0.1.30
3
+ Version: 0.1.31
4
4
  Summary: CLI tools for AsciiDoc documentation projects
5
5
  Author: Rolfe Dlugy-Hegwer
6
6
  License: MIT License
@@ -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=hoDKff3jqyJiGZ3IqjcWF7AXM4XUQE-vVg2NpJYECs4,21066
5
- convert_callouts_to_deflist.py,sha256=MfNbbTzaODFIK6jdPdCoMCe27KqMjJFjdoIiGazm978,17852
4
+ convert_callouts_interactive.py,sha256=wi3EhxWYiUlq7orFiMK3aY2XluzS1tEZFSpVd4meQl4,21980
5
+ convert_callouts_to_deflist.py,sha256=JnYyluTQpEFOLnIVLH4qYrkrD19gtORVniQu162__YM,24715
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=ZYQIddaouEouPkrDRGIt8f35a1rO5M92Ry5Fi5ZL_2g,3926
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=cFFxoVAVgEQNercbk4PIJNys9rD4H5kC3FeTG_aoHeE,4448
16
- callout_lib/detector.py,sha256=im25jLFRQYuD5vXQ5kUlCrl_Zm2x9QJlSDRWlDe_eQc,13999
17
- callout_lib/table_parser.py,sha256=QO-MkeRwn3c05bBqquQvcqzdhpPEFAhnmGr7VphZhcA,26323
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
@@ -29,9 +29,10 @@ doc_utils/unused_images.py,sha256=nqn36Bbrmon2KlGlcaruNjJJvTQ8_9H0WU9GvCW7rW8,14
29
29
  doc_utils/validate_links.py,sha256=iBGXnwdeLlgIT3fo3v01ApT5k0X2FtctsvkrE6E3VMk,19610
30
30
  doc_utils/version.py,sha256=pnEM8Q0l-kM56_u5y1indjEWaQx61Dkm50erPJRxqH8,203
31
31
  doc_utils/version_check.py,sha256=-31Y6AN0KGi_CUCAVOOhf6bPO3r7SQIXPxxeffLAF0w,7535
32
- rolfedh_doc_utils-0.1.30.dist-info/licenses/LICENSE,sha256=vLxtwMVOJA_hEy8b77niTkdmQI9kNJskXHq0dBS36e0,1075
33
- rolfedh_doc_utils-0.1.30.dist-info/METADATA,sha256=1GESxrnld2SncZtc3TucYNxUOG7nDeQJn5e2trIfWMg,8325
34
- rolfedh_doc_utils-0.1.30.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
35
- rolfedh_doc_utils-0.1.30.dist-info/entry_points.txt,sha256=vL_LlLKOiurRzchrq8iRUQG19Xi9lSAFVZGjO-xyErk,577
36
- rolfedh_doc_utils-0.1.30.dist-info/top_level.txt,sha256=J4xtr3zoyCip27b3GnticFVZoyz5HHtgGqHQ-SZONCA,265
37
- rolfedh_doc_utils-0.1.30.dist-info/RECORD,,
32
+ doc_utils/warnings_report.py,sha256=20yfwqBjOprfFhQwCujbcsvjJCbHHhmH84uAujm-y-o,8877
33
+ rolfedh_doc_utils-0.1.31.dist-info/licenses/LICENSE,sha256=vLxtwMVOJA_hEy8b77niTkdmQI9kNJskXHq0dBS36e0,1075
34
+ rolfedh_doc_utils-0.1.31.dist-info/METADATA,sha256=pVW5aKDus2NB85h5WhPjkhmDeFtge3_Woyc55w-aM40,8325
35
+ rolfedh_doc_utils-0.1.31.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
36
+ rolfedh_doc_utils-0.1.31.dist-info/entry_points.txt,sha256=vL_LlLKOiurRzchrq8iRUQG19Xi9lSAFVZGjO-xyErk,577
37
+ rolfedh_doc_utils-0.1.31.dist-info/top_level.txt,sha256=J4xtr3zoyCip27b3GnticFVZoyz5HHtgGqHQ-SZONCA,265
38
+ rolfedh_doc_utils-0.1.31.dist-info/RECORD,,