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
@@ -0,0 +1,628 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ convert-callouts-to-deflist - Convert AsciiDoc callouts to definition list format
4
+
5
+ Converts code blocks with callout-style annotations (<1>, <2>, etc.) to cleaner
6
+ definition list format with "where:" prefix, bulleted list format, or inline comments.
7
+
8
+ This tool automatically scans all .adoc files in the current directory (recursively)
9
+ by default, or you can specify a specific file or directory.
10
+ """
11
+
12
+ import sys
13
+ import argparse
14
+ from pathlib import Path
15
+ from typing import List, Tuple
16
+
17
+ # Import from callout_lib
18
+ from callout_lib import (
19
+ CalloutDetector,
20
+ DefListConverter,
21
+ BulletListConverter,
22
+ CommentConverter,
23
+ )
24
+
25
+ # Import warnings report generator
26
+ from doc_utils.warnings_report import generate_warnings_report
27
+
28
+ # Import version
29
+ from doc_utils.version import __version__
30
+
31
+
32
+ # Colors for output
33
+ class Colors:
34
+ RED = '\033[0;31m'
35
+ GREEN = '\033[0;32m'
36
+ YELLOW = '\033[1;33m'
37
+ NC = '\033[0m' # No Color
38
+
39
+
40
+ def print_colored(message: str, color: str = Colors.NC) -> None:
41
+ """Print message with color"""
42
+ print(f"{color}{message}{Colors.NC}")
43
+
44
+
45
+ class CalloutConverter:
46
+ """Converts callout-style documentation to various formats."""
47
+
48
+ def __init__(self, dry_run: bool = False, verbose: bool = False, output_format: str = 'deflist',
49
+ max_comment_length: int = 120, force: bool = False, definition_prefix: str = ""):
50
+ self.dry_run = dry_run
51
+ self.verbose = verbose
52
+ self.output_format = output_format # 'deflist', 'bullets', or 'comments'
53
+ self.max_comment_length = max_comment_length # Max length for inline comments
54
+ self.force = force # Force strip callouts even with warnings
55
+ self.definition_prefix = definition_prefix # Prefix to add before definitions (e.g., "Specifies ")
56
+ self.changes_made = 0
57
+ self.warnings = [] # Collect warnings for summary
58
+ self.long_comment_warnings = [] # Warnings for comments exceeding max length
59
+
60
+ # Initialize detector and converters
61
+ self.detector = CalloutDetector()
62
+
63
+ def log(self, message: str):
64
+ """Print message if verbose mode is enabled."""
65
+ if self.verbose:
66
+ print(f"[INFO] {message}")
67
+
68
+ def convert_file(self, input_file: Path) -> Tuple[int, bool]:
69
+ """
70
+ Convert callouts in a file to the specified output format.
71
+ Returns tuple of (number of conversions, whether file was modified).
72
+ """
73
+ # Read input file
74
+ try:
75
+ with open(input_file, 'r', encoding='utf-8') as f:
76
+ lines = [line.rstrip('\n') for line in f]
77
+ except Exception as e:
78
+ print_colored(f"Error reading {input_file}: {e}", Colors.RED)
79
+ return 0, False
80
+
81
+ self.log(f"Processing {input_file} ({len(lines)} lines)")
82
+
83
+ # Find all code blocks
84
+ blocks = self.detector.find_code_blocks(lines)
85
+ self.log(f"Found {len(blocks)} code blocks")
86
+
87
+ if not blocks:
88
+ return 0, False
89
+
90
+ # Process blocks in reverse order to maintain line numbers
91
+ new_lines = lines.copy()
92
+ conversions = 0
93
+
94
+ for block in reversed(blocks):
95
+ # Extract callouts from code (returns list of CalloutGroups)
96
+ callout_groups = self.detector.extract_callouts_from_code(block.content)
97
+
98
+ if not callout_groups:
99
+ self.log(f"No callouts in block at line {block.start_line + 1}")
100
+ continue
101
+
102
+ # Extract all callout numbers for logging
103
+ all_callout_nums = []
104
+ for group in callout_groups:
105
+ all_callout_nums.extend(group.callout_numbers)
106
+
107
+ self.log(f"Block at line {block.start_line + 1} has callouts: {all_callout_nums}")
108
+
109
+ # Extract explanations
110
+ explanations, explanation_end = self.detector.extract_callout_explanations(new_lines, block.end_line)
111
+
112
+ if not explanations:
113
+ self.log(f"No explanations found after block at line {block.start_line + 1}")
114
+ # Warn user about code blocks with callouts but no explanations
115
+ warning_msg = (
116
+ f"WARNING: {input_file.name} line {block.start_line + 1}: "
117
+ f"Code block has callouts {sorted(set(all_callout_nums))} but no explanations found after it. "
118
+ f"This may indicate: explanations are shared with another code block, "
119
+ f"explanations are in an unexpected location, or documentation error (missing explanations). "
120
+ f"Consider reviewing this block manually."
121
+ )
122
+ print_colored(warning_msg, Colors.YELLOW)
123
+ self.warnings.append(warning_msg)
124
+
125
+ # In force mode, strip callouts anyway
126
+ if not self.force:
127
+ continue
128
+ else:
129
+ self.log(f"FORCE MODE: Stripping callouts from block at line {block.start_line + 1} despite missing explanations")
130
+
131
+ # Just strip callouts without creating explanation list
132
+ converted_content = self.detector.remove_callouts_from_code(block.content)
133
+
134
+ # Replace in document
135
+ has_source_prefix = self.detector.CODE_BLOCK_START.match(new_lines[block.start_line])
136
+ if has_source_prefix:
137
+ content_start = block.start_line + 2 # After [source] and ----
138
+ else:
139
+ content_start = block.start_line + 1 # After ---- only
140
+ content_end = block.end_line
141
+
142
+ # Build new section with just code (no explanations)
143
+ new_section = (
144
+ new_lines[:content_start] +
145
+ converted_content +
146
+ [new_lines[content_end]] + # Keep closing delimiter
147
+ new_lines[content_end + 1:] # Keep rest of file
148
+ )
149
+
150
+ new_lines = new_section
151
+ conversions += 1
152
+ self.changes_made += 1
153
+ continue
154
+
155
+ # Validate callouts match
156
+ is_valid, code_nums, explanation_nums = self.detector.validate_callouts(callout_groups, explanations)
157
+ if not is_valid and explanations: # Only validate if we have explanations
158
+ # Format warning message with file and line numbers
159
+ line_range = f"{block.start_line + 1}-{block.end_line + 1}"
160
+ warning_msg = (
161
+ f"WARNING: {input_file.name} lines {line_range}: Callout mismatch: "
162
+ f"code has {sorted(code_nums)}, explanations have {sorted(explanation_nums)}"
163
+ )
164
+ print_colored(warning_msg, Colors.YELLOW)
165
+ self.warnings.append(warning_msg)
166
+
167
+ # In force mode, convert anyway
168
+ if not self.force:
169
+ continue
170
+ else:
171
+ self.log(f"FORCE MODE: Converting block at line {block.start_line + 1} despite callout mismatch")
172
+
173
+ self.log(f"Converting block at line {block.start_line + 1}")
174
+
175
+ # Convert based on format option
176
+ use_deflist_fallback = False
177
+ if self.output_format == 'comments':
178
+ # For comments format, replace callouts inline in the code
179
+ converted_content, long_warnings = CommentConverter.convert(
180
+ block.content, callout_groups, explanations, block.language,
181
+ max_length=self.max_comment_length, shorten_long=False
182
+ )
183
+
184
+ # If there are long comment warnings, fall back to definition list
185
+ if long_warnings:
186
+ for lw in long_warnings:
187
+ warning_msg = (
188
+ f"WARNING: {input_file.name} lines {block.start_line + 1}-{block.end_line + 1}: "
189
+ f"Callout <{lw.callout_num}> explanation too long ({lw.length} chars) "
190
+ f"for inline comment (max: {self.max_comment_length}). Falling back to definition list format."
191
+ )
192
+ print_colored(warning_msg, Colors.YELLOW)
193
+ self.warnings.append(warning_msg)
194
+ self.long_comment_warnings.append((input_file.name, block.start_line + 1, lw))
195
+
196
+ # Fall back to definition list
197
+ self.log(f"Falling back to definition list for block at line {block.start_line + 1}")
198
+ converted_content = self.detector.remove_callouts_from_code(block.content)
199
+ output_list = DefListConverter.convert(callout_groups, explanations, self.detector.last_table_title, self.definition_prefix)
200
+ use_deflist_fallback = True
201
+ else:
202
+ output_list = [] # No separate list after code block for comments
203
+ else:
204
+ # For deflist and bullets, remove callouts from code and create separate list
205
+ converted_content = self.detector.remove_callouts_from_code(block.content)
206
+
207
+ if self.output_format == 'bullets':
208
+ output_list = BulletListConverter.convert(callout_groups, explanations, self.detector.last_table_title)
209
+ else: # default to 'deflist'
210
+ output_list = DefListConverter.convert(callout_groups, explanations, self.detector.last_table_title, self.definition_prefix)
211
+
212
+ # Replace in document
213
+ # Check if block has [source] prefix
214
+ has_source_prefix = self.detector.CODE_BLOCK_START.match(new_lines[block.start_line])
215
+ if has_source_prefix:
216
+ content_start = block.start_line + 2 # After [source] and ----
217
+ else:
218
+ content_start = block.start_line + 1 # After ---- only
219
+ content_end = block.end_line
220
+
221
+ # For comments format (without fallback), remove explanations but don't add new list
222
+ # For deflist/bullets format, remove old explanations and add new list
223
+ if self.output_format == 'comments' and not use_deflist_fallback:
224
+ # Remove old callout explanations (list or table format)
225
+ # Find where explanations/table actually starts to preserve content in between
226
+ if self.detector.last_table:
227
+ explanation_start_line = self.detector.last_table.start_line
228
+ else:
229
+ # List format: skip blank lines after code block
230
+ explanation_start_line = block.end_line + 1
231
+ while explanation_start_line < len(new_lines) and not new_lines[explanation_start_line].strip():
232
+ explanation_start_line += 1
233
+
234
+ new_section = (
235
+ new_lines[:content_start] +
236
+ converted_content +
237
+ [new_lines[content_end]] + # Keep closing delimiter
238
+ new_lines[content_end + 1:explanation_start_line] + # Preserve content between code and explanations
239
+ new_lines[explanation_end + 1:] # Skip explanations/table, keep rest
240
+ )
241
+ else:
242
+ # Remove old callout explanations and add new list
243
+ # Find where explanations/table actually starts
244
+ if self.detector.last_table:
245
+ # Table format: preserve content between code block and table start
246
+ explanation_start_line = self.detector.last_table.start_line
247
+ else:
248
+ # List format: skip blank lines after code block
249
+ explanation_start_line = block.end_line + 1
250
+ while explanation_start_line < len(new_lines) and not new_lines[explanation_start_line].strip():
251
+ explanation_start_line += 1
252
+
253
+ # Build the new section
254
+ new_section = (
255
+ new_lines[:content_start] +
256
+ converted_content +
257
+ [new_lines[content_end]] + # Keep closing delimiter
258
+ new_lines[content_end + 1:explanation_start_line] + # Preserve content between code and explanations
259
+ output_list +
260
+ new_lines[explanation_end + 1:]
261
+ )
262
+
263
+ new_lines = new_section
264
+ conversions += 1
265
+ self.changes_made += 1
266
+
267
+ # Write output
268
+ if conversions > 0 and not self.dry_run:
269
+ try:
270
+ with open(input_file, 'w', encoding='utf-8') as f:
271
+ f.write('\n'.join(new_lines) + '\n')
272
+ self.log(f"Wrote {input_file}")
273
+ except Exception as e:
274
+ print_colored(f"Error writing {input_file}: {e}", Colors.RED)
275
+ return 0, False
276
+
277
+ return conversions, conversions > 0
278
+
279
+
280
+ def find_adoc_files(path: Path, exclude_dirs: List[str] = None, exclude_files: List[str] = None) -> List[Path]:
281
+ """
282
+ Find all .adoc files in the given path.
283
+
284
+ Args:
285
+ path: Path to search (file or directory)
286
+ exclude_dirs: List of directory patterns to exclude
287
+ exclude_files: List of file patterns to exclude
288
+
289
+ Returns:
290
+ List of Path objects for .adoc files
291
+ """
292
+ adoc_files = []
293
+ exclude_dirs = exclude_dirs or []
294
+ exclude_files = exclude_files or []
295
+
296
+ # Always exclude .vale directory by default (Vale linter fixtures)
297
+ if '.vale' not in exclude_dirs:
298
+ exclude_dirs.append('.vale')
299
+
300
+ if path.is_file():
301
+ if path.suffix == '.adoc':
302
+ # Check if file should be excluded
303
+ if not any(excl in str(path) for excl in exclude_files):
304
+ adoc_files.append(path)
305
+ elif path.is_dir():
306
+ # Recursively find all .adoc files
307
+ for adoc_file in path.rglob('*.adoc'):
308
+ # Check if in excluded directory
309
+ if any(excl in str(adoc_file) for excl in exclude_dirs):
310
+ continue
311
+ # Check if file should be excluded
312
+ if any(excl in str(adoc_file) for excl in exclude_files):
313
+ continue
314
+ adoc_files.append(adoc_file)
315
+
316
+ return sorted(adoc_files)
317
+
318
+
319
+ def load_exclusion_list(exclusion_file: Path) -> Tuple[List[str], List[str]]:
320
+ """
321
+ Load exclusion list from file.
322
+ Returns tuple of (excluded_dirs, excluded_files).
323
+ """
324
+ excluded_dirs = []
325
+ excluded_files = []
326
+
327
+ try:
328
+ with open(exclusion_file, 'r') as f:
329
+ for line in f:
330
+ line = line.strip()
331
+ # Skip comments and empty lines
332
+ if not line or line.startswith('#'):
333
+ continue
334
+
335
+ # If it ends with /, it's a directory
336
+ if line.endswith('/'):
337
+ excluded_dirs.append(line.rstrip('/'))
338
+ else:
339
+ # Could be file or directory - check if it has extension
340
+ if '.' in Path(line).name:
341
+ excluded_files.append(line)
342
+ else:
343
+ excluded_dirs.append(line)
344
+ except Exception as e:
345
+ print_colored(f"Warning: Could not read exclusion file {exclusion_file}: {e}", Colors.YELLOW)
346
+
347
+ return excluded_dirs, excluded_files
348
+
349
+
350
+ def main():
351
+ """Main entry point"""
352
+ parser = argparse.ArgumentParser(
353
+ description='Convert AsciiDoc callouts to various formats',
354
+ formatter_class=argparse.RawDescriptionHelpFormatter,
355
+ epilog="""
356
+ Convert AsciiDoc callout-style documentation to various formats.
357
+
358
+ This script identifies code blocks with callout numbers (<1>, <2>, etc.) and their
359
+ corresponding explanation lines, then converts them to your chosen format.
360
+
361
+ Formats:
362
+ deflist - Definition list with "where:" prefix (default)
363
+ bullets - Bulleted list format
364
+ comments - Inline comments within code (removes separate explanations)
365
+
366
+ Examples:
367
+ %(prog)s # Process all .adoc files in current directory
368
+ %(prog)s modules/ # Process all .adoc files in modules/
369
+ %(prog)s assemblies/my-guide.adoc # Process single file
370
+ %(prog)s --dry-run modules/ # Preview changes without modifying
371
+ %(prog)s --format bullets modules/ # Convert to bulleted list format
372
+ %(prog)s --format comments src/ # Convert to inline comments
373
+ %(prog)s --exclude-dir .vale modules/ # Exclude .vale directory
374
+
375
+ Example transformation (deflist format):
376
+ FROM:
377
+ [source,yaml]
378
+ ----
379
+ name: <my-secret> <1>
380
+ key: <my-key> <2>
381
+ ----
382
+ <1> Secret name
383
+ <2> Key value
384
+
385
+ TO:
386
+ [source,yaml]
387
+ ----
388
+ name: <my-secret>
389
+ key: <my-key>
390
+ ----
391
+
392
+ where:
393
+
394
+ `<my-secret>`::
395
+ Secret name
396
+
397
+ `<my-key>`::
398
+ Key value
399
+ """
400
+ )
401
+
402
+ parser.add_argument(
403
+ '--version',
404
+ action='version',
405
+ version=f'%(prog)s {__version__}'
406
+ )
407
+ parser.add_argument(
408
+ 'path',
409
+ nargs='?',
410
+ default='.',
411
+ help='File or directory to process (default: current directory)'
412
+ )
413
+ parser.add_argument(
414
+ '-n', '--dry-run',
415
+ action='store_true',
416
+ help='Show what would be changed without modifying files'
417
+ )
418
+ parser.add_argument(
419
+ '-v', '--verbose',
420
+ action='store_true',
421
+ help='Enable verbose output'
422
+ )
423
+ parser.add_argument(
424
+ '-f', '--format',
425
+ choices=['deflist', 'bullets', 'comments'],
426
+ default='deflist',
427
+ help='Output format: "deflist" for definition list (default), "bullets" for bulleted list, "comments" for inline comments'
428
+ )
429
+ parser.add_argument(
430
+ '--max-comment-length',
431
+ type=int,
432
+ default=120,
433
+ help='Maximum length for inline comments before falling back to definition list (default: 120 characters)'
434
+ )
435
+ parser.add_argument(
436
+ '--exclude-dir',
437
+ action='append',
438
+ dest='exclude_dirs',
439
+ default=[],
440
+ help='Directory to exclude (can be used multiple times)'
441
+ )
442
+ parser.add_argument(
443
+ '--exclude-file',
444
+ action='append',
445
+ dest='exclude_files',
446
+ default=[],
447
+ help='File to exclude (can be used multiple times)'
448
+ )
449
+ parser.add_argument(
450
+ '--exclude-list',
451
+ type=Path,
452
+ help='Path to file containing directories/files to exclude, one per line'
453
+ )
454
+ parser.add_argument(
455
+ '--warnings-report',
456
+ dest='warnings_report',
457
+ action='store_true',
458
+ default=True,
459
+ help='Generate warnings report file (default: enabled)'
460
+ )
461
+ parser.add_argument(
462
+ '--no-warnings-report',
463
+ dest='warnings_report',
464
+ action='store_false',
465
+ help='Disable warnings report file generation'
466
+ )
467
+ parser.add_argument(
468
+ '--warnings-file',
469
+ type=Path,
470
+ default=Path('callout-warnings-report.adoc'),
471
+ help='Path for warnings report file (default: callout-warnings-report.adoc)'
472
+ )
473
+ parser.add_argument(
474
+ '--force',
475
+ action='store_true',
476
+ help='Force strip callouts from code blocks even with warnings (USE WITH CAUTION: only after reviewing and fixing callout issues)'
477
+ )
478
+ parser.add_argument(
479
+ '-s', '--specifies',
480
+ action='store_true',
481
+ help='Add "Specifies " prefix before each definition (only applies to deflist format)'
482
+ )
483
+ parser.add_argument(
484
+ '--prefix',
485
+ type=str,
486
+ default='',
487
+ help='Custom prefix to add before each definition (only applies to deflist format, e.g., "Indicates ")'
488
+ )
489
+
490
+ args = parser.parse_args()
491
+
492
+ # Load exclusion list if provided
493
+ if args.exclude_list:
494
+ if args.exclude_list.exists():
495
+ excluded_dirs, excluded_files = load_exclusion_list(args.exclude_list)
496
+ args.exclude_dirs.extend(excluded_dirs)
497
+ args.exclude_files.extend(excluded_files)
498
+ else:
499
+ print_colored(f"Warning: Exclusion list file not found: {args.exclude_list}", Colors.YELLOW)
500
+
501
+ # Convert path to Path object
502
+ target_path = Path(args.path)
503
+
504
+ # Check if path exists
505
+ if not target_path.exists():
506
+ print_colored(f"Error: Path does not exist: {target_path}", Colors.RED)
507
+ sys.exit(1)
508
+
509
+ # Display dry-run mode message
510
+ if args.dry_run:
511
+ print_colored("DRY RUN MODE - No files will be modified", Colors.YELLOW)
512
+
513
+ # Find all AsciiDoc files
514
+ adoc_files = find_adoc_files(target_path, args.exclude_dirs, args.exclude_files)
515
+
516
+ if not adoc_files:
517
+ if target_path.is_file():
518
+ print_colored(f"Warning: {target_path} is not an AsciiDoc file (.adoc)", Colors.YELLOW)
519
+ else:
520
+ print(f"No AsciiDoc files found in {target_path}")
521
+ print("Processed 0 AsciiDoc file(s)")
522
+ return
523
+
524
+ print(f"Found {len(adoc_files)} AsciiDoc file(s) to process")
525
+
526
+ # If force mode is enabled, show warning and ask for confirmation
527
+ if args.force and not args.dry_run:
528
+ print_colored("\n⚠️ FORCE MODE ENABLED ⚠️", Colors.YELLOW)
529
+ print_colored("This will strip callouts from code blocks even when warnings are present.", Colors.YELLOW)
530
+ print_colored("You should only use this option AFTER:", Colors.YELLOW)
531
+ print_colored(" 1. Reviewing all warnings in the warnings report", Colors.YELLOW)
532
+ print_colored(" 2. Manually fixing callout issues where appropriate", Colors.YELLOW)
533
+ print_colored(" 3. Confirming that remaining warnings are acceptable", Colors.YELLOW)
534
+ print()
535
+ response = input("Are you sure you want to proceed with force mode? (yes/no): ").strip().lower()
536
+ if response not in ['yes', 'y']:
537
+ print_colored("Operation cancelled.", Colors.YELLOW)
538
+ sys.exit(0)
539
+ print()
540
+
541
+ # Determine definition prefix
542
+ definition_prefix = ""
543
+ if args.specifies:
544
+ definition_prefix = "Specifies "
545
+ elif args.prefix:
546
+ definition_prefix = args.prefix
547
+ # Add trailing space if user didn't include one
548
+ if definition_prefix and not definition_prefix.endswith(' '):
549
+ definition_prefix += ' '
550
+
551
+ # Create converter
552
+ converter = CalloutConverter(dry_run=args.dry_run, verbose=args.verbose, output_format=args.format,
553
+ max_comment_length=args.max_comment_length, force=args.force,
554
+ definition_prefix=definition_prefix)
555
+
556
+ # Process each file
557
+ files_processed = 0
558
+ files_modified = 0
559
+ total_conversions = 0
560
+
561
+ for file_path in adoc_files:
562
+ try:
563
+ conversions, modified = converter.convert_file(file_path)
564
+
565
+ if modified:
566
+ files_modified += 1
567
+ total_conversions += conversions
568
+ if args.dry_run:
569
+ print_colored(f"Would modify: {file_path} ({conversions} code block(s))", Colors.YELLOW)
570
+ else:
571
+ print_colored(f"Modified: {file_path} ({conversions} code block(s))", Colors.GREEN)
572
+ elif args.verbose:
573
+ print(f" No callouts found in: {file_path}")
574
+
575
+ files_processed += 1
576
+
577
+ except KeyboardInterrupt:
578
+ print_colored("\nOperation cancelled by user", Colors.YELLOW)
579
+ sys.exit(1)
580
+ except Exception as e:
581
+ print_colored(f"Unexpected error processing {file_path}: {e}", Colors.RED)
582
+ if args.verbose:
583
+ import traceback
584
+ traceback.print_exc()
585
+
586
+ # Summary
587
+ print(f"\nProcessed {files_processed} AsciiDoc file(s)")
588
+ if args.dry_run and files_modified > 0:
589
+ print(f"Would modify {files_modified} file(s) with {total_conversions} code block conversion(s)")
590
+ elif files_modified > 0:
591
+ print_colored(f"Modified {files_modified} file(s) with {total_conversions} code block conversion(s)", Colors.GREEN)
592
+ else:
593
+ print("No files with callouts to convert")
594
+
595
+ # Display warning summary if any warnings were collected
596
+ if converter.warnings:
597
+ # Generate warnings report if enabled
598
+ if args.warnings_report:
599
+ try:
600
+ generate_warnings_report(converter.warnings, args.warnings_file)
601
+ print_colored(f"\n⚠️ {len(converter.warnings)} Warning(s) - See {args.warnings_file} for details", Colors.YELLOW)
602
+ print()
603
+ print_colored(f"Suggestion: Review and fix the callout issues listed in {args.warnings_file}, then rerun this command.", Colors.YELLOW)
604
+ except Exception as e:
605
+ print_colored(f"\n⚠️ {len(converter.warnings)} Warning(s):", Colors.YELLOW)
606
+ print_colored(f"Error generating warnings report: {e}", Colors.RED)
607
+ # Fall back to displaying warnings in console
608
+ for warning in converter.warnings:
609
+ print_colored(f" {warning}", Colors.YELLOW)
610
+ print()
611
+ print_colored("Suggestion: Fix the callout issues listed above and rerun this command.", Colors.YELLOW)
612
+ else:
613
+ # Console-only output (legacy behavior)
614
+ print_colored(f"\n⚠️ {len(converter.warnings)} Warning(s):", Colors.YELLOW)
615
+ for warning in converter.warnings:
616
+ print_colored(f" {warning}", Colors.YELLOW)
617
+ print()
618
+ print_colored("Suggestion: Fix the callout issues listed above and rerun this command.", Colors.YELLOW)
619
+ print()
620
+
621
+ if args.dry_run and files_modified > 0:
622
+ print_colored("DRY RUN - No files were modified", Colors.YELLOW)
623
+
624
+ return 0 if files_processed >= 0 else 1
625
+
626
+
627
+ if __name__ == '__main__':
628
+ sys.exit(main())