rolfedh-doc-utils 0.1.4__py3-none-any.whl → 0.1.41__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.
Files changed (52) hide show
  1. archive_unused_files.py +18 -5
  2. archive_unused_images.py +9 -2
  3. callout_lib/__init__.py +22 -0
  4. callout_lib/converter_bullets.py +103 -0
  5. callout_lib/converter_comments.py +295 -0
  6. callout_lib/converter_deflist.py +134 -0
  7. callout_lib/detector.py +364 -0
  8. callout_lib/table_parser.py +804 -0
  9. check_published_links.py +1083 -0
  10. check_scannability.py +6 -0
  11. check_source_directives.py +101 -0
  12. convert_callouts_interactive.py +567 -0
  13. convert_callouts_to_deflist.py +628 -0
  14. convert_freemarker_to_asciidoc.py +288 -0
  15. convert_tables_to_deflists.py +479 -0
  16. doc_utils/convert_freemarker_to_asciidoc.py +708 -0
  17. doc_utils/duplicate_content.py +409 -0
  18. doc_utils/duplicate_includes.py +347 -0
  19. doc_utils/extract_link_attributes.py +618 -0
  20. doc_utils/format_asciidoc_spacing.py +285 -0
  21. doc_utils/insert_abstract_role.py +220 -0
  22. doc_utils/inventory_conditionals.py +164 -0
  23. doc_utils/missing_source_directive.py +211 -0
  24. doc_utils/replace_link_attributes.py +187 -0
  25. doc_utils/spinner.py +119 -0
  26. doc_utils/unused_adoc.py +150 -22
  27. doc_utils/unused_attributes.py +218 -6
  28. doc_utils/unused_images.py +81 -9
  29. doc_utils/validate_links.py +576 -0
  30. doc_utils/version.py +8 -0
  31. doc_utils/version_check.py +243 -0
  32. doc_utils/warnings_report.py +237 -0
  33. doc_utils_cli.py +158 -0
  34. extract_link_attributes.py +120 -0
  35. find_duplicate_content.py +209 -0
  36. find_duplicate_includes.py +198 -0
  37. find_unused_attributes.py +84 -6
  38. format_asciidoc_spacing.py +134 -0
  39. insert_abstract_role.py +163 -0
  40. inventory_conditionals.py +53 -0
  41. replace_link_attributes.py +214 -0
  42. rolfedh_doc_utils-0.1.41.dist-info/METADATA +246 -0
  43. rolfedh_doc_utils-0.1.41.dist-info/RECORD +52 -0
  44. {rolfedh_doc_utils-0.1.4.dist-info → rolfedh_doc_utils-0.1.41.dist-info}/WHEEL +1 -1
  45. rolfedh_doc_utils-0.1.41.dist-info/entry_points.txt +20 -0
  46. rolfedh_doc_utils-0.1.41.dist-info/top_level.txt +21 -0
  47. validate_links.py +213 -0
  48. rolfedh_doc_utils-0.1.4.dist-info/METADATA +0 -285
  49. rolfedh_doc_utils-0.1.4.dist-info/RECORD +0 -17
  50. rolfedh_doc_utils-0.1.4.dist-info/entry_points.txt +0 -5
  51. rolfedh_doc_utils-0.1.4.dist-info/top_level.txt +0 -5
  52. {rolfedh_doc_utils-0.1.4.dist-info → rolfedh_doc_utils-0.1.41.dist-info}/licenses/LICENSE +0 -0
archive_unused_files.py CHANGED
@@ -1,35 +1,48 @@
1
1
  """
2
2
  Archive Unused AsciiDoc Files
3
3
 
4
- Scans './modules' and './assemblies' for AsciiDoc files not referenced by any other AsciiDoc file in the project. Optionally archives and deletes them.
4
+ Automatically discovers and scans 'modules' and 'assemblies' directories for AsciiDoc files
5
+ not referenced by any other AsciiDoc file in the project. Optionally archives and deletes them.
5
6
 
6
7
  For full documentation and usage examples, see archive_unused_files.md in this directory.
7
8
  """
8
9
 
9
10
  import argparse
10
11
  from doc_utils.unused_adoc import find_unused_adoc
12
+ from doc_utils.version_check import check_version_on_startup
11
13
  from doc_utils.file_utils import parse_exclude_list_file
14
+ from doc_utils.version import __version__
12
15
 
16
+ from doc_utils.spinner import Spinner
13
17
  def main():
14
- parser = argparse.ArgumentParser(description='Archive unused AsciiDoc files.')
18
+ # Check for updates (non-blocking, won't interfere with tool operation)
19
+ check_version_on_startup()
20
+ parser = argparse.ArgumentParser(
21
+ description='Archive unused AsciiDoc files.',
22
+ epilog='By default, automatically discovers all modules and assemblies directories in the repository.'
23
+ )
15
24
  parser.add_argument('--archive', action='store_true', help='Move the files to a dated zip in the archive directory.')
25
+ parser.add_argument('--commented', action='store_true', help='Include files that are referenced only in commented lines in the archive operation.')
26
+ parser.add_argument('--scan-dir', action='append', default=[], help='Specific directory to scan (can be used multiple times). If not specified, auto-discovers directories.')
16
27
  parser.add_argument('--exclude-dir', action='append', default=[], help='Directory to exclude (can be used multiple times).')
17
28
  parser.add_argument('--exclude-file', action='append', default=[], help='File to exclude (can be used multiple times).')
18
29
  parser.add_argument('--exclude-list', type=str, help='Path to a file containing directories or files to exclude, one per line.')
30
+ parser.add_argument('--version', action='version', version=f'%(prog)s {__version__}')
19
31
  args = parser.parse_args()
20
32
 
21
- scan_dirs = ['./modules', './modules/rn', './assemblies']
33
+ # Use provided scan directories or None for auto-discovery
34
+ scan_dirs = args.scan_dir if args.scan_dir else None
22
35
  archive_dir = './archive'
23
36
 
24
37
  exclude_dirs = list(args.exclude_dir)
25
38
  exclude_files = list(args.exclude_file)
26
-
39
+
27
40
  if args.exclude_list:
28
41
  list_dirs, list_files = parse_exclude_list_file(args.exclude_list)
29
42
  exclude_dirs.extend(list_dirs)
30
43
  exclude_files.extend(list_files)
31
44
 
32
- find_unused_adoc(scan_dirs, archive_dir, args.archive, exclude_dirs, exclude_files)
45
+ find_unused_adoc(scan_dirs, archive_dir, args.archive, exclude_dirs, exclude_files, args.commented)
33
46
 
34
47
  if __name__ == '__main__':
35
48
  main()
archive_unused_images.py CHANGED
@@ -8,14 +8,21 @@ For full documentation and usage examples, see archive_unused_files.md in this d
8
8
 
9
9
  import argparse
10
10
  from doc_utils.unused_images import find_unused_images
11
+ from doc_utils.version_check import check_version_on_startup
11
12
  from doc_utils.file_utils import parse_exclude_list_file
13
+ from doc_utils.version import __version__
12
14
 
15
+ from doc_utils.spinner import Spinner
13
16
  def main():
17
+ # Check for updates (non-blocking, won't interfere with tool operation)
18
+ check_version_on_startup()
14
19
  parser = argparse.ArgumentParser(description='Archive unused image files.')
15
20
  parser.add_argument('--archive', action='store_true', help='Move the files to a dated zip in the archive directory.')
21
+ parser.add_argument('--commented', action='store_true', help='Include images that are referenced only in commented lines in the archive operation.')
16
22
  parser.add_argument('--exclude-dir', action='append', default=[], help='Directory to exclude (can be used multiple times).')
17
23
  parser.add_argument('--exclude-file', action='append', default=[], help='File to exclude (can be used multiple times).')
18
24
  parser.add_argument('--exclude-list', type=str, help='Path to a file containing directories or files to exclude, one per line.')
25
+ parser.add_argument('--version', action='version', version=f'%(prog)s {__version__}')
19
26
  args = parser.parse_args()
20
27
 
21
28
  scan_dirs = ['.']
@@ -23,13 +30,13 @@ def main():
23
30
 
24
31
  exclude_dirs = list(args.exclude_dir)
25
32
  exclude_files = list(args.exclude_file)
26
-
33
+
27
34
  if args.exclude_list:
28
35
  list_dirs, list_files = parse_exclude_list_file(args.exclude_list)
29
36
  exclude_dirs.extend(list_dirs)
30
37
  exclude_files.extend(list_files)
31
38
 
32
- find_unused_images(scan_dirs, archive_dir, args.archive, exclude_dirs, exclude_files)
39
+ find_unused_images(scan_dirs, archive_dir, args.archive, exclude_dirs, exclude_files, args.commented)
33
40
 
34
41
  if __name__ == '__main__':
35
42
  main()
@@ -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,103 @@
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], table_title: str = "") -> 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
+ table_title: Optional table title (e.g., ".Descriptions of delete event")
37
+ Will be converted to lead-in sentence
38
+ explanations: Dict mapping callout numbers to Callout objects
39
+
40
+ Returns:
41
+ List of strings representing the bulleted list
42
+ """
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
50
+
51
+ # Process each group (which may contain one or more callouts)
52
+ for group in callout_groups:
53
+ code_line = group.code_line
54
+ callout_nums = group.callout_numbers
55
+
56
+ # Check if this is a user-replaceable value (contains angle brackets but not heredoc)
57
+ # User values are single words/phrases in angle brackets like <my-value>
58
+ user_values = BulletListConverter.USER_VALUE_PATTERN.findall(code_line)
59
+
60
+ if user_values and len(user_values) == 1 and len(code_line) < 100:
61
+ # This looks like a user-replaceable value placeholder
62
+ # Format the value (ensure it has angle brackets)
63
+ user_value = user_values[0]
64
+ if not user_value.startswith('<'):
65
+ user_value = f'<{user_value}>'
66
+ if not user_value.endswith('>'):
67
+ user_value = f'{user_value}>'
68
+ term = f'`{user_value}`'
69
+ else:
70
+ # This is a code line - strip whitespace before wrapping in backticks
71
+ term = f'`{code_line.strip()}`'
72
+
73
+ # Collect all explanations for this group
74
+ all_explanation_lines = []
75
+ for idx, callout_num in enumerate(callout_nums):
76
+ explanation = explanations[callout_num]
77
+
78
+ # Add explanation lines, prepending "Optional. " to first line if needed
79
+ for line_idx, line in enumerate(explanation.lines):
80
+ if line_idx == 0 and explanation.is_optional:
81
+ all_explanation_lines.append(f'Optional. {line}')
82
+ else:
83
+ all_explanation_lines.append(line)
84
+
85
+ # If there are more callouts in this group, add a line break
86
+ if idx < len(callout_nums) - 1:
87
+ all_explanation_lines.append('')
88
+
89
+ # Format as bullet point: * `term`: explanation
90
+ # First line uses the bullet marker
91
+ lines.append(f'* {term}: {all_explanation_lines[0]}')
92
+
93
+ # Continuation lines (if any) are indented to align with first line
94
+ for continuation_line in all_explanation_lines[1:]:
95
+ if continuation_line: # Skip empty lines for now
96
+ lines.append(f' {continuation_line}')
97
+ else:
98
+ lines.append('')
99
+
100
+ # Add blank line after each bullet point
101
+ lines.append('')
102
+
103
+ 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,134 @@
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], table_title: str = "",
20
+ definition_prefix: str = "") -> List[str]:
21
+ """
22
+ Create definition list from callout groups and explanations.
23
+
24
+ For callouts with user-replaceable values in angle brackets, uses those.
25
+ For callouts without values, uses the actual code line as the term.
26
+
27
+ When multiple callouts share the same code line (same group), their
28
+ explanations are merged using AsciiDoc list continuation (+).
29
+
30
+ Args:
31
+ callout_groups: List of CalloutGroup objects from code block
32
+ explanations: Dict mapping callout numbers to Callout objects
33
+ table_title: Optional table title (e.g., ".Descriptions of delete event")
34
+ Will be converted to lead-in sentence (e.g., "Descriptions of delete event, where:")
35
+ definition_prefix: Optional prefix to add before each definition (e.g., "Specifies ")
36
+
37
+ Returns:
38
+ List of strings representing the definition list
39
+ """
40
+ # Convert table title to lead-in sentence if present
41
+ if table_title:
42
+ # Remove leading dot and trailing period if present
43
+ title_text = table_title.lstrip('.').rstrip('.')
44
+ lines = [f'\n{title_text}, where:']
45
+ else:
46
+ lines = ['\nwhere:']
47
+
48
+ # Process each group (which may contain one or more callouts)
49
+ for group in callout_groups:
50
+ code_line = group.code_line
51
+ callout_nums = group.callout_numbers
52
+
53
+ # COMMENTED OUT: User-replaceable value detection causes false positives
54
+ # with Java generics (e.g., <MyEntity, Integer>) and other valid syntax
55
+ # that uses angle brackets. Always use the full code line as the term.
56
+ #
57
+ # # Check if this is a user-replaceable value (contains angle brackets but not heredoc)
58
+ # # User values are single words/phrases in angle brackets like <my-value>
59
+ # user_values = DefListConverter.USER_VALUE_PATTERN.findall(code_line)
60
+ #
61
+ # if user_values and len(user_values) == 1 and len(code_line) < 100:
62
+ # # This looks like a user-replaceable value placeholder
63
+ # # Format the value (ensure it has angle brackets)
64
+ # user_value = user_values[0]
65
+ # if not user_value.startswith('<'):
66
+ # user_value = f'<{user_value}>'
67
+ # if not user_value.endswith('>'):
68
+ # user_value = f'{user_value}>'
69
+ # term = f'`{user_value}`'
70
+ # else:
71
+ # # This is a code line - strip whitespace before wrapping in backticks
72
+ # term = f'`{code_line.strip()}`'
73
+
74
+ # Always use the full code line - strip whitespace before wrapping in backticks
75
+ term = f'`{code_line.strip()}`'
76
+
77
+ # Add blank line before each term
78
+ lines.append('')
79
+ lines.append(f'{term}::')
80
+
81
+ # Add explanations for all callouts in this group
82
+ for idx, callout_num in enumerate(callout_nums):
83
+ explanation = explanations[callout_num]
84
+
85
+ # If this is not the first explanation in the group, add continuation marker
86
+ if idx > 0:
87
+ lines.append('+')
88
+
89
+ # Add explanation lines, prepending "Optional. " to first line if needed
90
+ # Handle blank lines and conditionals by inserting continuation markers
91
+ need_continuation = False
92
+ had_content = False # Track if we've output any non-conditional content
93
+
94
+ for line_idx, line in enumerate(explanation.lines):
95
+ stripped = line.strip()
96
+
97
+ # Check if this is a blank line
98
+ if stripped == '':
99
+ # Next non-blank line will need a continuation marker
100
+ need_continuation = True
101
+ continue # Skip blank lines
102
+
103
+ # Check if this is a conditional directive
104
+ is_conditional = stripped.startswith(('ifdef::', 'ifndef::', 'endif::'))
105
+
106
+ # Add continuation marker if:
107
+ # 1. Previous line was blank (need_continuation=True), OR
108
+ # 2. This is a conditional and we've had content before (need separator)
109
+ if need_continuation or (is_conditional and had_content and line_idx > 0):
110
+ lines.append('+')
111
+ need_continuation = False
112
+
113
+ # Add the line with optional prefix
114
+ if line_idx == 0:
115
+ # First line of definition
116
+ if explanation.is_optional:
117
+ # Optional marker takes precedence, then prefix
118
+ if definition_prefix:
119
+ lines.append(f'Optional. {definition_prefix}{line}')
120
+ else:
121
+ lines.append(f'Optional. {line}')
122
+ elif definition_prefix:
123
+ # Add prefix to first line
124
+ lines.append(f'{definition_prefix}{line}')
125
+ else:
126
+ lines.append(line)
127
+ else:
128
+ lines.append(line)
129
+
130
+ # Track that we've output content (not just conditionals)
131
+ if not is_conditional:
132
+ had_content = True
133
+
134
+ return lines