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,567 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ convert-callouts-interactive - Interactive AsciiDoc callout converter
4
+
5
+ Interactively converts code blocks with callouts, prompting for format choice
6
+ (definition list, bulleted list, or inline comments) for each code block.
7
+ """
8
+
9
+ import sys
10
+ import argparse
11
+ from pathlib import Path
12
+ from typing import List, Tuple, Optional
13
+
14
+ # Import from callout_lib
15
+ from callout_lib import (
16
+ CalloutDetector,
17
+ DefListConverter,
18
+ BulletListConverter,
19
+ CommentConverter,
20
+ CodeBlock,
21
+ )
22
+
23
+ # Import version
24
+ from doc_utils.version import __version__
25
+
26
+
27
+ # Colors for output
28
+ class Colors:
29
+ RED = '\033[0;31m'
30
+ GREEN = '\033[0;32m'
31
+ YELLOW = '\033[1;33m'
32
+ BLUE = '\033[0;34m'
33
+ CYAN = '\033[0;36m'
34
+ MAGENTA = '\033[0;35m'
35
+ BOLD = '\033[1m'
36
+ NC = '\033[0m' # No Color
37
+
38
+
39
+ def print_colored(message: str, color: str = Colors.NC) -> None:
40
+ """Print message with color"""
41
+ print(f"{color}{message}{Colors.NC}")
42
+
43
+
44
+ def print_separator(char: str = '=', length: int = 80, color: str = Colors.CYAN) -> None:
45
+ """Print a separator line"""
46
+ print_colored(char * length, color)
47
+
48
+
49
+ class InteractiveCalloutConverter:
50
+ """Interactive converter for AsciiDoc callouts."""
51
+
52
+ def __init__(self, dry_run: bool = False, context_lines: int = 3):
53
+ self.dry_run = dry_run
54
+ self.context_lines = context_lines
55
+ self.detector = CalloutDetector()
56
+ self.changes_made = 0
57
+ self.warnings = []
58
+ self.apply_to_all = None # None, 'deflist', 'bullets', 'comments', or 'skip'
59
+
60
+ def show_code_block_preview(self, lines: List[str], block: CodeBlock, file_path: Path) -> None:
61
+ """Display a preview of the code block with context."""
62
+ print_separator('=', 80, Colors.CYAN)
63
+ print_colored(f"\nFile: {file_path}", Colors.BOLD)
64
+ print_colored(f"Code block at lines {block.start_line + 1}-{block.end_line + 1}", Colors.BLUE)
65
+ if block.language:
66
+ print_colored(f"Language: {block.language}", Colors.BLUE)
67
+ print()
68
+
69
+ # Show context before
70
+ context_start = max(0, block.start_line - self.context_lines)
71
+ if context_start < block.start_line:
72
+ print_colored(" ... context before ...", Colors.CYAN)
73
+ for i in range(context_start, block.start_line):
74
+ print_colored(f" {i + 1:4d} | {lines[i]}", Colors.CYAN)
75
+
76
+ # Show the code block itself
77
+ # Check if has [source] prefix
78
+ has_source_prefix = self.detector.CODE_BLOCK_START.match(lines[block.start_line])
79
+ if has_source_prefix:
80
+ print_colored(f" {block.start_line + 1:4d} | {lines[block.start_line]}", Colors.YELLOW)
81
+ print_colored(f" {block.start_line + 2:4d} | {lines[block.start_line + 1]}", Colors.YELLOW)
82
+ content_start_line = block.start_line + 2
83
+ else:
84
+ print_colored(f" {block.start_line + 1:4d} | {lines[block.start_line]}", Colors.YELLOW)
85
+ content_start_line = block.start_line + 1
86
+
87
+ # Show code content with callouts highlighted
88
+ for i, line in enumerate(block.content):
89
+ line_num = content_start_line + i
90
+ if '<' in line and '>' in line and any(f'<{n}>' in line for n in range(1, 100)):
91
+ # Highlight lines with callouts
92
+ print_colored(f" {line_num + 1:4d} | {line}", Colors.MAGENTA)
93
+ else:
94
+ print(f" {line_num + 1:4d} | {line}")
95
+
96
+ # Show closing delimiter
97
+ print_colored(f" {block.end_line + 1:4d} | {lines[block.end_line]}", Colors.YELLOW)
98
+
99
+ # Show context after (callout explanations)
100
+ context_end = min(len(lines), block.end_line + 1 + self.context_lines + 10) # More context for explanations
101
+ if block.end_line + 1 < context_end:
102
+ print_colored(" ... callout explanations ...", Colors.CYAN)
103
+ for i in range(block.end_line + 1, context_end):
104
+ if i >= len(lines):
105
+ break
106
+ # Stop showing context if we hit a new section
107
+ if lines[i].strip() and lines[i].startswith('='):
108
+ break
109
+ print_colored(f" {i + 1:4d} | {lines[i]}", Colors.CYAN)
110
+ print()
111
+
112
+ def get_user_choice_for_long_comments(self, block: CodeBlock, long_warnings) -> Optional[str]:
113
+ """
114
+ Prompt user for choice when comments are too long.
115
+ Returns: 'shorten', 'deflist', 'bullets', 'skip', or None for quit
116
+ """
117
+ print_colored("\n⚠️ WARNING: Long Comment Detected", Colors.YELLOW)
118
+ print()
119
+ for lw in long_warnings:
120
+ print_colored(f" Callout <{lw.callout_num}>: {lw.length} characters", Colors.YELLOW)
121
+ print_colored(f" Text: {lw.text[:100]}{'...' if len(lw.text) > 100 else ''}", Colors.CYAN)
122
+ print()
123
+
124
+ print("This explanation is too long for a readable inline comment.")
125
+ print("\nWhat would you like to do?")
126
+ print(" [s] Use Shortened version (first sentence only)")
127
+ print(" [d] Use Definition list format instead")
128
+ print(" [b] Use Bulleted list format instead")
129
+ print(" [k] Skip this block")
130
+ print(" [q] Skip current file")
131
+ print(" [Q] Quit script entirely (Ctrl+C)")
132
+
133
+ while True:
134
+ try:
135
+ choice = input("\nYour choice [s/d/b/k/q/Q]: ").strip()
136
+
137
+ if choice in ['Q', 'QUIT', 'EXIT']:
138
+ # Quit script entirely
139
+ print_colored("\nQuitting script...", Colors.YELLOW)
140
+ sys.exit(0)
141
+ elif choice.lower() in ['q', 'quit', 'exit']:
142
+ # Skip current file
143
+ return None
144
+ elif choice.lower() in ['s', 'shorten', 'short']:
145
+ return 'shorten'
146
+ elif choice.lower() in ['d', 'deflist']:
147
+ return 'deflist'
148
+ elif choice.lower() in ['b', 'bullets', 'bullet']:
149
+ return 'bullets'
150
+ elif choice.lower() in ['k', 'skip']:
151
+ return 'skip'
152
+ else:
153
+ print_colored("Invalid choice. Please enter s, d, b, k, q, or Q.", Colors.RED)
154
+
155
+ except (KeyboardInterrupt, EOFError):
156
+ print()
157
+ return None
158
+
159
+ def get_user_choice(self, block_num: int, total_blocks: int) -> Optional[str]:
160
+ """
161
+ Prompt user for conversion choice.
162
+ Returns: 'deflist', 'bullets', 'comments', 'skip', or None for quit
163
+ """
164
+ print_colored(f"\n[Code block {block_num}/{total_blocks}]", Colors.BOLD)
165
+
166
+ # Check if user wants to apply same choice to all
167
+ if self.apply_to_all:
168
+ print_colored(f"Applying previous choice to all: {self.apply_to_all}", Colors.GREEN)
169
+ return self.apply_to_all
170
+
171
+ print("\nChoose conversion format:")
172
+ print(" [d] Definition list (where:)")
173
+ print(" [b] Bulleted list")
174
+ print(" [c] Inline comments")
175
+ print(" [s] Skip this block")
176
+ print(" [a] Apply choice to All remaining blocks")
177
+ print(" [q] Skip current file")
178
+ print(" [Q] Quit script entirely (Ctrl+C)")
179
+
180
+ while True:
181
+ try:
182
+ choice = input("\nYour choice [d/b/c/s/a/q/Q]: ").strip()
183
+
184
+ if choice in ['Q', 'QUIT', 'EXIT']:
185
+ # Quit script entirely
186
+ print_colored("\nQuitting script...", Colors.YELLOW)
187
+ sys.exit(0)
188
+ elif choice.lower() in ['q', 'quit', 'exit']:
189
+ # Skip current file
190
+ return None
191
+ elif choice.lower() in ['s', 'skip']:
192
+ return 'skip'
193
+ elif choice.lower() in ['d', 'deflist']:
194
+ return 'deflist'
195
+ elif choice.lower() in ['b', 'bullets', 'bullet']:
196
+ return 'bullets'
197
+ elif choice.lower() in ['c', 'comments', 'comment']:
198
+ return 'comments'
199
+ elif choice.lower() in ['a', 'all']:
200
+ # Ask for the format to apply to all
201
+ print("\nWhat format should be applied to all remaining blocks?")
202
+ print(" [d] Definition list")
203
+ print(" [b] Bulleted list")
204
+ print(" [c] Inline comments")
205
+ print(" [s] Skip all")
206
+ format_choice = input("Format [d/b/c/s]: ").strip().lower()
207
+
208
+ if format_choice in ['d', 'deflist']:
209
+ self.apply_to_all = 'deflist'
210
+ return 'deflist'
211
+ elif format_choice in ['b', 'bullets', 'bullet']:
212
+ self.apply_to_all = 'bullets'
213
+ return 'bullets'
214
+ elif format_choice in ['c', 'comments', 'comment']:
215
+ self.apply_to_all = 'comments'
216
+ return 'comments'
217
+ elif format_choice in ['s', 'skip']:
218
+ self.apply_to_all = 'skip'
219
+ return 'skip'
220
+ else:
221
+ print_colored("Invalid choice. Please try again.", Colors.RED)
222
+ else:
223
+ print_colored("Invalid choice. Please enter d, b, c, s, a, or q.", Colors.RED)
224
+
225
+ except (KeyboardInterrupt, EOFError):
226
+ print()
227
+ return None
228
+
229
+ def convert_file(self, input_file: Path) -> Tuple[int, bool]:
230
+ """
231
+ Interactively convert callouts in a file.
232
+ Returns tuple of (number of conversions, whether file was modified).
233
+ """
234
+ # Read input file
235
+ try:
236
+ with open(input_file, 'r', encoding='utf-8') as f:
237
+ lines = [line.rstrip('\n') for line in f]
238
+ except Exception as e:
239
+ print_colored(f"Error reading {input_file}: {e}", Colors.RED)
240
+ return 0, False
241
+
242
+ # Find all code blocks with callouts
243
+ all_blocks = self.detector.find_code_blocks(lines)
244
+ blocks_with_callouts = []
245
+
246
+ for block in all_blocks:
247
+ callout_groups = self.detector.extract_callouts_from_code(block.content)
248
+ if callout_groups:
249
+ blocks_with_callouts.append(block)
250
+
251
+ if not blocks_with_callouts:
252
+ print(f"No code blocks with callouts found in {input_file}")
253
+ return 0, False
254
+
255
+ print_colored(f"\n{'='*80}", Colors.GREEN)
256
+ print_colored(f"Processing: {input_file}", Colors.BOLD)
257
+ print_colored(f"Found {len(blocks_with_callouts)} code block(s) with callouts", Colors.GREEN)
258
+ print_colored(f"{'='*80}\n", Colors.GREEN)
259
+
260
+ # Process blocks and collect conversions
261
+ conversions = [] # List of (block, format_choice) tuples
262
+ total_blocks = len(blocks_with_callouts)
263
+
264
+ for idx, block in enumerate(blocks_with_callouts, 1):
265
+ # Show preview
266
+ self.show_code_block_preview(lines, block, input_file)
267
+
268
+ # Get user choice
269
+ choice = self.get_user_choice(idx, total_blocks)
270
+
271
+ if choice is None:
272
+ print_colored("\nSkipping remaining blocks in this file.", Colors.YELLOW)
273
+ return 0, False
274
+ elif choice == 'skip':
275
+ print_colored("Skipping this block.\n", Colors.YELLOW)
276
+ continue
277
+
278
+ # If user chose comments, check for long comments first
279
+ if choice == 'comments':
280
+ # Extract callouts to check comment lengths
281
+ callout_groups = self.detector.extract_callouts_from_code(block.content)
282
+ explanations, _ = self.detector.extract_callout_explanations(lines, block.end_line)
283
+
284
+ # Check for long comments
285
+ long_warnings = CommentConverter.check_comment_lengths(
286
+ explanations, block.language, max_length=120
287
+ )
288
+
289
+ if long_warnings:
290
+ # Prompt user for what to do with long comments
291
+ long_choice = self.get_user_choice_for_long_comments(block, long_warnings)
292
+
293
+ if long_choice is None:
294
+ print_colored("\nSkipping remaining blocks in this file.", Colors.YELLOW)
295
+ return 0, False
296
+ elif long_choice == 'skip':
297
+ print_colored("Skipping this block.\n", Colors.YELLOW)
298
+ continue
299
+ elif long_choice == 'shorten':
300
+ # Store that we want shortened comments
301
+ conversions.append((block, 'comments-shorten'))
302
+ print_colored("Will convert to: inline comments (shortened)\n", Colors.GREEN)
303
+ continue
304
+ else:
305
+ # User chose deflist or bullets instead
306
+ choice = long_choice
307
+ print_colored(f"Will convert to: {choice} instead\n", Colors.GREEN)
308
+
309
+ conversions.append((block, choice))
310
+ print_colored(f"Will convert to: {choice}\n", Colors.GREEN)
311
+
312
+ if not conversions:
313
+ print("No blocks selected for conversion.")
314
+ return 0, False
315
+
316
+ # Apply conversions (in reverse order to maintain line numbers)
317
+ new_lines = lines.copy()
318
+
319
+ for block, format_choice in reversed(conversions):
320
+ # Extract callouts
321
+ callout_groups = self.detector.extract_callouts_from_code(block.content)
322
+ explanations, explanation_end = self.detector.extract_callout_explanations(new_lines, block.end_line)
323
+
324
+ if not explanations:
325
+ # Get callout numbers for warning message
326
+ all_callout_nums = []
327
+ for group in callout_groups:
328
+ all_callout_nums.extend(group.callout_numbers)
329
+
330
+ warning_msg = (
331
+ f"WARNING: {input_file.name} line {block.start_line + 1}: "
332
+ f"Code block has callouts {sorted(set(all_callout_nums))} but no explanations found after it. "
333
+ f"This may indicate: explanations are shared with another code block, "
334
+ f"explanations are in an unexpected location, or documentation error (missing explanations). "
335
+ f"Consider reviewing this block manually."
336
+ )
337
+ print_colored(warning_msg, Colors.YELLOW)
338
+ self.warnings.append(warning_msg)
339
+ continue
340
+
341
+ # Validate
342
+ is_valid, code_nums, explanation_nums = self.detector.validate_callouts(callout_groups, explanations)
343
+ if not is_valid:
344
+ warning_msg = (
345
+ f"WARNING: {input_file.name} lines {block.start_line + 1}-{block.end_line + 1}: "
346
+ f"Callout mismatch: code has {sorted(code_nums)}, explanations have {sorted(explanation_nums)}"
347
+ )
348
+ print_colored(warning_msg, Colors.YELLOW)
349
+ self.warnings.append(warning_msg)
350
+ continue
351
+
352
+ # Convert based on choice
353
+ if format_choice == 'comments' or format_choice == 'comments-shorten':
354
+ shorten = (format_choice == 'comments-shorten')
355
+ converted_content, _ = CommentConverter.convert(
356
+ block.content, callout_groups, explanations, block.language,
357
+ max_length=None, shorten_long=shorten
358
+ )
359
+ output_list = []
360
+ else:
361
+ converted_content = self.detector.remove_callouts_from_code(block.content)
362
+ if format_choice == 'bullets':
363
+ output_list = BulletListConverter.convert(callout_groups, explanations, self.detector.last_table_title)
364
+ else: # deflist
365
+ output_list = DefListConverter.convert(callout_groups, explanations, self.detector.last_table_title)
366
+
367
+ # Replace in document
368
+ has_source_prefix = self.detector.CODE_BLOCK_START.match(new_lines[block.start_line])
369
+ if has_source_prefix:
370
+ content_start = block.start_line + 2
371
+ else:
372
+ content_start = block.start_line + 1
373
+ content_end = block.end_line
374
+
375
+ if format_choice == 'comments':
376
+ # Keep everything, just replace code content
377
+ new_section = (
378
+ new_lines[:content_start] +
379
+ converted_content +
380
+ new_lines[content_end:]
381
+ )
382
+ else:
383
+ # Remove old explanations, add new list
384
+ new_section = (
385
+ new_lines[:content_start] +
386
+ converted_content +
387
+ [new_lines[content_end]] +
388
+ output_list +
389
+ new_lines[explanation_end + 1:]
390
+ )
391
+
392
+ new_lines = new_section
393
+ self.changes_made += 1
394
+
395
+ # Write output
396
+ total_conversions = len(conversions)
397
+ if total_conversions > 0 and not self.dry_run:
398
+ try:
399
+ with open(input_file, 'w', encoding='utf-8') as f:
400
+ f.write('\n'.join(new_lines) + '\n')
401
+ print_colored(f"\n✓ Saved changes to {input_file}", Colors.GREEN)
402
+ except Exception as e:
403
+ print_colored(f"Error writing {input_file}: {e}", Colors.RED)
404
+ return 0, False
405
+ elif self.dry_run:
406
+ print_colored(f"\n[DRY RUN] Would save {total_conversions} conversion(s) to {input_file}", Colors.YELLOW)
407
+
408
+ return total_conversions, total_conversions > 0
409
+
410
+
411
+ def find_adoc_files(path: Path, exclude_dirs: List[str] = None) -> List[Path]:
412
+ """Find all .adoc files in the given path."""
413
+ adoc_files = []
414
+ exclude_dirs = exclude_dirs or []
415
+
416
+ # Always exclude .vale directory
417
+ if '.vale' not in exclude_dirs:
418
+ exclude_dirs.append('.vale')
419
+
420
+ if path.is_file():
421
+ if path.suffix == '.adoc':
422
+ adoc_files.append(path)
423
+ elif path.is_dir():
424
+ for adoc_file in path.rglob('*.adoc'):
425
+ if any(excl in str(adoc_file) for excl in exclude_dirs):
426
+ continue
427
+ adoc_files.append(adoc_file)
428
+
429
+ return sorted(adoc_files)
430
+
431
+
432
+ def main():
433
+ """Main entry point"""
434
+ parser = argparse.ArgumentParser(
435
+ description='Interactively convert AsciiDoc callouts to various formats',
436
+ formatter_class=argparse.RawDescriptionHelpFormatter,
437
+ epilog="""
438
+ Interactive AsciiDoc callout converter.
439
+
440
+ This tool scans .adoc files for code blocks with callouts and prompts you
441
+ to choose the conversion format for each block individually:
442
+ - Definition list (where:)
443
+ - Bulleted list
444
+ - Inline comments
445
+
446
+ For each code block with callouts, you'll see:
447
+ - File name and location
448
+ - Code block preview with context
449
+ - Callout explanations
450
+
451
+ Then choose how to convert that specific block, or apply a choice to all
452
+ remaining blocks.
453
+
454
+ Examples:
455
+ %(prog)s myfile.adoc # Process single file interactively
456
+ %(prog)s modules/ # Process all .adoc files in directory
457
+ %(prog)s --dry-run modules/ # Preview without making changes
458
+ %(prog)s --context 5 myfile.adoc # Show 5 lines of context
459
+ """
460
+ )
461
+
462
+ parser.add_argument(
463
+ '--version',
464
+ action='version',
465
+ version=f'%(prog)s {__version__}'
466
+ )
467
+ parser.add_argument(
468
+ 'path',
469
+ nargs='?',
470
+ default='.',
471
+ help='File or directory to process (default: current directory)'
472
+ )
473
+ parser.add_argument(
474
+ '-n', '--dry-run',
475
+ action='store_true',
476
+ help='Preview changes without modifying files'
477
+ )
478
+ parser.add_argument(
479
+ '-c', '--context',
480
+ type=int,
481
+ default=3,
482
+ help='Number of context lines to show before/after code blocks (default: 3)'
483
+ )
484
+ parser.add_argument(
485
+ '--exclude-dir',
486
+ action='append',
487
+ dest='exclude_dirs',
488
+ default=[],
489
+ help='Directory to exclude (can be used multiple times)'
490
+ )
491
+
492
+ args = parser.parse_args()
493
+
494
+ # Convert path to Path object
495
+ target_path = Path(args.path)
496
+
497
+ if not target_path.exists():
498
+ print_colored(f"Error: Path does not exist: {target_path}", Colors.RED)
499
+ sys.exit(1)
500
+
501
+ if args.dry_run:
502
+ print_colored("DRY RUN MODE - No files will be modified\n", Colors.YELLOW)
503
+
504
+ # Find all AsciiDoc files
505
+ adoc_files = find_adoc_files(target_path, args.exclude_dirs)
506
+
507
+ if not adoc_files:
508
+ if target_path.is_file():
509
+ print_colored(f"Error: {target_path} is not an AsciiDoc file (.adoc)", Colors.RED)
510
+ else:
511
+ print(f"No AsciiDoc files found in {target_path}")
512
+ sys.exit(1)
513
+
514
+ if len(adoc_files) > 1:
515
+ print(f"Found {len(adoc_files)} AsciiDoc file(s) to process\n")
516
+
517
+ # Create converter
518
+ converter = InteractiveCalloutConverter(dry_run=args.dry_run, context_lines=args.context)
519
+
520
+ # Process each file
521
+ files_modified = 0
522
+ total_conversions = 0
523
+
524
+ for file_path in adoc_files:
525
+ try:
526
+ conversions, modified = converter.convert_file(file_path)
527
+
528
+ if modified:
529
+ files_modified += 1
530
+ total_conversions += conversions
531
+
532
+ except KeyboardInterrupt:
533
+ print_colored("\n\nScript interrupted by user (Ctrl+C)", Colors.YELLOW)
534
+ sys.exit(0)
535
+ except Exception as e:
536
+ print_colored(f"\nUnexpected error processing {file_path}: {e}", Colors.RED)
537
+ import traceback
538
+ traceback.print_exc()
539
+
540
+ # Summary
541
+ print_separator('=', 80, Colors.GREEN)
542
+ print_colored("\nSummary:", Colors.BOLD)
543
+ print(f" Files processed: {len(adoc_files)}")
544
+ if args.dry_run and files_modified > 0:
545
+ print(f" Would modify: {files_modified} file(s)")
546
+ print(f" Total conversions: {total_conversions}")
547
+ elif files_modified > 0:
548
+ print_colored(f" Files modified: {files_modified}", Colors.GREEN)
549
+ print_colored(f" Total conversions: {total_conversions}", Colors.GREEN)
550
+ else:
551
+ print(" No files modified")
552
+
553
+ if converter.warnings:
554
+ print_colored(f"\n⚠ {len(converter.warnings)} Warning(s):", Colors.YELLOW)
555
+ for warning in converter.warnings:
556
+ print_colored(f" {warning}", Colors.YELLOW)
557
+ print()
558
+ print_colored("Suggestion: Fix the callout mismatches in the files above and rerun this command.", Colors.YELLOW)
559
+
560
+ if args.dry_run and files_modified > 0:
561
+ print_colored("\nDRY RUN - No files were modified", Colors.YELLOW)
562
+
563
+ print_separator('=', 80, Colors.GREEN)
564
+
565
+
566
+ if __name__ == '__main__':
567
+ main()