rolfedh-doc-utils 0.1.22__tar.gz → 0.1.23__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.23}/PKG-INFO +1 -1
  2. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/convert_callouts_to_deflist.py +141 -63
  3. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/pyproject.toml +1 -1
  4. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23/rolfedh_doc_utils.egg-info}/PKG-INFO +1 -1
  5. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/LICENSE +0 -0
  6. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/README.md +0 -0
  7. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/archive_unused_files.py +0 -0
  8. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/archive_unused_images.py +0 -0
  9. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/check_scannability.py +0 -0
  10. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/doc_utils/__init__.py +0 -0
  11. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/doc_utils/extract_link_attributes.py +0 -0
  12. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/doc_utils/file_utils.py +0 -0
  13. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/doc_utils/format_asciidoc_spacing.py +0 -0
  14. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/doc_utils/replace_link_attributes.py +0 -0
  15. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/doc_utils/scannability.py +0 -0
  16. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/doc_utils/spinner.py +0 -0
  17. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/doc_utils/topic_map_parser.py +0 -0
  18. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/doc_utils/unused_adoc.py +0 -0
  19. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/doc_utils/unused_attributes.py +0 -0
  20. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/doc_utils/unused_images.py +0 -0
  21. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/doc_utils/validate_links.py +0 -0
  22. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/doc_utils/version.py +0 -0
  23. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/doc_utils/version_check.py +0 -0
  24. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/doc_utils_cli.py +0 -0
  25. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/extract_link_attributes.py +0 -0
  26. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/find_unused_attributes.py +0 -0
  27. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/format_asciidoc_spacing.py +0 -0
  28. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/replace_link_attributes.py +0 -0
  29. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/rolfedh_doc_utils.egg-info/SOURCES.txt +0 -0
  30. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/rolfedh_doc_utils.egg-info/dependency_links.txt +0 -0
  31. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/rolfedh_doc_utils.egg-info/entry_points.txt +0 -0
  32. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/rolfedh_doc_utils.egg-info/requires.txt +0 -0
  33. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/rolfedh_doc_utils.egg-info/top_level.txt +0 -0
  34. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/setup.cfg +0 -0
  35. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/setup.py +0 -0
  36. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/tests/test_archive_unused_files.py +0 -0
  37. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/tests/test_archive_unused_images.py +0 -0
  38. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/tests/test_auto_discovery.py +0 -0
  39. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/tests/test_check_scannability.py +0 -0
  40. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/tests/test_cli_entry_points.py +0 -0
  41. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/tests/test_extract_link_attributes.py +0 -0
  42. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/tests/test_file_utils.py +0 -0
  43. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/tests/test_fixture_archive_unused_files.py +0 -0
  44. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/tests/test_fixture_archive_unused_images.py +0 -0
  45. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/tests/test_fixture_check_scannability.py +0 -0
  46. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/tests/test_parse_exclude_list.py +0 -0
  47. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/tests/test_replace_link_attributes.py +0 -0
  48. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/tests/test_symlink_handling.py +0 -0
  49. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/tests/test_topic_map_parser.py +0 -0
  50. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/tests/test_unused_attributes.py +0 -0
  51. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/tests/test_validate_links.py +0 -0
  52. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/tests/test_version_check.py +0 -0
  53. {rolfedh_doc_utils-0.1.22 → rolfedh_doc_utils-0.1.23}/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.23
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+(.+)$')
@@ -81,6 +89,7 @@ class CalloutConverter:
81
89
  i = 0
82
90
 
83
91
  while i < len(lines):
92
+ # Check for [source] prefix first
84
93
  match = self.CODE_BLOCK_START.match(lines[i])
85
94
  if match:
86
95
  language = match.group(1)
@@ -106,44 +115,71 @@ class CalloutConverter:
106
115
  ))
107
116
  break
108
117
  i += 1
118
+ # Check for plain delimited blocks without [source] prefix
119
+ elif lines[i].strip() in ['----', '....']:
120
+ delimiter = lines[i].strip()
121
+ start = i
122
+ i += 1
123
+ content_start = i
124
+
125
+ # Find the closing delimiter
126
+ while i < len(lines):
127
+ if lines[i].strip() == delimiter:
128
+ content = lines[content_start:i]
129
+ # Only add if block contains callouts
130
+ if any(self.CALLOUT_IN_CODE.search(line) for line in content):
131
+ blocks.append(CodeBlock(
132
+ start_line=start,
133
+ end_line=i,
134
+ delimiter=delimiter,
135
+ content=content,
136
+ language=None
137
+ ))
138
+ break
139
+ i += 1
109
140
  i += 1
110
141
 
111
142
  return blocks
112
143
 
113
- def extract_callouts_from_code(self, content: List[str]) -> Dict[int, Optional[str]]:
144
+ def extract_callouts_from_code(self, content: List[str]) -> List[CalloutGroup]:
114
145
  """
115
146
  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)
147
+ Returns list of CalloutGroups, where each group contains:
148
+ - The code line (with user-replaceable value if found, or full line)
149
+ - List of callout numbers on that line
119
150
 
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.
151
+ Multiple callouts on the same line are grouped together to be merged
152
+ in the definition list.
122
153
  """
123
- callouts = {}
154
+ groups = []
124
155
 
125
156
  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()
157
+ # Look for all callout numbers on this line
158
+ callout_matches = list(self.CALLOUT_IN_CODE.finditer(line))
159
+ if callout_matches:
160
+ # Remove all callouts from the line to get the actual code
161
+ line_without_callouts = self.CALLOUT_IN_CODE.sub('', line).strip()
134
162
 
135
163
  # Find all angle-bracket enclosed values
136
- user_values = self.USER_VALUE_PATTERN.findall(line_without_callout)
164
+ user_values = self.USER_VALUE_PATTERN.findall(line_without_callouts)
137
165
 
166
+ # Determine what to use as the code line term
138
167
  if user_values:
139
168
  # Use the rightmost (closest to the callout) user value
140
- callouts[callout_num] = user_values[-1]
169
+ code_line = user_values[-1]
141
170
  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
171
+ # No angle-bracket value found - use the actual code line
172
+ code_line = line_without_callouts
145
173
 
146
- return callouts
174
+ # Collect all callout numbers on this line
175
+ callout_nums = [int(m.group(1)) for m in callout_matches]
176
+
177
+ groups.append(CalloutGroup(
178
+ code_line=code_line,
179
+ callout_numbers=callout_nums
180
+ ))
181
+
182
+ return groups
147
183
 
148
184
  def extract_callout_explanations(self, lines: List[str], start_line: int) -> Tuple[Dict[int, Callout], int]:
149
185
  """
@@ -153,8 +189,8 @@ class CalloutConverter:
153
189
  explanations = {}
154
190
  i = start_line + 1 # Start after the closing delimiter
155
191
 
156
- # Skip blank lines
157
- while i < len(lines) and not lines[i].strip():
192
+ # Skip blank lines and continuation markers (+)
193
+ while i < len(lines) and (not lines[i].strip() or lines[i].strip() == '+'):
158
194
  i += 1
159
195
 
160
196
  # Collect consecutive callout explanation lines
@@ -162,31 +198,48 @@ class CalloutConverter:
162
198
  match = self.CALLOUT_EXPLANATION.match(lines[i])
163
199
  if match:
164
200
  num = int(match.group(1))
165
- text = match.group(2).strip()
201
+ first_line = match.group(2).strip()
202
+ explanation_lines = [first_line]
203
+ i += 1
166
204
 
167
- # Check if marked as optional
205
+ # Collect continuation lines (lines that don't start with a new callout)
206
+ # Continue until we hit a blank line, a new callout, or certain patterns
207
+ while i < len(lines):
208
+ line = lines[i]
209
+ # Stop if we hit a blank line, new callout, or list start marker
210
+ if not line.strip() or self.CALLOUT_EXPLANATION.match(line) or line.startswith('[start='):
211
+ break
212
+ # Add continuation line preserving original formatting
213
+ explanation_lines.append(line)
214
+ i += 1
215
+
216
+ # Check if marked as optional (only in first line)
168
217
  is_optional = False
169
- if text.lower().startswith('optional.') or text.lower().startswith('optional:'):
218
+ if first_line.lower().startswith('optional.') or first_line.lower().startswith('optional:'):
170
219
  is_optional = True
171
- text = text[9:].strip() # Remove "Optional." or "Optional:"
172
- elif '(Optional)' in text or '(optional)' in text:
220
+ # Remove "Optional." or "Optional:" from first line
221
+ explanation_lines[0] = first_line[9:].strip()
222
+ elif '(Optional)' in first_line or '(optional)' in first_line:
173
223
  is_optional = True
174
- text = re.sub(r'\s*\(optional\)\s*', ' ', text, flags=re.IGNORECASE).strip()
224
+ explanation_lines[0] = re.sub(r'\s*\(optional\)\s*', ' ', first_line, flags=re.IGNORECASE).strip()
175
225
 
176
- explanations[num] = Callout(num, text, is_optional)
177
- i += 1
226
+ explanations[num] = Callout(num, explanation_lines, is_optional)
178
227
  else:
179
228
  break
180
229
 
181
230
  return explanations, i - 1
182
231
 
183
- def validate_callouts(self, code_callouts: Dict[int, str], explanations: Dict[int, Callout],
232
+ def validate_callouts(self, callout_groups: List[CalloutGroup], explanations: Dict[int, Callout],
184
233
  input_file: Path = None, block_start: int = None, block_end: int = None) -> bool:
185
234
  """
186
235
  Validate that callout numbers in code match explanation numbers.
187
236
  Returns True if valid, False otherwise.
188
237
  """
189
- code_nums = set(code_callouts.keys())
238
+ # Extract all callout numbers from groups
239
+ code_nums = set()
240
+ for group in callout_groups:
241
+ code_nums.update(group.callout_numbers)
242
+
190
243
  explanation_nums = set(explanations.keys())
191
244
 
192
245
  if code_nums != explanation_nums:
@@ -208,31 +261,35 @@ class CalloutConverter:
208
261
  return True
209
262
 
210
263
  def remove_callouts_from_code(self, content: List[str]) -> List[str]:
211
- """Remove callout numbers from code block content."""
264
+ """Remove callout numbers from code block content (handles multiple callouts per line)."""
212
265
  cleaned = []
213
266
  for line in content:
214
- cleaned.append(self.CALLOUT_IN_CODE.sub('', line))
267
+ # Remove all callout numbers and trailing whitespace
268
+ cleaned.append(self.CALLOUT_IN_CODE.sub('', line).rstrip())
215
269
  return cleaned
216
270
 
217
- def create_definition_list(self, code_callouts: Dict[int, str], explanations: Dict[int, Callout]) -> List[str]:
271
+ def create_definition_list(self, callout_groups: List[CalloutGroup], explanations: Dict[int, Callout]) -> List[str]:
218
272
  """
219
- Create definition list from callouts and explanations.
273
+ Create definition list from callout groups and explanations.
220
274
 
221
275
  For callouts with user-replaceable values in angle brackets, uses those.
222
276
  For callouts without values, uses the actual code line as the term.
277
+
278
+ When multiple callouts share the same code line (same group), their
279
+ explanations are merged using AsciiDoc list continuation (+).
223
280
  """
224
- lines = ['\nwhere:\n']
281
+ lines = ['\nwhere:']
225
282
 
226
- # Sort by callout number
227
- for num in sorted(code_callouts.keys()):
228
- value = code_callouts[num]
229
- explanation = explanations[num]
283
+ # Process each group (which may contain one or more callouts)
284
+ for group in callout_groups:
285
+ code_line = group.code_line
286
+ callout_nums = group.callout_numbers
230
287
 
231
288
  # Check if this is a user-replaceable value (contains angle brackets but not heredoc)
232
289
  # User values are single words/phrases in angle brackets like <my-value>
233
- user_values = self.USER_VALUE_PATTERN.findall(value)
290
+ user_values = self.USER_VALUE_PATTERN.findall(code_line)
234
291
 
235
- if user_values and len(user_values) == 1 and len(value) < 100:
292
+ if user_values and len(user_values) == 1 and len(code_line) < 100:
236
293
  # This looks like a user-replaceable value placeholder
237
294
  # Format the value (ensure it has angle brackets)
238
295
  user_value = user_values[0]
@@ -243,15 +300,26 @@ class CalloutConverter:
243
300
  term = f'`{user_value}`'
244
301
  else:
245
302
  # This is a code line - use it as-is in backticks
246
- term = f'`{value}`'
303
+ term = f'`{code_line}`'
304
+
305
+ # Add blank line before each term
306
+ lines.append('')
307
+ lines.append(f'{term}::')
308
+
309
+ # Add explanations for all callouts in this group
310
+ for idx, callout_num in enumerate(callout_nums):
311
+ explanation = explanations[callout_num]
247
312
 
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}'
313
+ # If this is not the first explanation in the group, add continuation marker
314
+ if idx > 0:
315
+ lines.append('+')
252
316
 
253
- lines.append(f'\n{term}::')
254
- lines.append(f'{explanation_text}\n')
317
+ # Add explanation lines, prepending "Optional. " to first line if needed
318
+ for line_idx, line in enumerate(explanation.lines):
319
+ if line_idx == 0 and explanation.is_optional:
320
+ lines.append(f'Optional. {line}')
321
+ else:
322
+ lines.append(line)
255
323
 
256
324
  return lines
257
325
 
@@ -282,14 +350,19 @@ class CalloutConverter:
282
350
  conversions = 0
283
351
 
284
352
  for block in reversed(blocks):
285
- # Extract callouts from code
286
- code_callouts = self.extract_callouts_from_code(block.content)
353
+ # Extract callouts from code (returns list of CalloutGroups)
354
+ callout_groups = self.extract_callouts_from_code(block.content)
287
355
 
288
- if not code_callouts:
356
+ if not callout_groups:
289
357
  self.log(f"No callouts in block at line {block.start_line + 1}")
290
358
  continue
291
359
 
292
- self.log(f"Block at line {block.start_line + 1} has callouts: {list(code_callouts.keys())}")
360
+ # Extract all callout numbers for logging
361
+ all_callout_nums = []
362
+ for group in callout_groups:
363
+ all_callout_nums.extend(group.callout_numbers)
364
+
365
+ self.log(f"Block at line {block.start_line + 1} has callouts: {all_callout_nums}")
293
366
 
294
367
  # Extract explanations
295
368
  explanations, explanation_end = self.extract_callout_explanations(new_lines, block.end_line)
@@ -299,7 +372,7 @@ class CalloutConverter:
299
372
  continue
300
373
 
301
374
  # Validate callouts match
302
- if not self.validate_callouts(code_callouts, explanations, input_file, block.start_line, block.end_line):
375
+ if not self.validate_callouts(callout_groups, explanations, input_file, block.start_line, block.end_line):
303
376
  continue
304
377
 
305
378
  self.log(f"Converting block at line {block.start_line + 1}")
@@ -308,11 +381,16 @@ class CalloutConverter:
308
381
  cleaned_content = self.remove_callouts_from_code(block.content)
309
382
 
310
383
  # Create definition list
311
- def_list = self.create_definition_list(code_callouts, explanations)
384
+ def_list = self.create_definition_list(callout_groups, explanations)
312
385
 
313
386
  # Replace in document
314
387
  # 1. Update code block content
315
- content_start = block.start_line + 2 # After [source] and ----
388
+ # Check if block has [source] prefix by checking if start_line contains [source]
389
+ has_source_prefix = self.CODE_BLOCK_START.match(new_lines[block.start_line])
390
+ if has_source_prefix:
391
+ content_start = block.start_line + 2 # After [source] and ----
392
+ else:
393
+ content_start = block.start_line + 1 # After ---- only
316
394
  content_end = block.end_line
317
395
 
318
396
  # 2. Remove old callout explanations
@@ -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.23"
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.23
4
4
  Summary: CLI tools for AsciiDoc documentation projects
5
5
  Author: Rolfe Dlugy-Hegwer
6
6
  License: MIT License