rolfedh-doc-utils 0.1.23__tar.gz → 0.1.25__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 (60) hide show
  1. {rolfedh_doc_utils-0.1.23/rolfedh_doc_utils.egg-info → rolfedh_doc_utils-0.1.25}/PKG-INFO +1 -1
  2. rolfedh_doc_utils-0.1.25/callout_lib/__init__.py +22 -0
  3. rolfedh_doc_utils-0.1.25/callout_lib/converter_bullets.py +95 -0
  4. rolfedh_doc_utils-0.1.25/callout_lib/converter_comments.py +295 -0
  5. rolfedh_doc_utils-0.1.25/callout_lib/converter_deflist.py +79 -0
  6. rolfedh_doc_utils-0.1.25/callout_lib/detector.py +220 -0
  7. rolfedh_doc_utils-0.1.25/convert_callouts_interactive.py +532 -0
  8. rolfedh_doc_utils-0.1.25/convert_callouts_to_deflist.py +473 -0
  9. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/pyproject.toml +4 -3
  10. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25/rolfedh_doc_utils.egg-info}/PKG-INFO +1 -1
  11. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/rolfedh_doc_utils.egg-info/SOURCES.txt +6 -0
  12. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/rolfedh_doc_utils.egg-info/entry_points.txt +1 -0
  13. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/rolfedh_doc_utils.egg-info/top_level.txt +2 -0
  14. rolfedh_doc_utils-0.1.23/convert_callouts_to_deflist.py +0 -671
  15. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/LICENSE +0 -0
  16. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/README.md +0 -0
  17. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/archive_unused_files.py +0 -0
  18. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/archive_unused_images.py +0 -0
  19. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/check_scannability.py +0 -0
  20. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/doc_utils/__init__.py +0 -0
  21. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/doc_utils/extract_link_attributes.py +0 -0
  22. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/doc_utils/file_utils.py +0 -0
  23. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/doc_utils/format_asciidoc_spacing.py +0 -0
  24. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/doc_utils/replace_link_attributes.py +0 -0
  25. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/doc_utils/scannability.py +0 -0
  26. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/doc_utils/spinner.py +0 -0
  27. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/doc_utils/topic_map_parser.py +0 -0
  28. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/doc_utils/unused_adoc.py +0 -0
  29. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/doc_utils/unused_attributes.py +0 -0
  30. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/doc_utils/unused_images.py +0 -0
  31. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/doc_utils/validate_links.py +0 -0
  32. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/doc_utils/version.py +0 -0
  33. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/doc_utils/version_check.py +0 -0
  34. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/doc_utils_cli.py +0 -0
  35. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/extract_link_attributes.py +0 -0
  36. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/find_unused_attributes.py +0 -0
  37. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/format_asciidoc_spacing.py +0 -0
  38. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/replace_link_attributes.py +0 -0
  39. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/rolfedh_doc_utils.egg-info/dependency_links.txt +0 -0
  40. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/rolfedh_doc_utils.egg-info/requires.txt +0 -0
  41. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/setup.cfg +0 -0
  42. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/setup.py +0 -0
  43. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/tests/test_archive_unused_files.py +0 -0
  44. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/tests/test_archive_unused_images.py +0 -0
  45. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/tests/test_auto_discovery.py +0 -0
  46. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/tests/test_check_scannability.py +0 -0
  47. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/tests/test_cli_entry_points.py +0 -0
  48. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/tests/test_extract_link_attributes.py +0 -0
  49. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/tests/test_file_utils.py +0 -0
  50. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/tests/test_fixture_archive_unused_files.py +0 -0
  51. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/tests/test_fixture_archive_unused_images.py +0 -0
  52. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/tests/test_fixture_check_scannability.py +0 -0
  53. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/tests/test_parse_exclude_list.py +0 -0
  54. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/tests/test_replace_link_attributes.py +0 -0
  55. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/tests/test_symlink_handling.py +0 -0
  56. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/tests/test_topic_map_parser.py +0 -0
  57. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/tests/test_unused_attributes.py +0 -0
  58. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/tests/test_validate_links.py +0 -0
  59. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/tests/test_version_check.py +0 -0
  60. {rolfedh_doc_utils-0.1.23 → rolfedh_doc_utils-0.1.25}/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.23
3
+ Version: 0.1.25
4
4
  Summary: CLI tools for AsciiDoc documentation projects
5
5
  Author: Rolfe Dlugy-Hegwer
6
6
  License: MIT License
@@ -0,0 +1,22 @@
1
+ """
2
+ Callout Library - Shared modules for AsciiDoc callout conversion
3
+
4
+ This library provides reusable components for converting AsciiDoc callouts
5
+ to various formats including definition lists, bulleted lists, and inline comments.
6
+ """
7
+
8
+ from .detector import CalloutDetector, CodeBlock, CalloutGroup, Callout
9
+ from .converter_deflist import DefListConverter
10
+ from .converter_bullets import BulletListConverter
11
+ from .converter_comments import CommentConverter, LongCommentWarning
12
+
13
+ __all__ = [
14
+ 'CalloutDetector',
15
+ 'CodeBlock',
16
+ 'CalloutGroup',
17
+ 'Callout',
18
+ 'DefListConverter',
19
+ 'BulletListConverter',
20
+ 'CommentConverter',
21
+ 'LongCommentWarning',
22
+ ]
@@ -0,0 +1,95 @@
1
+ """
2
+ Bulleted List Converter Module
3
+
4
+ Converts callouts to bulleted list format following Red Hat style guide.
5
+ """
6
+
7
+ import re
8
+ from typing import List, Dict
9
+ from .detector import CalloutGroup, Callout
10
+
11
+
12
+ class BulletListConverter:
13
+ """Converts callouts to bulleted list format."""
14
+
15
+ # Pattern to detect user-replaceable values in angle brackets
16
+ USER_VALUE_PATTERN = re.compile(r'(?<!<)<([a-zA-Z][^>]*)>')
17
+
18
+ @staticmethod
19
+ def convert(callout_groups: List[CalloutGroup], explanations: Dict[int, Callout]) -> List[str]:
20
+ """
21
+ Create bulleted list from callout groups and explanations.
22
+
23
+ Follows Red Hat style guide format:
24
+ - Each bullet starts with `*` followed by backticked code element
25
+ - Colon separates element from explanation
26
+ - Blank line between each bullet point
27
+
28
+ For callouts with user-replaceable values in angle brackets, uses those.
29
+ For callouts without values, uses the actual code line as the term.
30
+
31
+ When multiple callouts share the same code line (same group), their
32
+ explanations are merged with line breaks.
33
+
34
+ Args:
35
+ callout_groups: List of CalloutGroup objects from code block
36
+ explanations: Dict mapping callout numbers to Callout objects
37
+
38
+ Returns:
39
+ List of strings representing the bulleted list
40
+ """
41
+ lines = [''] # Start with blank line before list
42
+
43
+ # Process each group (which may contain one or more callouts)
44
+ for group in callout_groups:
45
+ code_line = group.code_line
46
+ callout_nums = group.callout_numbers
47
+
48
+ # Check if this is a user-replaceable value (contains angle brackets but not heredoc)
49
+ # User values are single words/phrases in angle brackets like <my-value>
50
+ user_values = BulletListConverter.USER_VALUE_PATTERN.findall(code_line)
51
+
52
+ if user_values and len(user_values) == 1 and len(code_line) < 100:
53
+ # This looks like a user-replaceable value placeholder
54
+ # Format the value (ensure it has angle brackets)
55
+ user_value = user_values[0]
56
+ if not user_value.startswith('<'):
57
+ user_value = f'<{user_value}>'
58
+ if not user_value.endswith('>'):
59
+ user_value = f'{user_value}>'
60
+ term = f'`{user_value}`'
61
+ else:
62
+ # This is a code line - use it as-is in backticks
63
+ term = f'`{code_line}`'
64
+
65
+ # Collect all explanations for this group
66
+ all_explanation_lines = []
67
+ for idx, callout_num in enumerate(callout_nums):
68
+ explanation = explanations[callout_num]
69
+
70
+ # Add explanation lines, prepending "Optional. " to first line if needed
71
+ for line_idx, line in enumerate(explanation.lines):
72
+ if line_idx == 0 and explanation.is_optional:
73
+ all_explanation_lines.append(f'Optional. {line}')
74
+ else:
75
+ all_explanation_lines.append(line)
76
+
77
+ # If there are more callouts in this group, add a line break
78
+ if idx < len(callout_nums) - 1:
79
+ all_explanation_lines.append('')
80
+
81
+ # Format as bullet point: * `term`: explanation
82
+ # First line uses the bullet marker
83
+ lines.append(f'* {term}: {all_explanation_lines[0]}')
84
+
85
+ # Continuation lines (if any) are indented to align with first line
86
+ for continuation_line in all_explanation_lines[1:]:
87
+ if continuation_line: # Skip empty lines for now
88
+ lines.append(f' {continuation_line}')
89
+ else:
90
+ lines.append('')
91
+
92
+ # Add blank line after each bullet point
93
+ lines.append('')
94
+
95
+ return lines
@@ -0,0 +1,295 @@
1
+ """
2
+ Inline Comments Converter Module
3
+
4
+ Converts callouts to inline comments within code blocks.
5
+ """
6
+
7
+ import re
8
+ from typing import List, Dict, Optional, Tuple
9
+ from .detector import CalloutGroup, Callout
10
+
11
+
12
+ class LongCommentWarning:
13
+ """Represents a warning about a comment that exceeds the length threshold."""
14
+ def __init__(self, callout_num: int, length: int, text: str, line_num: int = None):
15
+ self.callout_num = callout_num
16
+ self.length = length
17
+ self.text = text
18
+ self.line_num = line_num
19
+
20
+
21
+ class CommentConverter:
22
+ """Converts callouts to inline comments in code."""
23
+
24
+ # Map of programming language identifiers to their comment syntax
25
+ COMMENT_SYNTAX = {
26
+ # Single-line comment languages
27
+ 'java': '//',
28
+ 'javascript': '//',
29
+ 'js': '//',
30
+ 'typescript': '//',
31
+ 'ts': '//',
32
+ 'c': '//',
33
+ 'cpp': '//',
34
+ 'c++': '//',
35
+ 'csharp': '//',
36
+ 'cs': '//',
37
+ 'go': '//',
38
+ 'rust': '//',
39
+ 'swift': '//',
40
+ 'kotlin': '//',
41
+ 'scala': '//',
42
+ 'groovy': '//',
43
+
44
+ # Hash comment languages
45
+ 'python': '#',
46
+ 'py': '#',
47
+ 'ruby': '#',
48
+ 'rb': '#',
49
+ 'perl': '#',
50
+ 'bash': '#',
51
+ 'sh': '#',
52
+ 'shell': '#',
53
+ 'yaml': '#',
54
+ 'yml': '#',
55
+ 'properties': '#',
56
+ 'r': '#',
57
+ 'powershell': '#',
58
+ 'ps1': '#',
59
+
60
+ # SQL variants
61
+ 'sql': '--',
62
+ 'plsql': '--',
63
+ 'tsql': '--',
64
+
65
+ # Lua
66
+ 'lua': '--',
67
+
68
+ # Lisp-like
69
+ 'lisp': ';;',
70
+ 'scheme': ';;',
71
+ 'clojure': ';;',
72
+
73
+ # Markup languages
74
+ 'html': '<!--',
75
+ 'xml': '<!--',
76
+ 'svg': '<!--',
77
+
78
+ # Other
79
+ 'matlab': '%',
80
+ 'tex': '%',
81
+ 'latex': '%',
82
+ }
83
+
84
+ # Languages that need closing comment syntax
85
+ COMMENT_CLOSING = {
86
+ 'html': '-->',
87
+ 'xml': '-->',
88
+ 'svg': '-->',
89
+ }
90
+
91
+ @staticmethod
92
+ def get_comment_syntax(language: Optional[str]) -> tuple[str, str]:
93
+ """
94
+ Get comment syntax for a given language.
95
+ Returns tuple of (opening, closing) comment markers.
96
+ For single-line comments, closing is empty string.
97
+
98
+ Args:
99
+ language: Language identifier (e.g., 'java', 'python')
100
+
101
+ Returns:
102
+ Tuple of (opening_marker, closing_marker)
103
+ """
104
+ if not language:
105
+ # Default to generic comment marker
106
+ return '#', ''
107
+
108
+ lang_lower = language.lower()
109
+ opening = CommentConverter.COMMENT_SYNTAX.get(lang_lower, '#')
110
+ closing = CommentConverter.COMMENT_CLOSING.get(lang_lower, '')
111
+
112
+ return opening, closing
113
+
114
+ @staticmethod
115
+ def format_comment(text: str, opening: str, closing: str = '') -> str:
116
+ """
117
+ Format text as a comment using the given markers.
118
+
119
+ Args:
120
+ text: Comment text
121
+ opening: Opening comment marker (e.g., '//', '#', '<!--')
122
+ closing: Closing comment marker (e.g., '-->')
123
+
124
+ Returns:
125
+ Formatted comment string
126
+ """
127
+ if closing:
128
+ return f'{opening} {text} {closing}'
129
+ else:
130
+ return f'{opening} {text}'
131
+
132
+ @staticmethod
133
+ def get_comment_length(explanation: Callout, opening: str, closing: str = '') -> int:
134
+ """
135
+ Calculate the total length of a comment including markers and text.
136
+
137
+ Args:
138
+ explanation: Callout explanation
139
+ opening: Opening comment marker
140
+ closing: Closing comment marker (if any)
141
+
142
+ Returns:
143
+ Total character length of the formatted comment
144
+ """
145
+ # Combine all explanation lines into single comment text
146
+ comment_parts = []
147
+ for exp_line in explanation.lines:
148
+ comment_parts.append(exp_line.strip())
149
+ comment_text = ' '.join(comment_parts)
150
+
151
+ # Add "Optional:" prefix if needed
152
+ if explanation.is_optional:
153
+ comment_text = f'Optional: {comment_text}'
154
+
155
+ # Calculate total length with markers
156
+ if closing:
157
+ total = len(f'{opening} {comment_text} {closing}')
158
+ else:
159
+ total = len(f'{opening} {comment_text}')
160
+
161
+ return total
162
+
163
+ @staticmethod
164
+ def shorten_comment(explanation: Callout) -> str:
165
+ """
166
+ Shorten a comment to its first sentence or clause.
167
+
168
+ Args:
169
+ explanation: Callout explanation
170
+
171
+ Returns:
172
+ Shortened comment text
173
+ """
174
+ # Get first line
175
+ first_line = explanation.lines[0] if explanation.lines else ''
176
+
177
+ # Find first sentence-ending punctuation
178
+ for delimiter in ['. ', '! ', '? ']:
179
+ if delimiter in first_line:
180
+ short = first_line.split(delimiter)[0] + delimiter.strip()
181
+ return short
182
+
183
+ # No sentence delimiter found, return first line
184
+ return first_line
185
+
186
+ @staticmethod
187
+ def check_comment_lengths(explanations: Dict[int, Callout], language: Optional[str] = None,
188
+ max_length: int = 100) -> List[LongCommentWarning]:
189
+ """
190
+ Check if any comments exceed the maximum length threshold.
191
+
192
+ Args:
193
+ explanations: Dict of callout explanations
194
+ language: Programming language for comment syntax
195
+ max_length: Maximum allowed comment length (default: 100)
196
+
197
+ Returns:
198
+ List of LongCommentWarning objects for comments exceeding threshold
199
+ """
200
+ opening, closing = CommentConverter.get_comment_syntax(language)
201
+ warnings = []
202
+
203
+ for num, explanation in explanations.items():
204
+ length = CommentConverter.get_comment_length(explanation, opening, closing)
205
+ if length > max_length:
206
+ # Combine all lines for warning text
207
+ full_text = ' '.join(exp_line.strip() for exp_line in explanation.lines)
208
+ warnings.append(LongCommentWarning(num, length, full_text))
209
+
210
+ return warnings
211
+
212
+ @staticmethod
213
+ def convert(code_content: List[str], callout_groups: List[CalloutGroup],
214
+ explanations: Dict[int, Callout], language: Optional[str] = None,
215
+ max_length: Optional[int] = None, shorten_long: bool = False) -> Tuple[List[str], List[LongCommentWarning]]:
216
+ """
217
+ Convert callouts to inline comments within code.
218
+
219
+ This replaces callout markers (<1>, <2>, etc.) with actual comments containing
220
+ the explanation text. The comment syntax is determined by the code block's language.
221
+
222
+ Args:
223
+ code_content: Original code block content with callout markers
224
+ callout_groups: List of CalloutGroup objects (not used for inline conversion)
225
+ explanations: Dict mapping callout numbers to Callout objects
226
+ language: Programming language identifier for comment syntax
227
+ max_length: Maximum comment length before triggering warning (default: None = no limit)
228
+ shorten_long: If True, automatically shorten long comments to first sentence
229
+
230
+ Returns:
231
+ Tuple of (converted lines, list of LongCommentWarning objects)
232
+ """
233
+ opening, closing = CommentConverter.get_comment_syntax(language)
234
+ warnings = []
235
+
236
+ # Check for long comments if max_length specified
237
+ if max_length:
238
+ warnings = CommentConverter.check_comment_lengths(explanations, language, max_length)
239
+
240
+ # Pattern for callout number in code block
241
+ CALLOUT_IN_CODE = re.compile(r'<(\d+)>')
242
+
243
+ result_lines = []
244
+
245
+ for line in code_content:
246
+ # Find all callout markers on this line
247
+ matches = list(CALLOUT_IN_CODE.finditer(line))
248
+
249
+ if not matches:
250
+ # No callouts on this line - keep as-is
251
+ result_lines.append(line)
252
+ continue
253
+
254
+ # Process line with callouts
255
+ # Start from the end to maintain positions during replacement
256
+ modified_line = line
257
+
258
+ for match in reversed(matches):
259
+ callout_num = int(match.group(1))
260
+
261
+ if callout_num not in explanations:
262
+ # Callout number not found in explanations - skip
263
+ continue
264
+
265
+ explanation = explanations[callout_num]
266
+
267
+ # Check if we should shorten this comment
268
+ if shorten_long and any(w.callout_num == callout_num for w in warnings):
269
+ # Use shortened version
270
+ comment_text = CommentConverter.shorten_comment(explanation)
271
+ if explanation.is_optional:
272
+ comment_text = f'Optional: {comment_text}'
273
+ else:
274
+ # Build comment text from explanation lines
275
+ # For inline comments, combine multi-line explanations with spaces
276
+ comment_parts = []
277
+ for exp_line in explanation.lines:
278
+ comment_parts.append(exp_line.strip())
279
+
280
+ comment_text = ' '.join(comment_parts)
281
+
282
+ # Add "Optional:" prefix if needed
283
+ if explanation.is_optional:
284
+ comment_text = f'Optional: {comment_text}'
285
+
286
+ # Format as comment
287
+ comment = CommentConverter.format_comment(comment_text, opening, closing)
288
+
289
+ # Replace the callout marker with the comment
290
+ start, end = match.span()
291
+ modified_line = modified_line[:start] + comment + modified_line[end:]
292
+
293
+ result_lines.append(modified_line)
294
+
295
+ return result_lines, warnings
@@ -0,0 +1,79 @@
1
+ """
2
+ Definition List Converter Module
3
+
4
+ Converts callouts to AsciiDoc definition list format with "where:" prefix.
5
+ """
6
+
7
+ import re
8
+ from typing import List, Dict
9
+ from .detector import CalloutGroup, Callout
10
+
11
+
12
+ class DefListConverter:
13
+ """Converts callouts to definition list format."""
14
+
15
+ # Pattern to detect user-replaceable values in angle brackets
16
+ USER_VALUE_PATTERN = re.compile(r'(?<!<)<([a-zA-Z][^>]*)>')
17
+
18
+ @staticmethod
19
+ def convert(callout_groups: List[CalloutGroup], explanations: Dict[int, Callout]) -> List[str]:
20
+ """
21
+ Create definition list from callout groups and explanations.
22
+
23
+ For callouts with user-replaceable values in angle brackets, uses those.
24
+ For callouts without values, uses the actual code line as the term.
25
+
26
+ When multiple callouts share the same code line (same group), their
27
+ explanations are merged using AsciiDoc list continuation (+).
28
+
29
+ Args:
30
+ callout_groups: List of CalloutGroup objects from code block
31
+ explanations: Dict mapping callout numbers to Callout objects
32
+
33
+ Returns:
34
+ List of strings representing the definition list
35
+ """
36
+ lines = ['\nwhere:']
37
+
38
+ # Process each group (which may contain one or more callouts)
39
+ for group in callout_groups:
40
+ code_line = group.code_line
41
+ callout_nums = group.callout_numbers
42
+
43
+ # Check if this is a user-replaceable value (contains angle brackets but not heredoc)
44
+ # User values are single words/phrases in angle brackets like <my-value>
45
+ user_values = DefListConverter.USER_VALUE_PATTERN.findall(code_line)
46
+
47
+ if user_values and len(user_values) == 1 and len(code_line) < 100:
48
+ # This looks like a user-replaceable value placeholder
49
+ # Format the value (ensure it has angle brackets)
50
+ user_value = user_values[0]
51
+ if not user_value.startswith('<'):
52
+ user_value = f'<{user_value}>'
53
+ if not user_value.endswith('>'):
54
+ user_value = f'{user_value}>'
55
+ term = f'`{user_value}`'
56
+ else:
57
+ # This is a code line - use it as-is in backticks
58
+ term = f'`{code_line}`'
59
+
60
+ # Add blank line before each term
61
+ lines.append('')
62
+ lines.append(f'{term}::')
63
+
64
+ # Add explanations for all callouts in this group
65
+ for idx, callout_num in enumerate(callout_nums):
66
+ explanation = explanations[callout_num]
67
+
68
+ # If this is not the first explanation in the group, add continuation marker
69
+ if idx > 0:
70
+ lines.append('+')
71
+
72
+ # Add explanation lines, prepending "Optional. " to first line if needed
73
+ for line_idx, line in enumerate(explanation.lines):
74
+ if line_idx == 0 and explanation.is_optional:
75
+ lines.append(f'Optional. {line}')
76
+ else:
77
+ lines.append(line)
78
+
79
+ return lines