rolfedh-doc-utils 0.1.38__tar.gz → 0.1.39__tar.gz

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 (70) hide show
  1. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/PKG-INFO +1 -1
  2. rolfedh_doc_utils-0.1.39/convert_freemarker_to_asciidoc.py +288 -0
  3. rolfedh_doc_utils-0.1.39/doc_utils/convert_freemarker_to_asciidoc.py +708 -0
  4. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/doc_utils/version.py +1 -1
  5. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/pyproject.toml +3 -2
  6. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/rolfedh_doc_utils.egg-info/PKG-INFO +1 -1
  7. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/rolfedh_doc_utils.egg-info/SOURCES.txt +2 -0
  8. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/rolfedh_doc_utils.egg-info/entry_points.txt +1 -0
  9. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/rolfedh_doc_utils.egg-info/top_level.txt +1 -0
  10. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/LICENSE +0 -0
  11. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/README.md +0 -0
  12. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/archive_unused_files.py +0 -0
  13. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/archive_unused_images.py +0 -0
  14. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/callout_lib/__init__.py +0 -0
  15. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/callout_lib/converter_bullets.py +0 -0
  16. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/callout_lib/converter_comments.py +0 -0
  17. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/callout_lib/converter_deflist.py +0 -0
  18. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/callout_lib/detector.py +0 -0
  19. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/callout_lib/table_parser.py +0 -0
  20. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/check_published_links.py +0 -0
  21. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/check_scannability.py +0 -0
  22. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/check_source_directives.py +0 -0
  23. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/convert_callouts_interactive.py +0 -0
  24. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/convert_callouts_to_deflist.py +0 -0
  25. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/convert_tables_to_deflists.py +0 -0
  26. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/doc_utils/__init__.py +0 -0
  27. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/doc_utils/extract_link_attributes.py +0 -0
  28. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/doc_utils/file_utils.py +0 -0
  29. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/doc_utils/format_asciidoc_spacing.py +0 -0
  30. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/doc_utils/missing_source_directive.py +0 -0
  31. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/doc_utils/replace_link_attributes.py +0 -0
  32. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/doc_utils/scannability.py +0 -0
  33. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/doc_utils/spinner.py +0 -0
  34. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/doc_utils/topic_map_parser.py +0 -0
  35. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/doc_utils/unused_adoc.py +0 -0
  36. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/doc_utils/unused_attributes.py +0 -0
  37. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/doc_utils/unused_images.py +0 -0
  38. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/doc_utils/validate_links.py +0 -0
  39. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/doc_utils/version_check.py +0 -0
  40. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/doc_utils/warnings_report.py +0 -0
  41. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/doc_utils_cli.py +0 -0
  42. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/extract_link_attributes.py +0 -0
  43. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/find_unused_attributes.py +0 -0
  44. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/format_asciidoc_spacing.py +0 -0
  45. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/replace_link_attributes.py +0 -0
  46. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/rolfedh_doc_utils.egg-info/dependency_links.txt +0 -0
  47. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/rolfedh_doc_utils.egg-info/requires.txt +0 -0
  48. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/setup.cfg +0 -0
  49. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/setup.py +0 -0
  50. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/tests/test_archive_unused_files.py +0 -0
  51. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/tests/test_archive_unused_images.py +0 -0
  52. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/tests/test_auto_discovery.py +0 -0
  53. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/tests/test_check_scannability.py +0 -0
  54. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/tests/test_cli_entry_points.py +0 -0
  55. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/tests/test_convert_tables_to_deflists.py +0 -0
  56. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/tests/test_extract_link_attributes.py +0 -0
  57. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/tests/test_file_utils.py +0 -0
  58. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/tests/test_fixture_archive_unused_files.py +0 -0
  59. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/tests/test_fixture_archive_unused_images.py +0 -0
  60. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/tests/test_fixture_check_scannability.py +0 -0
  61. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/tests/test_parse_exclude_list.py +0 -0
  62. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/tests/test_replace_link_attributes.py +0 -0
  63. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/tests/test_symlink_handling.py +0 -0
  64. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/tests/test_table_callout_conversion.py +0 -0
  65. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/tests/test_table_parser.py +0 -0
  66. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/tests/test_topic_map_parser.py +0 -0
  67. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/tests/test_unused_attributes.py +0 -0
  68. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/tests/test_validate_links.py +0 -0
  69. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/tests/test_version_check.py +0 -0
  70. {rolfedh_doc_utils-0.1.38 → rolfedh_doc_utils-0.1.39}/validate_links.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rolfedh-doc-utils
3
- Version: 0.1.38
3
+ Version: 0.1.39
4
4
  Summary: CLI tools for AsciiDoc documentation projects
5
5
  Author: Rolfe Dlugy-Hegwer
6
6
  License: MIT License
@@ -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()
@@ -0,0 +1,708 @@
1
+ """
2
+ Convert FreeMarker-templated AsciiDoc to standard AsciiDoc.
3
+
4
+ Core logic for converting Keycloak-style FreeMarker template markup in AsciiDoc
5
+ files to standard AsciiDoc format. This module handles:
6
+
7
+ - Removing FreeMarker import statements (<#import ...>)
8
+ - Converting <@tmpl.guide> and <@template.guide> blocks to AsciiDoc title/summary
9
+ - Removing closing </@tmpl.guide> and </@template.guide> tags
10
+ - Converting <@links.*> macros to AsciiDoc xref cross-references
11
+ - Converting <@kc.*> command macros to code blocks
12
+ - Removing or preserving <@profile.*> conditional blocks
13
+ - Removing <@opts.*> option macros (build-time generated content)
14
+ - Removing <@features.table> macros (build-time generated content)
15
+ - Handling <#noparse> blocks (preserving content, removing tags)
16
+
17
+ PATTERNS THAT CANNOT BE RELIABLY CONVERTED (require manual review):
18
+ - <#list> loops: Dynamic content generation, no static equivalent
19
+ - <#if>/<#else> conditionals: Logic-based content, context-dependent
20
+ - <#assign> variables: Build-time variable assignment
21
+ - <@features.table ctx.*/>: Dynamic feature tables generated at build time
22
+ - Index files with <#list ctx.guides>: Auto-generated navigation
23
+ """
24
+
25
+ import re
26
+ from pathlib import Path
27
+ from typing import List, Tuple, Optional, Dict
28
+ from dataclasses import dataclass, field
29
+
30
+
31
+ # Mapping of link types to their base paths
32
+ # These are the guide categories used in Keycloak docs
33
+ LINK_TYPE_PATHS: Dict[str, str] = {
34
+ 'server': 'server',
35
+ 'ha': 'high-availability',
36
+ 'gettingstarted': 'getting-started',
37
+ 'operator': 'operator',
38
+ 'migration': 'migration',
39
+ 'securing': 'securing-apps',
40
+ 'securingapps': 'securing-apps', # Alternative spelling used in some files
41
+ 'observability': 'observability',
42
+ }
43
+
44
+ # Mapping of kc command types to their shell commands
45
+ KC_COMMAND_MAP: Dict[str, str] = {
46
+ 'start': 'bin/kc.sh start',
47
+ 'startdev': 'bin/kc.sh start-dev',
48
+ 'build': 'bin/kc.sh build',
49
+ 'export': 'bin/kc.sh export',
50
+ 'import': 'bin/kc.sh import',
51
+ 'admin': 'bin/kcadm.sh',
52
+ 'bootstrapadmin': 'bin/kc.sh bootstrap-admin',
53
+ 'updatecompatibility': 'bin/kc.sh update-compatibility',
54
+ }
55
+
56
+
57
+ @dataclass
58
+ class ConversionStats:
59
+ """Statistics for a conversion operation."""
60
+ imports_removed: int = 0
61
+ guide_blocks_converted: int = 0
62
+ closing_tags_removed: int = 0
63
+ link_macros_converted: int = 0
64
+ kc_macros_converted: int = 0
65
+ profile_blocks_handled: int = 0
66
+ noparse_blocks_handled: int = 0
67
+ opts_macros_removed: int = 0
68
+ features_macros_removed: int = 0
69
+ other_macros_removed: int = 0
70
+ directives_marked: int = 0
71
+
72
+
73
+ @dataclass
74
+ class ConversionResult:
75
+ """Result of converting a single file."""
76
+ file_path: Path
77
+ changes_made: bool
78
+ stats: ConversionStats = field(default_factory=ConversionStats)
79
+ messages: List[str] = field(default_factory=list)
80
+
81
+
82
+ def extract_guide_attributes(content: str) -> Optional[Tuple[str, str, int, int]]:
83
+ """
84
+ Extract title and summary from a <@tmpl.guide> or <@template.guide> block.
85
+
86
+ Args:
87
+ content: The file content to parse
88
+
89
+ Returns:
90
+ Tuple of (title, summary, start_index, end_index) or None if not found
91
+ """
92
+ # Match both <@tmpl.guide> and <@template.guide> variants
93
+ pattern = r'<@(?:tmpl|template)\.guide\s*\n?((?:[^>]|\n)*?)>'
94
+
95
+ match = re.search(pattern, content)
96
+ if not match:
97
+ return None
98
+
99
+ block_content = match.group(1)
100
+ start_idx = match.start()
101
+ end_idx = match.end()
102
+
103
+ # Extract title attribute
104
+ title_match = re.search(r'title\s*=\s*"([^"]*)"', block_content)
105
+ title = title_match.group(1) if title_match else ""
106
+
107
+ # Extract summary attribute
108
+ summary_match = re.search(r'summary\s*=\s*"([^"]*)"', block_content)
109
+ summary = summary_match.group(1) if summary_match else ""
110
+
111
+ return (title, summary, start_idx, end_idx)
112
+
113
+
114
+ def remove_freemarker_imports(content: str) -> Tuple[str, int]:
115
+ """
116
+ Remove FreeMarker import statements from content.
117
+
118
+ Args:
119
+ content: The file content
120
+
121
+ Returns:
122
+ Tuple of (modified content, count of imports removed)
123
+ """
124
+ # Match lines that are FreeMarker imports: <#import "..." as ...>
125
+ import_pattern = r'^<#import\s+"[^"]+"\s+as\s+\w+>\s*\n?'
126
+
127
+ count = len(re.findall(import_pattern, content, re.MULTILINE))
128
+ new_content = re.sub(import_pattern, '', content, flags=re.MULTILINE)
129
+
130
+ return (new_content, count)
131
+
132
+
133
+ def convert_guide_block(content: str) -> Tuple[str, bool]:
134
+ """
135
+ Convert <@tmpl.guide> or <@template.guide> block to standard AsciiDoc title and summary.
136
+
137
+ Args:
138
+ content: The file content
139
+
140
+ Returns:
141
+ Tuple of (modified content, whether conversion was made)
142
+ """
143
+ result = extract_guide_attributes(content)
144
+ if not result:
145
+ return (content, False)
146
+
147
+ title, summary, start_idx, end_idx = result
148
+
149
+ # Build the AsciiDoc replacement
150
+ replacement_parts = []
151
+
152
+ if title:
153
+ replacement_parts.append(f"= {title}")
154
+ replacement_parts.append("") # Blank line after title
155
+
156
+ if summary:
157
+ replacement_parts.append(summary)
158
+ replacement_parts.append("") # Blank line after summary
159
+
160
+ replacement = "\n".join(replacement_parts)
161
+
162
+ # Replace the guide block with the AsciiDoc equivalent
163
+ new_content = content[:start_idx] + replacement + content[end_idx:]
164
+
165
+ # Clean up any excessive blank lines at the start
166
+ new_content = re.sub(r'^\n+', '', new_content)
167
+
168
+ return (new_content, True)
169
+
170
+
171
+ def remove_closing_guide_tag(content: str) -> Tuple[str, bool]:
172
+ """
173
+ Remove the closing </@tmpl.guide> or </@template.guide> tag.
174
+
175
+ Args:
176
+ content: The file content
177
+
178
+ Returns:
179
+ Tuple of (modified content, whether tag was removed)
180
+ """
181
+ # Match both variants
182
+ pattern = r'\n*</@(?:tmpl|template)\.guide>\s*\n*'
183
+ if re.search(pattern, content):
184
+ new_content = re.sub(pattern, '\n', content)
185
+ return (new_content, True)
186
+ return (content, False)
187
+
188
+
189
+ def convert_link_macros(content: str, base_path: str = '') -> Tuple[str, int]:
190
+ """
191
+ Convert <@links.*> macros to AsciiDoc xref cross-references.
192
+
193
+ Converts patterns like:
194
+ - <@links.server id="hostname"/> -> xref:server/hostname.adoc[]
195
+ - <@links.ha id="intro" anchor="section"/> -> xref:high-availability/intro.adoc#section[]
196
+ - <@links.securingapps id="client-registration"/> -> xref:securing-apps/client-registration.adoc[]
197
+
198
+ Args:
199
+ content: The file content
200
+ base_path: Optional base path prefix for xref links
201
+
202
+ Returns:
203
+ Tuple of (modified content, count of macros converted)
204
+ """
205
+ count = 0
206
+
207
+ def replace_link(match):
208
+ nonlocal count
209
+ count += 1
210
+
211
+ link_type = match.group(1) # e.g., 'server', 'ha', 'gettingstarted', 'securingapps'
212
+ link_id = match.group(2) # e.g., 'hostname', 'introduction'
213
+ anchor = match.group(4) if match.group(4) else '' # Optional anchor
214
+
215
+ # Map link type to directory path
216
+ dir_path = LINK_TYPE_PATHS.get(link_type, link_type)
217
+
218
+ # Build the file path
219
+ file_path = f"{link_id}.adoc"
220
+
221
+ # Build the xref
222
+ if base_path:
223
+ xref_path = f"{base_path}/{dir_path}/{file_path}"
224
+ else:
225
+ xref_path = f"{dir_path}/{file_path}"
226
+
227
+ if anchor:
228
+ xref_path += f"#{anchor}"
229
+
230
+ # Return xref with empty link text (will use document title)
231
+ return f"xref:{xref_path}[]"
232
+
233
+ # Pattern to match link macros:
234
+ # <@links.TYPE id="ID"/> or <@links.TYPE id="ID" anchor="ANCHOR"/>
235
+ # Also handles extra whitespace variations
236
+ link_pattern = r'<@links\.(\w+)\s+id="([^"]+)"(\s+anchor="([^"]+)")?\s*/>'
237
+
238
+ new_content = re.sub(link_pattern, replace_link, content)
239
+
240
+ return (new_content, count)
241
+
242
+
243
+ def convert_kc_macros(content: str) -> Tuple[str, int]:
244
+ """
245
+ Convert <@kc.*> command macros to AsciiDoc code blocks.
246
+
247
+ Converts patterns like:
248
+ - <@kc.start parameters="--hostname x"/> -> [source,bash]\n----\nbin/kc.sh start --hostname x\n----
249
+ - <@kc.build parameters="--db postgres"/> -> [source,bash]\n----\nbin/kc.sh build --db postgres\n----
250
+ - <@kc.export parameters="--dir <dir>"/> -> [source,bash]\n----\nbin/kc.sh export --dir <dir>\n----
251
+ - <@kc.import parameters="--file <file>"/> -> [source,bash]\n----\nbin/kc.sh import --file <file>\n----
252
+
253
+ Args:
254
+ content: The file content
255
+
256
+ Returns:
257
+ Tuple of (modified content, count of macros converted)
258
+ """
259
+ count = 0
260
+
261
+ def replace_kc_macro(match):
262
+ nonlocal count
263
+ count += 1
264
+
265
+ command = match.group(1) # e.g., 'start', 'build', 'admin', 'export', 'import'
266
+ parameters = match.group(2) # e.g., '--hostname my.keycloak.org'
267
+
268
+ # Get the base command from the map, or use generic kc.sh
269
+ if command in KC_COMMAND_MAP:
270
+ base_cmd = KC_COMMAND_MAP[command]
271
+ else:
272
+ # Generic kc command
273
+ base_cmd = f"bin/kc.sh {command}"
274
+
275
+ # Build the full command line
276
+ if parameters:
277
+ cmd_line = f"{base_cmd} {parameters}".strip()
278
+ else:
279
+ cmd_line = base_cmd
280
+
281
+ # Return as a code block
282
+ return f"[source,bash]\n----\n{cmd_line}\n----"
283
+
284
+ # Pattern to match kc macros: <@kc.COMMAND parameters="PARAMS"/>
285
+ kc_pattern = r'<@kc\.(\w+)\s+parameters="([^"]*)"\s*/>'
286
+
287
+ new_content = re.sub(kc_pattern, replace_kc_macro, content)
288
+
289
+ return (new_content, count)
290
+
291
+
292
+ def handle_profile_blocks(content: str, keep_community: bool = True) -> Tuple[str, int]:
293
+ """
294
+ Handle <@profile.*> conditional blocks.
295
+
296
+ These blocks wrap content that should only appear in specific editions
297
+ (community vs product). By default, we keep community content and remove
298
+ product-specific content.
299
+
300
+ Args:
301
+ content: The file content
302
+ keep_community: If True, keep community content; if False, keep product content
303
+
304
+ Returns:
305
+ Tuple of (modified content, count of blocks handled)
306
+ """
307
+ count = 0
308
+
309
+ # First, handle ifCommunity blocks
310
+ community_pattern = r'<@profile\.ifCommunity>\s*\n?(.*?)</@profile\.ifCommunity>\s*\n?'
311
+
312
+ def handle_community(match):
313
+ nonlocal count
314
+ count += 1
315
+ if keep_community:
316
+ # Keep the content, remove the tags
317
+ return match.group(1)
318
+ else:
319
+ # Remove the entire block
320
+ return ''
321
+
322
+ content = re.sub(community_pattern, handle_community, content, flags=re.DOTALL)
323
+
324
+ # Handle ifProduct blocks
325
+ product_pattern = r'<@profile\.ifProduct>\s*\n?(.*?)</@profile\.ifProduct>\s*\n?'
326
+
327
+ def handle_product(match):
328
+ nonlocal count
329
+ count += 1
330
+ if not keep_community:
331
+ # Keep the content, remove the tags
332
+ return match.group(1)
333
+ else:
334
+ # Remove the entire block
335
+ return ''
336
+
337
+ content = re.sub(product_pattern, handle_product, content, flags=re.DOTALL)
338
+
339
+ return (content, count)
340
+
341
+
342
+ def handle_noparse_blocks(content: str) -> Tuple[str, int]:
343
+ """
344
+ Handle <#noparse> blocks by removing the tags but keeping the content.
345
+
346
+ These blocks are used to escape FreeMarker syntax in code examples.
347
+ The content inside should be preserved as-is.
348
+
349
+ Args:
350
+ content: The file content
351
+
352
+ Returns:
353
+ Tuple of (modified content, count of blocks handled)
354
+ """
355
+ count = 0
356
+
357
+ # Pattern for noparse blocks: <#noparse>...</#noparse>
358
+ noparse_pattern = r'<#noparse>\s*\n?(.*?)</#noparse>\s*\n?'
359
+
360
+ def handle_noparse(match):
361
+ nonlocal count
362
+ count += 1
363
+ # Keep the content, remove the tags
364
+ return match.group(1)
365
+
366
+ content = re.sub(noparse_pattern, handle_noparse, content, flags=re.DOTALL)
367
+
368
+ return (content, count)
369
+
370
+
371
+ def remove_opts_macros(content: str) -> Tuple[str, int]:
372
+ """
373
+ Remove <@opts.*> option macros.
374
+
375
+ These macros generate option documentation at build time and have no
376
+ meaningful conversion to static AsciiDoc. They are replaced with a
377
+ placeholder comment.
378
+
379
+ Args:
380
+ content: The file content
381
+
382
+ Returns:
383
+ Tuple of (modified content, count of macros removed)
384
+ """
385
+ count = 0
386
+
387
+ # Pattern for self-closing opts macros: <@opts.expectedValues option="..."/>
388
+ self_closing_pattern = r'<@opts\.\w+[^/>]*\/>'
389
+
390
+ def replace_self_closing(match):
391
+ nonlocal count
392
+ count += 1
393
+ return '// Configuration options are documented in the all-config guide'
394
+
395
+ content = re.sub(self_closing_pattern, replace_self_closing, content)
396
+
397
+ # Pattern for block opts macros: <@opts.list ...>...</@opts.list>
398
+ block_pattern = r'<@opts\.\w+[^>]*>.*?</@opts\.\w+>'
399
+
400
+ def replace_block(match):
401
+ nonlocal count
402
+ count += 1
403
+ return '// Configuration options are documented in the all-config guide'
404
+
405
+ content = re.sub(block_pattern, replace_block, content, flags=re.DOTALL)
406
+
407
+ return (content, count)
408
+
409
+
410
+ def remove_features_macros(content: str) -> Tuple[str, int]:
411
+ """
412
+ Remove <@features.table> macros.
413
+
414
+ These macros generate feature tables at build time and cannot be
415
+ converted to static content.
416
+
417
+ Args:
418
+ content: The file content
419
+
420
+ Returns:
421
+ Tuple of (modified content, count of macros removed)
422
+ """
423
+ count = 0
424
+
425
+ # Pattern for features.table macros: <@features.table ctx.features.supported/>
426
+ features_pattern = r'<@features\.table[^/>]*\/>'
427
+
428
+ def replace_features(match):
429
+ nonlocal count
430
+ count += 1
431
+ return '// Feature table is generated at build time - see the features guide'
432
+
433
+ content = re.sub(features_pattern, replace_features, content)
434
+
435
+ return (content, count)
436
+
437
+
438
+ def mark_unconvertible_directives(content: str) -> Tuple[str, int]:
439
+ """
440
+ Mark FreeMarker directives that cannot be reliably converted.
441
+
442
+ These include:
443
+ - <#list> loops
444
+ - <#if>/<#else> conditionals
445
+ - <#assign> variables
446
+
447
+ Args:
448
+ content: The file content
449
+
450
+ Returns:
451
+ Tuple of (modified content, count of directives marked)
452
+ """
453
+ count = 0
454
+
455
+ # Pattern for list blocks: <#list ...>...</#list>
456
+ list_pattern = r'(<#list\s+[^>]+>)(.*?)(</#list>)'
457
+
458
+ def mark_list(match):
459
+ nonlocal count
460
+ count += 1
461
+ opening = match.group(1)
462
+ inner = match.group(2)
463
+ closing = match.group(3)
464
+ return f"// TODO: Manual conversion required - FreeMarker list loop\n// {opening}\n{inner}// {closing}"
465
+
466
+ content = re.sub(list_pattern, mark_list, content, flags=re.DOTALL)
467
+
468
+ # Pattern for if/else blocks: <#if ...>...</#if>
469
+ if_pattern = r'(<#if\s+[^>]+>)(.*?)(</#if>)'
470
+
471
+ def mark_if(match):
472
+ nonlocal count
473
+ count += 1
474
+ opening = match.group(1)
475
+ inner = match.group(2)
476
+ closing = match.group(3)
477
+ return f"// TODO: Manual conversion required - FreeMarker conditional\n// {opening}\n{inner}// {closing}"
478
+
479
+ content = re.sub(if_pattern, mark_if, content, flags=re.DOTALL)
480
+
481
+ # Pattern for standalone else: <#else>
482
+ else_pattern = r'<#else>'
483
+
484
+ def mark_else(match):
485
+ nonlocal count
486
+ count += 1
487
+ return '// <#else>'
488
+
489
+ content = re.sub(else_pattern, mark_else, content)
490
+
491
+ # Pattern for assign: <#assign ...>
492
+ assign_pattern = r'<#assign\s+[^>]+>'
493
+
494
+ def mark_assign(match):
495
+ nonlocal count
496
+ count += 1
497
+ return f"// TODO: Manual conversion required - {match.group(0)}"
498
+
499
+ content = re.sub(assign_pattern, mark_assign, content)
500
+
501
+ return (content, count)
502
+
503
+
504
+ def remove_remaining_macros(content: str) -> Tuple[str, int]:
505
+ """
506
+ Remove any remaining FreeMarker macros that weren't handled by specific converters.
507
+
508
+ Args:
509
+ content: The file content
510
+
511
+ Returns:
512
+ Tuple of (modified content, count of macros removed)
513
+ """
514
+ count = 0
515
+
516
+ # Self-closing macros: <@something .../>
517
+ self_closing = r'<@\w+\.[^/>]+/>'
518
+ self_closing_count = len(re.findall(self_closing, content))
519
+ if self_closing_count > 0:
520
+ content = re.sub(self_closing, '// TODO: Unconverted FreeMarker macro', content)
521
+ count += self_closing_count
522
+
523
+ # Block macros: <@something>...</@something>
524
+ block_pattern = r'<@\w+[^>]*>.*?</@\w+[^>]*>'
525
+ block_count = len(re.findall(block_pattern, content, re.DOTALL))
526
+ if block_count > 0:
527
+ content = re.sub(block_pattern, '// TODO: Unconverted FreeMarker macro', content, flags=re.DOTALL)
528
+ count += block_count
529
+
530
+ return (content, count)
531
+
532
+
533
+ def process_file(
534
+ file_path: Path,
535
+ dry_run: bool = False,
536
+ verbose: bool = False,
537
+ convert_all: bool = True,
538
+ keep_community: bool = True,
539
+ base_path: str = ''
540
+ ) -> ConversionResult:
541
+ """
542
+ Process a single AsciiDoc file to convert FreeMarker markup.
543
+
544
+ Args:
545
+ file_path: Path to the file to process
546
+ dry_run: If True, show what would be changed without modifying
547
+ verbose: If True, show detailed output
548
+ convert_all: If True, convert all FreeMarker macros (not just structure)
549
+ keep_community: If True, keep community content in profile blocks
550
+ base_path: Optional base path prefix for xref links
551
+
552
+ Returns:
553
+ ConversionResult with details of changes made
554
+ """
555
+ messages = []
556
+ stats = ConversionStats()
557
+
558
+ if verbose:
559
+ messages.append(f"Processing: {file_path}")
560
+
561
+ try:
562
+ with open(file_path, 'r', encoding='utf-8') as f:
563
+ original_content = f.read()
564
+ except (IOError, UnicodeDecodeError) as e:
565
+ raise IOError(f"Error reading {file_path}: {e}")
566
+
567
+ content = original_content
568
+
569
+ # Step 1: Remove FreeMarker imports
570
+ content, count = remove_freemarker_imports(content)
571
+ stats.imports_removed = count
572
+ if count > 0 and verbose:
573
+ messages.append(f" Removed {count} FreeMarker import(s)")
574
+
575
+ # Step 2: Convert guide block (both tmpl.guide and template.guide)
576
+ content, converted = convert_guide_block(content)
577
+ if converted:
578
+ stats.guide_blocks_converted = 1
579
+ if verbose:
580
+ messages.append(" Converted guide block to AsciiDoc title/summary")
581
+
582
+ # Step 3: Remove closing tag
583
+ content, removed = remove_closing_guide_tag(content)
584
+ if removed:
585
+ stats.closing_tags_removed = 1
586
+ if verbose:
587
+ messages.append(" Removed closing guide tag")
588
+
589
+ # Step 4: Convert inline macros if requested
590
+ if convert_all:
591
+ # Handle noparse blocks first (preserve content)
592
+ content, count = handle_noparse_blocks(content)
593
+ stats.noparse_blocks_handled = count
594
+ if count > 0 and verbose:
595
+ messages.append(f" Processed {count} <#noparse> block(s)")
596
+
597
+ # Convert link macros to xrefs
598
+ content, count = convert_link_macros(content, base_path)
599
+ stats.link_macros_converted = count
600
+ if count > 0 and verbose:
601
+ messages.append(f" Converted {count} <@links.*> macro(s) to xref")
602
+
603
+ # Convert kc command macros to code blocks
604
+ content, count = convert_kc_macros(content)
605
+ stats.kc_macros_converted = count
606
+ if count > 0 and verbose:
607
+ messages.append(f" Converted {count} <@kc.*> macro(s) to code blocks")
608
+
609
+ # Handle profile conditional blocks
610
+ content, count = handle_profile_blocks(content, keep_community)
611
+ stats.profile_blocks_handled = count
612
+ if count > 0 and verbose:
613
+ messages.append(f" Processed {count} <@profile.*> block(s)")
614
+
615
+ # Remove opts macros
616
+ content, count = remove_opts_macros(content)
617
+ stats.opts_macros_removed = count
618
+ if count > 0 and verbose:
619
+ messages.append(f" Removed {count} <@opts.*> macro(s)")
620
+
621
+ # Remove features macros
622
+ content, count = remove_features_macros(content)
623
+ stats.features_macros_removed = count
624
+ if count > 0 and verbose:
625
+ messages.append(f" Removed {count} <@features.*> macro(s)")
626
+
627
+ # Mark unconvertible directives
628
+ content, count = mark_unconvertible_directives(content)
629
+ stats.directives_marked = count
630
+ if count > 0 and verbose:
631
+ messages.append(f" Marked {count} FreeMarker directive(s) for manual review")
632
+
633
+ # Remove any remaining macros
634
+ content, count = remove_remaining_macros(content)
635
+ stats.other_macros_removed = count
636
+ if count > 0 and verbose:
637
+ messages.append(f" Marked {count} other macro(s) for review")
638
+
639
+ # Determine if changes were made
640
+ changes_made = content != original_content
641
+
642
+ # Write changes if not dry run
643
+ if changes_made and not dry_run:
644
+ try:
645
+ with open(file_path, 'w', encoding='utf-8') as f:
646
+ f.write(content)
647
+ except IOError as e:
648
+ raise IOError(f"Error writing {file_path}: {e}")
649
+
650
+ if not changes_made and verbose:
651
+ messages.append(" No FreeMarker markup found")
652
+
653
+ return ConversionResult(
654
+ file_path=file_path,
655
+ changes_made=changes_made,
656
+ stats=stats,
657
+ messages=messages
658
+ )
659
+
660
+
661
+ def find_adoc_files(path: Path) -> List[Path]:
662
+ """
663
+ Find all .adoc files in the given path.
664
+
665
+ Args:
666
+ path: File or directory to search
667
+
668
+ Returns:
669
+ List of Path objects for .adoc files
670
+ """
671
+ adoc_files = []
672
+
673
+ if path.is_file():
674
+ if path.suffix == '.adoc':
675
+ adoc_files.append(path)
676
+ elif path.is_dir():
677
+ # Use os.walk to avoid following symlinks
678
+ import os
679
+ for root, dirs, files in os.walk(path, followlinks=False):
680
+ # Skip hidden directories
681
+ dirs[:] = [d for d in dirs if not d.startswith('.')]
682
+ for file in files:
683
+ if file.endswith('.adoc'):
684
+ adoc_files.append(Path(root) / file)
685
+
686
+ return sorted(adoc_files)
687
+
688
+
689
+ def has_freemarker_content(file_path: Path) -> bool:
690
+ """
691
+ Check if a file contains FreeMarker markup.
692
+
693
+ Args:
694
+ file_path: Path to the file to check
695
+
696
+ Returns:
697
+ True if file contains FreeMarker markup
698
+ """
699
+ try:
700
+ with open(file_path, 'r', encoding='utf-8') as f:
701
+ content = f.read()
702
+ return bool(
703
+ re.search(r'<#import', content) or
704
+ re.search(r'<#\w+', content) or # Any FreeMarker directive
705
+ re.search(r'<@\w+\.', content) # Any FreeMarker macro
706
+ )
707
+ except (IOError, UnicodeDecodeError):
708
+ return False
@@ -1,7 +1,7 @@
1
1
  """Version information for doc-utils."""
2
2
 
3
3
  # This should match the version in pyproject.toml
4
- __version__ = "0.1.37"
4
+ __version__ = "0.1.39"
5
5
 
6
6
  def get_version():
7
7
  """Return the current version string."""
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rolfedh-doc-utils"
7
- version = "0.1.38"
7
+ version = "0.1.39"
8
8
  description = "CLI tools for AsciiDoc documentation projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -29,10 +29,11 @@ convert-callouts-interactive = "convert_callouts_interactive:main"
29
29
  check-source-directives = "check_source_directives:main"
30
30
  convert-tables-to-deflists = "convert_tables_to_deflists:main"
31
31
  check-published-links = "check_published_links:main"
32
+ convert-freemarker-to-asciidoc = "convert_freemarker_to_asciidoc:main"
32
33
 
33
34
  [tool.setuptools.packages.find]
34
35
  where = ["."]
35
36
  include = ["doc_utils*", "callout_lib*"]
36
37
 
37
38
  [tool.setuptools]
38
- py-modules = ["doc_utils_cli", "find_unused_attributes", "check_scannability", "archive_unused_files", "archive_unused_images", "format_asciidoc_spacing", "replace_link_attributes", "extract_link_attributes", "validate_links", "convert_callouts_to_deflist", "convert_callouts_interactive", "check_source_directives", "convert_tables_to_deflists", "check_published_links"]
39
+ py-modules = ["doc_utils_cli", "find_unused_attributes", "check_scannability", "archive_unused_files", "archive_unused_images", "format_asciidoc_spacing", "replace_link_attributes", "extract_link_attributes", "validate_links", "convert_callouts_to_deflist", "convert_callouts_interactive", "check_source_directives", "convert_tables_to_deflists", "check_published_links", "convert_freemarker_to_asciidoc"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rolfedh-doc-utils
3
- Version: 0.1.38
3
+ Version: 0.1.39
4
4
  Summary: CLI tools for AsciiDoc documentation projects
5
5
  Author: Rolfe Dlugy-Hegwer
6
6
  License: MIT License
@@ -7,6 +7,7 @@ check_scannability.py
7
7
  check_source_directives.py
8
8
  convert_callouts_interactive.py
9
9
  convert_callouts_to_deflist.py
10
+ convert_freemarker_to_asciidoc.py
10
11
  convert_tables_to_deflists.py
11
12
  doc_utils_cli.py
12
13
  extract_link_attributes.py
@@ -23,6 +24,7 @@ callout_lib/converter_deflist.py
23
24
  callout_lib/detector.py
24
25
  callout_lib/table_parser.py
25
26
  doc_utils/__init__.py
27
+ doc_utils/convert_freemarker_to_asciidoc.py
26
28
  doc_utils/extract_link_attributes.py
27
29
  doc_utils/file_utils.py
28
30
  doc_utils/format_asciidoc_spacing.py
@@ -6,6 +6,7 @@ check-scannability = check_scannability:main
6
6
  check-source-directives = check_source_directives:main
7
7
  convert-callouts-interactive = convert_callouts_interactive:main
8
8
  convert-callouts-to-deflist = convert_callouts_to_deflist:main
9
+ convert-freemarker-to-asciidoc = convert_freemarker_to_asciidoc:main
9
10
  convert-tables-to-deflists = convert_tables_to_deflists:main
10
11
  doc-utils = doc_utils_cli:main
11
12
  extract-link-attributes = extract_link_attributes:main
@@ -6,6 +6,7 @@ check_scannability
6
6
  check_source_directives
7
7
  convert_callouts_interactive
8
8
  convert_callouts_to_deflist
9
+ convert_freemarker_to_asciidoc
9
10
  convert_tables_to_deflists
10
11
  doc_utils
11
12
  doc_utils_cli