rolfedh-doc-utils 0.1.22__tar.gz → 0.1.24__tar.gz

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.
Files changed (53) hide show
  1. {rolfedh_doc_utils-0.1.22/rolfedh_doc_utils.egg-info → rolfedh_doc_utils-0.1.24}/PKG-INFO +1 -1
  2. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/convert_callouts_to_deflist.py +226 -67
  3. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/pyproject.toml +1 -1
  4. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24/rolfedh_doc_utils.egg-info}/PKG-INFO +1 -1
  5. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/LICENSE +0 -0
  6. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/README.md +0 -0
  7. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/archive_unused_files.py +0 -0
  8. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/archive_unused_images.py +0 -0
  9. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/check_scannability.py +0 -0
  10. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/doc_utils/__init__.py +0 -0
  11. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/doc_utils/extract_link_attributes.py +0 -0
  12. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/doc_utils/file_utils.py +0 -0
  13. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/doc_utils/format_asciidoc_spacing.py +0 -0
  14. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/doc_utils/replace_link_attributes.py +0 -0
  15. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/doc_utils/scannability.py +0 -0
  16. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/doc_utils/spinner.py +0 -0
  17. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/doc_utils/topic_map_parser.py +0 -0
  18. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/doc_utils/unused_adoc.py +0 -0
  19. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/doc_utils/unused_attributes.py +0 -0
  20. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/doc_utils/unused_images.py +0 -0
  21. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/doc_utils/validate_links.py +0 -0
  22. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/doc_utils/version.py +0 -0
  23. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/doc_utils/version_check.py +0 -0
  24. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/doc_utils_cli.py +0 -0
  25. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/extract_link_attributes.py +0 -0
  26. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/find_unused_attributes.py +0 -0
  27. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/format_asciidoc_spacing.py +0 -0
  28. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/replace_link_attributes.py +0 -0
  29. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/rolfedh_doc_utils.egg-info/SOURCES.txt +0 -0
  30. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/rolfedh_doc_utils.egg-info/dependency_links.txt +0 -0
  31. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/rolfedh_doc_utils.egg-info/entry_points.txt +0 -0
  32. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/rolfedh_doc_utils.egg-info/requires.txt +0 -0
  33. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/rolfedh_doc_utils.egg-info/top_level.txt +0 -0
  34. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/setup.cfg +0 -0
  35. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/setup.py +0 -0
  36. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/tests/test_archive_unused_files.py +0 -0
  37. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/tests/test_archive_unused_images.py +0 -0
  38. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/tests/test_auto_discovery.py +0 -0
  39. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/tests/test_check_scannability.py +0 -0
  40. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/tests/test_cli_entry_points.py +0 -0
  41. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/tests/test_extract_link_attributes.py +0 -0
  42. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/tests/test_file_utils.py +0 -0
  43. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/tests/test_fixture_archive_unused_files.py +0 -0
  44. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/tests/test_fixture_archive_unused_images.py +0 -0
  45. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/tests/test_fixture_check_scannability.py +0 -0
  46. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/tests/test_parse_exclude_list.py +0 -0
  47. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/tests/test_replace_link_attributes.py +0 -0
  48. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/tests/test_symlink_handling.py +0 -0
  49. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/tests/test_topic_map_parser.py +0 -0
  50. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/tests/test_unused_attributes.py +0 -0
  51. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/tests/test_validate_links.py +0 -0
  52. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/tests/test_version_check.py +0 -0
  53. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.24}/validate_links.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rolfedh-doc-utils
3
- Version: 0.1.22
3
+ Version: 0.1.24
4
4
  Summary: CLI tools for AsciiDoc documentation projects
5
5
  Author: Rolfe Dlugy-Hegwer
6
6
  License: MIT License
@@ -34,10 +34,17 @@ def print_colored(message: str, color: str = Colors.NC) -> None:
34
34
  class Callout:
35
35
  """Represents a callout with its number and explanation text."""
36
36
  number: int
37
- text: str
37
+ lines: List[str] # List of lines to preserve formatting
38
38
  is_optional: bool = False
39
39
 
40
40
 
41
+ @dataclass
42
+ class CalloutGroup:
43
+ """Represents one or more callouts that share the same code line."""
44
+ code_line: str # The actual code line (without callouts)
45
+ callout_numbers: List[int] # List of callout numbers on this line
46
+
47
+
41
48
  @dataclass
42
49
  class CodeBlock:
43
50
  """Represents a code block with its content and metadata."""
@@ -51,11 +58,12 @@ class CodeBlock:
51
58
  class CalloutConverter:
52
59
  """Converts callout-style documentation to definition list format."""
53
60
 
54
- # Pattern for code block start: [source,language] or [source]
55
- CODE_BLOCK_START = re.compile(r'^\[source(?:,\s*(\w+))?\]')
61
+ # Pattern for code block start: [source,language] or [source] with optional attributes
62
+ # Matches: [source], [source,java], [source,java,subs="..."], [source,java,options="..."], etc.
63
+ CODE_BLOCK_START = re.compile(r'^\[source(?:,\s*(\w+))?(?:[,\s]+[^\]]+)?\]')
56
64
 
57
- # Pattern for callout number at end of line in code block
58
- CALLOUT_IN_CODE = re.compile(r'<(\d+)>\s*$')
65
+ # Pattern for callout number in code block (can appear multiple times per line)
66
+ CALLOUT_IN_CODE = re.compile(r'<(\d+)>')
59
67
 
60
68
  # Pattern for callout explanation line: <1> Explanation text
61
69
  CALLOUT_EXPLANATION = re.compile(r'^<(\d+)>\s+(.+)$')
@@ -64,9 +72,10 @@ class CalloutConverter:
64
72
  # Excludes heredoc syntax (<<) and comparison operators
65
73
  USER_VALUE_PATTERN = re.compile(r'(?<!<)<([a-zA-Z][^>]*)>')
66
74
 
67
- def __init__(self, dry_run: bool = False, verbose: bool = False):
75
+ def __init__(self, dry_run: bool = False, verbose: bool = False, output_format: str = 'deflist'):
68
76
  self.dry_run = dry_run
69
77
  self.verbose = verbose
78
+ self.output_format = output_format # 'deflist' or 'bullets'
70
79
  self.changes_made = 0
71
80
  self.warnings = [] # Collect warnings for summary
72
81
 
@@ -81,6 +90,7 @@ class CalloutConverter:
81
90
  i = 0
82
91
 
83
92
  while i < len(lines):
93
+ # Check for [source] prefix first
84
94
  match = self.CODE_BLOCK_START.match(lines[i])
85
95
  if match:
86
96
  language = match.group(1)
@@ -106,44 +116,71 @@ class CalloutConverter:
106
116
  ))
107
117
  break
108
118
  i += 1
119
+ # Check for plain delimited blocks without [source] prefix
120
+ elif lines[i].strip() in ['----', '....']:
121
+ delimiter = lines[i].strip()
122
+ start = i
123
+ i += 1
124
+ content_start = i
125
+
126
+ # Find the closing delimiter
127
+ while i < len(lines):
128
+ if lines[i].strip() == delimiter:
129
+ content = lines[content_start:i]
130
+ # Only add if block contains callouts
131
+ if any(self.CALLOUT_IN_CODE.search(line) for line in content):
132
+ blocks.append(CodeBlock(
133
+ start_line=start,
134
+ end_line=i,
135
+ delimiter=delimiter,
136
+ content=content,
137
+ language=None
138
+ ))
139
+ break
140
+ i += 1
109
141
  i += 1
110
142
 
111
143
  return blocks
112
144
 
113
- def extract_callouts_from_code(self, content: List[str]) -> Dict[int, Optional[str]]:
145
+ def extract_callouts_from_code(self, content: List[str]) -> List[CalloutGroup]:
114
146
  """
115
147
  Extract callout numbers from code block content.
116
- Returns dict mapping callout number to either:
117
- - The user-replaceable value (if angle brackets found), or
118
- - The actual code line (for callouts explaining code behavior)
148
+ Returns list of CalloutGroups, where each group contains:
149
+ - The code line (with user-replaceable value if found, or full line)
150
+ - List of callout numbers on that line
119
151
 
120
- For each callout, attempts to extract the most relevant user-replaceable value
121
- by looking for angle-bracket enclosed values on the same line.
152
+ Multiple callouts on the same line are grouped together to be merged
153
+ in the definition list.
122
154
  """
123
- callouts = {}
155
+ groups = []
124
156
 
125
157
  for line in content:
126
- # Look for callout number at end of line
127
- callout_match = self.CALLOUT_IN_CODE.search(line)
128
- if callout_match:
129
- callout_num = int(callout_match.group(1))
130
-
131
- # Extract the user-replaceable value (content in angle brackets)
132
- # Remove the callout number first
133
- line_without_callout = self.CALLOUT_IN_CODE.sub('', line).strip()
158
+ # Look for all callout numbers on this line
159
+ callout_matches = list(self.CALLOUT_IN_CODE.finditer(line))
160
+ if callout_matches:
161
+ # Remove all callouts from the line to get the actual code
162
+ line_without_callouts = self.CALLOUT_IN_CODE.sub('', line).strip()
134
163
 
135
164
  # Find all angle-bracket enclosed values
136
- user_values = self.USER_VALUE_PATTERN.findall(line_without_callout)
165
+ user_values = self.USER_VALUE_PATTERN.findall(line_without_callouts)
137
166
 
167
+ # Determine what to use as the code line term
138
168
  if user_values:
139
169
  # Use the rightmost (closest to the callout) user value
140
- callouts[callout_num] = user_values[-1]
170
+ code_line = user_values[-1]
141
171
  else:
142
- # No angle-bracket value found - store the actual code line
143
- # This will be used as the term in the definition list
144
- callouts[callout_num] = line_without_callout
172
+ # No angle-bracket value found - use the actual code line
173
+ code_line = line_without_callouts
174
+
175
+ # Collect all callout numbers on this line
176
+ callout_nums = [int(m.group(1)) for m in callout_matches]
145
177
 
146
- return callouts
178
+ groups.append(CalloutGroup(
179
+ code_line=code_line,
180
+ callout_numbers=callout_nums
181
+ ))
182
+
183
+ return groups
147
184
 
148
185
  def extract_callout_explanations(self, lines: List[str], start_line: int) -> Tuple[Dict[int, Callout], int]:
149
186
  """
@@ -153,8 +190,8 @@ class CalloutConverter:
153
190
  explanations = {}
154
191
  i = start_line + 1 # Start after the closing delimiter
155
192
 
156
- # Skip blank lines
157
- while i < len(lines) and not lines[i].strip():
193
+ # Skip blank lines and continuation markers (+)
194
+ while i < len(lines) and (not lines[i].strip() or lines[i].strip() == '+'):
158
195
  i += 1
159
196
 
160
197
  # Collect consecutive callout explanation lines
@@ -162,31 +199,48 @@ class CalloutConverter:
162
199
  match = self.CALLOUT_EXPLANATION.match(lines[i])
163
200
  if match:
164
201
  num = int(match.group(1))
165
- text = match.group(2).strip()
202
+ first_line = match.group(2).strip()
203
+ explanation_lines = [first_line]
204
+ i += 1
166
205
 
167
- # Check if marked as optional
206
+ # Collect continuation lines (lines that don't start with a new callout)
207
+ # Continue until we hit a blank line, a new callout, or certain patterns
208
+ while i < len(lines):
209
+ line = lines[i]
210
+ # Stop if we hit a blank line, new callout, or list start marker
211
+ if not line.strip() or self.CALLOUT_EXPLANATION.match(line) or line.startswith('[start='):
212
+ break
213
+ # Add continuation line preserving original formatting
214
+ explanation_lines.append(line)
215
+ i += 1
216
+
217
+ # Check if marked as optional (only in first line)
168
218
  is_optional = False
169
- if text.lower().startswith('optional.') or text.lower().startswith('optional:'):
219
+ if first_line.lower().startswith('optional.') or first_line.lower().startswith('optional:'):
170
220
  is_optional = True
171
- text = text[9:].strip() # Remove "Optional." or "Optional:"
172
- elif '(Optional)' in text or '(optional)' in text:
221
+ # Remove "Optional." or "Optional:" from first line
222
+ explanation_lines[0] = first_line[9:].strip()
223
+ elif '(Optional)' in first_line or '(optional)' in first_line:
173
224
  is_optional = True
174
- text = re.sub(r'\s*\(optional\)\s*', ' ', text, flags=re.IGNORECASE).strip()
225
+ explanation_lines[0] = re.sub(r'\s*\(optional\)\s*', ' ', first_line, flags=re.IGNORECASE).strip()
175
226
 
176
- explanations[num] = Callout(num, text, is_optional)
177
- i += 1
227
+ explanations[num] = Callout(num, explanation_lines, is_optional)
178
228
  else:
179
229
  break
180
230
 
181
231
  return explanations, i - 1
182
232
 
183
- def validate_callouts(self, code_callouts: Dict[int, str], explanations: Dict[int, Callout],
233
+ def validate_callouts(self, callout_groups: List[CalloutGroup], explanations: Dict[int, Callout],
184
234
  input_file: Path = None, block_start: int = None, block_end: int = None) -> bool:
185
235
  """
186
236
  Validate that callout numbers in code match explanation numbers.
187
237
  Returns True if valid, False otherwise.
188
238
  """
189
- code_nums = set(code_callouts.keys())
239
+ # Extract all callout numbers from groups
240
+ code_nums = set()
241
+ for group in callout_groups:
242
+ code_nums.update(group.callout_numbers)
243
+
190
244
  explanation_nums = set(explanations.keys())
191
245
 
192
246
  if code_nums != explanation_nums:
@@ -208,31 +262,35 @@ class CalloutConverter:
208
262
  return True
209
263
 
210
264
  def remove_callouts_from_code(self, content: List[str]) -> List[str]:
211
- """Remove callout numbers from code block content."""
265
+ """Remove callout numbers from code block content (handles multiple callouts per line)."""
212
266
  cleaned = []
213
267
  for line in content:
214
- cleaned.append(self.CALLOUT_IN_CODE.sub('', line))
268
+ # Remove all callout numbers and trailing whitespace
269
+ cleaned.append(self.CALLOUT_IN_CODE.sub('', line).rstrip())
215
270
  return cleaned
216
271
 
217
- def create_definition_list(self, code_callouts: Dict[int, str], explanations: Dict[int, Callout]) -> List[str]:
272
+ def create_definition_list(self, callout_groups: List[CalloutGroup], explanations: Dict[int, Callout]) -> List[str]:
218
273
  """
219
- Create definition list from callouts and explanations.
274
+ Create definition list from callout groups and explanations.
220
275
 
221
276
  For callouts with user-replaceable values in angle brackets, uses those.
222
277
  For callouts without values, uses the actual code line as the term.
278
+
279
+ When multiple callouts share the same code line (same group), their
280
+ explanations are merged using AsciiDoc list continuation (+).
223
281
  """
224
- lines = ['\nwhere:\n']
282
+ lines = ['\nwhere:']
225
283
 
226
- # Sort by callout number
227
- for num in sorted(code_callouts.keys()):
228
- value = code_callouts[num]
229
- explanation = explanations[num]
284
+ # Process each group (which may contain one or more callouts)
285
+ for group in callout_groups:
286
+ code_line = group.code_line
287
+ callout_nums = group.callout_numbers
230
288
 
231
289
  # Check if this is a user-replaceable value (contains angle brackets but not heredoc)
232
290
  # User values are single words/phrases in angle brackets like <my-value>
233
- user_values = self.USER_VALUE_PATTERN.findall(value)
291
+ user_values = self.USER_VALUE_PATTERN.findall(code_line)
234
292
 
235
- if user_values and len(user_values) == 1 and len(value) < 100:
293
+ if user_values and len(user_values) == 1 and len(code_line) < 100:
236
294
  # This looks like a user-replaceable value placeholder
237
295
  # Format the value (ensure it has angle brackets)
238
296
  user_value = user_values[0]
@@ -243,15 +301,97 @@ class CalloutConverter:
243
301
  term = f'`{user_value}`'
244
302
  else:
245
303
  # This is a code line - use it as-is in backticks
246
- term = f'`{value}`'
304
+ term = f'`{code_line}`'
305
+
306
+ # Add blank line before each term
307
+ lines.append('')
308
+ lines.append(f'{term}::')
247
309
 
248
- # Prepend "Optional. " to the explanation text if marked as optional
249
- explanation_text = explanation.text
250
- if explanation.is_optional:
251
- explanation_text = f'Optional. {explanation_text}'
310
+ # Add explanations for all callouts in this group
311
+ for idx, callout_num in enumerate(callout_nums):
312
+ explanation = explanations[callout_num]
252
313
 
253
- lines.append(f'\n{term}::')
254
- lines.append(f'{explanation_text}\n')
314
+ # If this is not the first explanation in the group, add continuation marker
315
+ if idx > 0:
316
+ lines.append('+')
317
+
318
+ # Add explanation lines, prepending "Optional. " to first line if needed
319
+ for line_idx, line in enumerate(explanation.lines):
320
+ if line_idx == 0 and explanation.is_optional:
321
+ lines.append(f'Optional. {line}')
322
+ else:
323
+ lines.append(line)
324
+
325
+ return lines
326
+
327
+ def create_bulleted_list(self, callout_groups: List[CalloutGroup], explanations: Dict[int, Callout]) -> List[str]:
328
+ """
329
+ Create bulleted list from callout groups and explanations.
330
+
331
+ Follows Red Hat style guide format:
332
+ - Each bullet starts with `*` followed by backticked code element
333
+ - Colon separates element from explanation
334
+ - Blank line between each bullet point
335
+
336
+ For callouts with user-replaceable values in angle brackets, uses those.
337
+ For callouts without values, uses the actual code line as the term.
338
+
339
+ When multiple callouts share the same code line (same group), their
340
+ explanations are merged with line breaks.
341
+ """
342
+ lines = [''] # Start with blank line before list
343
+
344
+ # Process each group (which may contain one or more callouts)
345
+ for group in callout_groups:
346
+ code_line = group.code_line
347
+ callout_nums = group.callout_numbers
348
+
349
+ # Check if this is a user-replaceable value (contains angle brackets but not heredoc)
350
+ # User values are single words/phrases in angle brackets like <my-value>
351
+ user_values = self.USER_VALUE_PATTERN.findall(code_line)
352
+
353
+ if user_values and len(user_values) == 1 and len(code_line) < 100:
354
+ # This looks like a user-replaceable value placeholder
355
+ # Format the value (ensure it has angle brackets)
356
+ user_value = user_values[0]
357
+ if not user_value.startswith('<'):
358
+ user_value = f'<{user_value}>'
359
+ if not user_value.endswith('>'):
360
+ user_value = f'{user_value}>'
361
+ term = f'`{user_value}`'
362
+ else:
363
+ # This is a code line - use it as-is in backticks
364
+ term = f'`{code_line}`'
365
+
366
+ # Collect all explanations for this group
367
+ all_explanation_lines = []
368
+ for idx, callout_num in enumerate(callout_nums):
369
+ explanation = explanations[callout_num]
370
+
371
+ # Add explanation lines, prepending "Optional. " to first line if needed
372
+ for line_idx, line in enumerate(explanation.lines):
373
+ if line_idx == 0 and explanation.is_optional:
374
+ all_explanation_lines.append(f'Optional. {line}')
375
+ else:
376
+ all_explanation_lines.append(line)
377
+
378
+ # If there are more callouts in this group, add a line break
379
+ if idx < len(callout_nums) - 1:
380
+ all_explanation_lines.append('')
381
+
382
+ # Format as bullet point: * `term`: explanation
383
+ # First line uses the bullet marker
384
+ lines.append(f'* {term}: {all_explanation_lines[0]}')
385
+
386
+ # Continuation lines (if any) are indented to align with first line
387
+ for continuation_line in all_explanation_lines[1:]:
388
+ if continuation_line: # Skip empty lines for now
389
+ lines.append(f' {continuation_line}')
390
+ else:
391
+ lines.append('')
392
+
393
+ # Add blank line after each bullet point
394
+ lines.append('')
255
395
 
256
396
  return lines
257
397
 
@@ -282,14 +422,19 @@ class CalloutConverter:
282
422
  conversions = 0
283
423
 
284
424
  for block in reversed(blocks):
285
- # Extract callouts from code
286
- code_callouts = self.extract_callouts_from_code(block.content)
425
+ # Extract callouts from code (returns list of CalloutGroups)
426
+ callout_groups = self.extract_callouts_from_code(block.content)
287
427
 
288
- if not code_callouts:
428
+ if not callout_groups:
289
429
  self.log(f"No callouts in block at line {block.start_line + 1}")
290
430
  continue
291
431
 
292
- self.log(f"Block at line {block.start_line + 1} has callouts: {list(code_callouts.keys())}")
432
+ # Extract all callout numbers for logging
433
+ all_callout_nums = []
434
+ for group in callout_groups:
435
+ all_callout_nums.extend(group.callout_numbers)
436
+
437
+ self.log(f"Block at line {block.start_line + 1} has callouts: {all_callout_nums}")
293
438
 
294
439
  # Extract explanations
295
440
  explanations, explanation_end = self.extract_callout_explanations(new_lines, block.end_line)
@@ -299,7 +444,7 @@ class CalloutConverter:
299
444
  continue
300
445
 
301
446
  # Validate callouts match
302
- if not self.validate_callouts(code_callouts, explanations, input_file, block.start_line, block.end_line):
447
+ if not self.validate_callouts(callout_groups, explanations, input_file, block.start_line, block.end_line):
303
448
  continue
304
449
 
305
450
  self.log(f"Converting block at line {block.start_line + 1}")
@@ -307,12 +452,20 @@ class CalloutConverter:
307
452
  # Remove callouts from code
308
453
  cleaned_content = self.remove_callouts_from_code(block.content)
309
454
 
310
- # Create definition list
311
- def_list = self.create_definition_list(code_callouts, explanations)
455
+ # Create output list (definition list or bulleted list based on format option)
456
+ if self.output_format == 'bullets':
457
+ output_list = self.create_bulleted_list(callout_groups, explanations)
458
+ else: # default to 'deflist'
459
+ output_list = self.create_definition_list(callout_groups, explanations)
312
460
 
313
461
  # Replace in document
314
462
  # 1. Update code block content
315
- content_start = block.start_line + 2 # After [source] and ----
463
+ # Check if block has [source] prefix by checking if start_line contains [source]
464
+ has_source_prefix = self.CODE_BLOCK_START.match(new_lines[block.start_line])
465
+ if has_source_prefix:
466
+ content_start = block.start_line + 2 # After [source] and ----
467
+ else:
468
+ content_start = block.start_line + 1 # After ---- only
316
469
  content_end = block.end_line
317
470
 
318
471
  # 2. Remove old callout explanations
@@ -325,7 +478,7 @@ class CalloutConverter:
325
478
  new_lines[:content_start] +
326
479
  cleaned_content +
327
480
  [new_lines[content_end]] + # Keep closing delimiter
328
- def_list +
481
+ output_list +
329
482
  new_lines[explanation_end + 1:]
330
483
  )
331
484
 
@@ -478,6 +631,12 @@ Example transformation:
478
631
  action='store_true',
479
632
  help='Enable verbose output'
480
633
  )
634
+ parser.add_argument(
635
+ '-f', '--format',
636
+ choices=['deflist', 'bullets'],
637
+ default='deflist',
638
+ help='Output format: "deflist" for definition list with "where:" (default), "bullets" for bulleted list'
639
+ )
481
640
  parser.add_argument(
482
641
  '--exclude-dir',
483
642
  action='append',
@@ -535,7 +694,7 @@ Example transformation:
535
694
  print(f"Found {len(adoc_files)} AsciiDoc file(s) to process")
536
695
 
537
696
  # Create converter
538
- converter = CalloutConverter(dry_run=args.dry_run, verbose=args.verbose)
697
+ converter = CalloutConverter(dry_run=args.dry_run, verbose=args.verbose, output_format=args.format)
539
698
 
540
699
  # Process each file
541
700
  files_processed = 0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rolfedh-doc-utils"
7
- version = "0.1.22"
7
+ version = "0.1.24"
8
8
  description = "CLI tools for AsciiDoc documentation projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rolfedh-doc-utils
3
- Version: 0.1.22
3
+ Version: 0.1.24
4
4
  Summary: CLI tools for AsciiDoc documentation projects
5
5
  Author: Rolfe Dlugy-Hegwer
6
6
  License: MIT License