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