rolfedh-doc-utils 0.1.38__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.
- convert_freemarker_to_asciidoc.py +288 -0
- doc_utils/convert_freemarker_to_asciidoc.py +708 -0
- doc_utils/version.py +1 -1
- {rolfedh_doc_utils-0.1.38.dist-info → rolfedh_doc_utils-0.1.39.dist-info}/METADATA +1 -1
- {rolfedh_doc_utils-0.1.38.dist-info → rolfedh_doc_utils-0.1.39.dist-info}/RECORD +9 -7
- {rolfedh_doc_utils-0.1.38.dist-info → rolfedh_doc_utils-0.1.39.dist-info}/entry_points.txt +1 -0
- {rolfedh_doc_utils-0.1.38.dist-info → rolfedh_doc_utils-0.1.39.dist-info}/top_level.txt +1 -0
- {rolfedh_doc_utils-0.1.38.dist-info → rolfedh_doc_utils-0.1.39.dist-info}/WHEEL +0 -0
- {rolfedh_doc_utils-0.1.38.dist-info → rolfedh_doc_utils-0.1.39.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
doc_utils/version.py
CHANGED
|
@@ -5,6 +5,7 @@ check_scannability.py,sha256=O6ROr-e624jVPvPpASpsWo0gTfuCFpA2mTSX61BjAEI,5478
|
|
|
5
5
|
check_source_directives.py,sha256=JiIvn_ph9VKPMH4zg-aSsuIGQZcnI_imj7rZLLE04L8,3660
|
|
6
6
|
convert_callouts_interactive.py,sha256=4PjiVIOWxNJiJLQuBHT3x6rE46-hgfFHSaoo5quYIs8,22889
|
|
7
7
|
convert_callouts_to_deflist.py,sha256=BoqW5_GkQ-KqNzn4vmE6lsQosrPV0lkB-bfAx3dzyMw,25886
|
|
8
|
+
convert_freemarker_to_asciidoc.py,sha256=ki0bFDPWxl9aUHK_-xqffIKF4KJYMXA8S4XLG_mOA0U,10097
|
|
8
9
|
convert_tables_to_deflists.py,sha256=PIP6xummuMqC3aSzahKKRBYahes_j5ZpHp_-k6BjurY,15599
|
|
9
10
|
doc_utils_cli.py,sha256=J3CE7cTDDCRGkhAknYejNWHhk5t9YFGt27WDVfR98Xk,5111
|
|
10
11
|
extract_link_attributes.py,sha256=wR2SmR2la-jR6DzDbas2PoNONgRZ4dZ6aqwzkwEv8Gs,3516
|
|
@@ -19,6 +20,7 @@ callout_lib/converter_deflist.py,sha256=Ocr3gutTo_Sl_MkzethZH1UO6mCDEcuExGMZF5Mf
|
|
|
19
20
|
callout_lib/detector.py,sha256=S0vZDa4zhTSn6Kv0hWfG56W-5srGxUc-nvpLe_gIx-A,15971
|
|
20
21
|
callout_lib/table_parser.py,sha256=ZucisADE8RDAk5HtIrttaPgBi6Hf8ZUpw7KzfbcmEjc,31450
|
|
21
22
|
doc_utils/__init__.py,sha256=qqZR3lohzkP63soymrEZPBGzzk6-nFzi4_tSffjmu_0,74
|
|
23
|
+
doc_utils/convert_freemarker_to_asciidoc.py,sha256=UGQ7iS_9bkVdDMAWBORXbK0Q5mLPmDs1cDJqoR4LLH8,22491
|
|
22
24
|
doc_utils/extract_link_attributes.py,sha256=U0EvPZReJQigNfbT-icBsVT6Li64hYki5W7MQz6qqbc,22743
|
|
23
25
|
doc_utils/file_utils.py,sha256=fpTh3xx759sF8sNocdn_arsP3KAv8XA6cTQTAVIZiZg,4247
|
|
24
26
|
doc_utils/format_asciidoc_spacing.py,sha256=RL2WU_dG_UfGL01LnevcyJfKsvYy_ogNyeoVX-Fyqks,13579
|
|
@@ -31,12 +33,12 @@ doc_utils/unused_adoc.py,sha256=LPQWPGEOizXECxepk7E_5cjTVvKn6RXQYTWG97Ps5VQ,9077
|
|
|
31
33
|
doc_utils/unused_attributes.py,sha256=OHyAdaBD7aNo357B0SLBN5NC_jNY5TWXMwgtfJNh3X8,7621
|
|
32
34
|
doc_utils/unused_images.py,sha256=hL8Qrik9QCkVh54eBLuNczRS9tMnsqIEfavNamM1UeQ,5664
|
|
33
35
|
doc_utils/validate_links.py,sha256=iBGXnwdeLlgIT3fo3v01ApT5k0X2FtctsvkrE6E3VMk,19610
|
|
34
|
-
doc_utils/version.py,sha256=
|
|
36
|
+
doc_utils/version.py,sha256=mkJOYT4u7NC5KNgMzzEELU6EsgaQ3H-X4wwsweHvB48,203
|
|
35
37
|
doc_utils/version_check.py,sha256=-31Y6AN0KGi_CUCAVOOhf6bPO3r7SQIXPxxeffLAF0w,7535
|
|
36
38
|
doc_utils/warnings_report.py,sha256=20yfwqBjOprfFhQwCujbcsvjJCbHHhmH84uAujm-y-o,8877
|
|
37
|
-
rolfedh_doc_utils-0.1.
|
|
38
|
-
rolfedh_doc_utils-0.1.
|
|
39
|
-
rolfedh_doc_utils-0.1.
|
|
40
|
-
rolfedh_doc_utils-0.1.
|
|
41
|
-
rolfedh_doc_utils-0.1.
|
|
42
|
-
rolfedh_doc_utils-0.1.
|
|
39
|
+
rolfedh_doc_utils-0.1.39.dist-info/licenses/LICENSE,sha256=vLxtwMVOJA_hEy8b77niTkdmQI9kNJskXHq0dBS36e0,1075
|
|
40
|
+
rolfedh_doc_utils-0.1.39.dist-info/METADATA,sha256=nqmzMTwyGMVR7Z3KHRnW8_b-jfmNCyI74-PLoth8vwk,8520
|
|
41
|
+
rolfedh_doc_utils-0.1.39.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
42
|
+
rolfedh_doc_utils-0.1.39.dist-info/entry_points.txt,sha256=ZIBQPjcFpwAaU5Yp_tlhwwFQYACPpStnvxa3H_9V0ws,813
|
|
43
|
+
rolfedh_doc_utils-0.1.39.dist-info/top_level.txt,sha256=fab48QbHL2zKfQ9FLlIjaeGKu39ekN0VVB2R5kR4CK0,369
|
|
44
|
+
rolfedh_doc_utils-0.1.39.dist-info/RECORD,,
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|