rolfedh-doc-utils 0.1.37__py3-none-any.whl → 0.1.39__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.
@@ -0,0 +1,288 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ convert-freemarker-to-asciidoc - Convert FreeMarker-templated AsciiDoc to standard AsciiDoc.
4
+
5
+ Converts Keycloak-style FreeMarker template markup in AsciiDoc files to standard
6
+ AsciiDoc format. This tool:
7
+
8
+ - Removes FreeMarker import statements (<#import ...>)
9
+ - Converts <@tmpl.guide> blocks to AsciiDoc title and short description
10
+ - Removes closing </@tmpl.guide> tags
11
+ - Converts <@links.*> macros to AsciiDoc xref cross-references
12
+ - Converts <@kc.*> command macros to code blocks
13
+ - Handles <@profile.*> conditional blocks (community vs product)
14
+ - Removes <@opts.*> option macros
15
+
16
+ Usage:
17
+ convert-freemarker-to-asciidoc # Process current directory
18
+ convert-freemarker-to-asciidoc docs/guides/ # Process specific directory
19
+ convert-freemarker-to-asciidoc --dry-run # Preview changes
20
+ convert-freemarker-to-asciidoc --structure-only # Only convert imports and guide blocks
21
+
22
+ Examples:
23
+ # Preview changes without modifying files
24
+ convert-freemarker-to-asciidoc --dry-run docs/guides/
25
+
26
+ # Convert all .adoc files in current directory
27
+ convert-freemarker-to-asciidoc
28
+
29
+ # Only convert structure (imports, guide blocks) - leave inline macros
30
+ convert-freemarker-to-asciidoc --structure-only docs/guides/server/
31
+
32
+ # Keep product content instead of community content
33
+ convert-freemarker-to-asciidoc --product docs/guides/
34
+ """
35
+
36
+ import argparse
37
+ import sys
38
+ from pathlib import Path
39
+
40
+ from doc_utils.convert_freemarker_to_asciidoc import (
41
+ process_file,
42
+ find_adoc_files,
43
+ has_freemarker_content,
44
+ ConversionStats
45
+ )
46
+ from doc_utils.version_check import check_version_on_startup
47
+ from doc_utils.version import __version__
48
+
49
+
50
+ class Colors:
51
+ """ANSI color codes for terminal output."""
52
+ RED = '\033[0;31m'
53
+ GREEN = '\033[0;32m'
54
+ YELLOW = '\033[1;33m'
55
+ CYAN = '\033[0;36m'
56
+ NC = '\033[0m' # No Color
57
+
58
+
59
+ def print_colored(message: str, color: str = Colors.NC) -> None:
60
+ """Print message with color."""
61
+ print(f"{color}{message}{Colors.NC}")
62
+
63
+
64
+ def aggregate_stats(stats_list: list) -> ConversionStats:
65
+ """Aggregate statistics from multiple conversion results."""
66
+ total = ConversionStats()
67
+ for stats in stats_list:
68
+ total.imports_removed += stats.imports_removed
69
+ total.guide_blocks_converted += stats.guide_blocks_converted
70
+ total.closing_tags_removed += stats.closing_tags_removed
71
+ total.link_macros_converted += stats.link_macros_converted
72
+ total.kc_macros_converted += stats.kc_macros_converted
73
+ total.profile_blocks_handled += stats.profile_blocks_handled
74
+ total.noparse_blocks_handled += stats.noparse_blocks_handled
75
+ total.opts_macros_removed += stats.opts_macros_removed
76
+ total.features_macros_removed += stats.features_macros_removed
77
+ total.other_macros_removed += stats.other_macros_removed
78
+ total.directives_marked += stats.directives_marked
79
+ return total
80
+
81
+
82
+ def format_stats_summary(stats: ConversionStats) -> str:
83
+ """Format statistics as a summary string."""
84
+ parts = []
85
+ if stats.imports_removed > 0:
86
+ parts.append(f"{stats.imports_removed} import(s)")
87
+ if stats.guide_blocks_converted > 0:
88
+ parts.append(f"{stats.guide_blocks_converted} guide block(s)")
89
+ if stats.link_macros_converted > 0:
90
+ parts.append(f"{stats.link_macros_converted} link(s) -> xref")
91
+ if stats.kc_macros_converted > 0:
92
+ parts.append(f"{stats.kc_macros_converted} command(s) -> code")
93
+ if stats.profile_blocks_handled > 0:
94
+ parts.append(f"{stats.profile_blocks_handled} profile block(s)")
95
+ if stats.noparse_blocks_handled > 0:
96
+ parts.append(f"{stats.noparse_blocks_handled} noparse block(s)")
97
+ if stats.opts_macros_removed > 0:
98
+ parts.append(f"{stats.opts_macros_removed} opts macro(s)")
99
+ if stats.features_macros_removed > 0:
100
+ parts.append(f"{stats.features_macros_removed} features macro(s)")
101
+ if stats.directives_marked > 0:
102
+ parts.append(f"{stats.directives_marked} directive(s) marked")
103
+ if stats.other_macros_removed > 0:
104
+ parts.append(f"{stats.other_macros_removed} other macro(s)")
105
+ return ', '.join(parts) if parts else 'no changes'
106
+
107
+
108
+ def main():
109
+ """Main entry point."""
110
+ # Check for updates (non-blocking)
111
+ check_version_on_startup()
112
+
113
+ parser = argparse.ArgumentParser(
114
+ description="Convert FreeMarker-templated AsciiDoc to standard AsciiDoc",
115
+ formatter_class=argparse.RawDescriptionHelpFormatter,
116
+ epilog="""
117
+ Convert FreeMarker template markup to standard AsciiDoc:
118
+
119
+ STRUCTURE (always converted):
120
+ - Removes <#import ...> statements
121
+ - Converts <@tmpl.guide title="..." summary="..."> to = Title and summary
122
+ - Removes </@tmpl.guide> closing tags
123
+
124
+ INLINE MACROS (converted by default, skip with --structure-only):
125
+ - <@links.server id="hostname"/> -> xref:server/hostname.adoc[]
126
+ - <@kc.start parameters="--hostname x"/> -> code block with bin/kc.sh command
127
+ - <@profile.ifCommunity> blocks -> kept (or removed with --product)
128
+ - <@opts.*> macros -> removed (build-time generated)
129
+
130
+ This tool is designed for converting Keycloak documentation from FreeMarker
131
+ template format to standard AsciiDoc that can be used with other toolchains.
132
+
133
+ Examples:
134
+ %(prog)s # Process all .adoc files
135
+ %(prog)s docs/guides/ # Process specific directory
136
+ %(prog)s docs/guides/server/hostname.adoc # Process single file
137
+ %(prog)s --dry-run docs/guides/ # Preview changes
138
+ %(prog)s --structure-only docs/guides/ # Only convert structure
139
+ %(prog)s --product docs/guides/ # Keep product content
140
+ """
141
+ )
142
+
143
+ parser.add_argument(
144
+ 'path',
145
+ nargs='?',
146
+ default='.',
147
+ help='File or directory to process (default: current directory)'
148
+ )
149
+ parser.add_argument(
150
+ '-n', '--dry-run',
151
+ action='store_true',
152
+ help='Show what would be changed without modifying files'
153
+ )
154
+ parser.add_argument(
155
+ '-v', '--verbose',
156
+ action='store_true',
157
+ help='Show detailed output for each file'
158
+ )
159
+ parser.add_argument(
160
+ '--structure-only',
161
+ action='store_true',
162
+ help='Only convert structure (imports, guide blocks); leave inline macros'
163
+ )
164
+ parser.add_argument(
165
+ '--product',
166
+ action='store_true',
167
+ help='Keep product content in profile blocks (default: keep community)'
168
+ )
169
+ parser.add_argument(
170
+ '--base-path',
171
+ default='',
172
+ help='Base path prefix for xref links (e.g., "guides")'
173
+ )
174
+ parser.add_argument(
175
+ '--only-freemarker',
176
+ action='store_true',
177
+ help='Only process files that contain FreeMarker markup (faster for mixed repos)'
178
+ )
179
+ parser.add_argument(
180
+ '--version',
181
+ action='version',
182
+ version=f'%(prog)s {__version__}'
183
+ )
184
+
185
+ args = parser.parse_args()
186
+
187
+ # Convert path to Path object
188
+ target_path = Path(args.path)
189
+
190
+ # Check if path exists
191
+ if not target_path.exists():
192
+ print_colored(f"Error: Path does not exist: {target_path}", Colors.RED)
193
+ sys.exit(1)
194
+
195
+ # Display mode messages
196
+ if args.dry_run:
197
+ print_colored("DRY RUN MODE - No files will be modified", Colors.YELLOW)
198
+ print()
199
+
200
+ if args.structure_only:
201
+ print("Converting structure only (imports, guide blocks)")
202
+ print()
203
+
204
+ # Find all AsciiDoc files
205
+ adoc_files = find_adoc_files(target_path)
206
+
207
+ if not adoc_files:
208
+ if target_path.is_file():
209
+ print_colored(
210
+ f"Warning: {target_path} is not an AsciiDoc file (.adoc)",
211
+ Colors.YELLOW
212
+ )
213
+ print("No AsciiDoc files found.")
214
+ return
215
+
216
+ # Filter to only files with FreeMarker content if requested
217
+ if args.only_freemarker:
218
+ adoc_files = [f for f in adoc_files if has_freemarker_content(f)]
219
+ if not adoc_files:
220
+ print("No files with FreeMarker markup found.")
221
+ return
222
+ if args.verbose:
223
+ print(f"Found {len(adoc_files)} file(s) with FreeMarker markup")
224
+ print()
225
+
226
+ # Process each file
227
+ files_processed = 0
228
+ files_modified = 0
229
+ all_stats = []
230
+
231
+ for file_path in adoc_files:
232
+ try:
233
+ result = process_file(
234
+ file_path,
235
+ dry_run=args.dry_run,
236
+ verbose=args.verbose,
237
+ convert_all=not args.structure_only,
238
+ keep_community=not args.product,
239
+ base_path=args.base_path
240
+ )
241
+
242
+ # Print verbose messages
243
+ if args.verbose:
244
+ for msg in result.messages:
245
+ print(msg)
246
+
247
+ if result.changes_made:
248
+ files_modified += 1
249
+ all_stats.append(result.stats)
250
+
251
+ if args.dry_run:
252
+ print_colored(f"Would modify: {file_path}", Colors.YELLOW)
253
+ else:
254
+ print_colored(f"Modified: {file_path}", Colors.GREEN)
255
+
256
+ files_processed += 1
257
+
258
+ except KeyboardInterrupt:
259
+ print_colored("\nOperation cancelled by user", Colors.YELLOW)
260
+ sys.exit(1)
261
+ except IOError as e:
262
+ print_colored(f"{e}", Colors.RED)
263
+ except Exception as e:
264
+ print_colored(f"Unexpected error processing {file_path}: {e}", Colors.RED)
265
+
266
+ # Print summary
267
+ print()
268
+ print(f"Processed {files_processed} AsciiDoc file(s)")
269
+
270
+ if files_modified > 0:
271
+ if args.dry_run:
272
+ print(f"Would modify {files_modified} file(s)")
273
+ else:
274
+ print(f"Modified {files_modified} file(s)")
275
+
276
+ # Detailed stats
277
+ total_stats = aggregate_stats(all_stats)
278
+ summary = format_stats_summary(total_stats)
279
+ print(f" ({summary})")
280
+ else:
281
+ print("No files needed conversion.")
282
+
283
+ print()
284
+ print_colored("FreeMarker to AsciiDoc conversion complete!", Colors.CYAN)
285
+
286
+
287
+ if __name__ == "__main__":
288
+ main()