11from __future__ import annotations
22
3- import subprocess
43from pathlib import Path
54from typing import Any , Dict , List
65
76from ..config import BindgenConfig
87from ..ir .model import IRModule
9- from .context import MappedFile , build_context
8+ from .context import MappedFile , TemplateContext , build_context
109from .naming import NameTransformer
10+ from .post_formatters import run_post_formatters
1111
1212
1313def _load_jinja ():
@@ -20,123 +20,23 @@ def _load_jinja():
2020 return Environment , FileSystemLoader
2121
2222
23- def _normalize_formatters (mapping_options : Dict [str , Any ]) -> List [Dict [str , Any ]]:
24- """Normalize formatter configs from mapping.options.formatters."""
25- raw = mapping_options .get ("formatters" , [])
26- if not isinstance (raw , list ):
27- print ("[bindgen] warning: mapping.options.formatters must be a list, ignoring" )
28- return []
29-
30- normalized : List [Dict [str , Any ]] = []
31- for index , item in enumerate (raw ):
32- if not isinstance (item , dict ):
33- print (f"[bindgen] warning: formatter[{ index } ] must be an object, skipping" )
34- continue
35-
36- enabled = bool (item .get ("enabled" , True ))
37- if not enabled :
38- continue
39-
40- cmd = item .get ("cmd" )
41- if not isinstance (cmd , list ) or not cmd or not all (
42- isinstance (token , str ) and token for token in cmd
43- ):
44- print (
45- f"[bindgen] warning: formatter[{ index } ].cmd must be a non-empty string array, skipping"
46- )
47- continue
48-
49- normalized .append (
50- {
51- "name" : str (item .get ("name" , f"formatter[{ index } ]" )),
52- "cmd" : cmd ,
53- "continue_on_error" : bool (item .get ("continue_on_error" , True )),
54- }
55- )
56-
57- return normalized
58-
59-
60- def _expand_tokens (cmd : List [str ], placeholders : Dict [str , str ]) -> List [str ]:
61- expanded : List [str ] = []
62- for token in cmd :
63- value = token
64- for key , replacement in placeholders .items ():
65- value = value .replace (key , replacement )
66- expanded .append (value )
67- return expanded
68-
69-
7023def _run_post_formatters (cfg : BindgenConfig , out_dir : Path , config_path : Path ) -> None :
71- """Run post-generation formatter commands."""
72- formatters = _normalize_formatters (cfg .mapping .options or {})
73- if not formatters :
74- return
75-
76- placeholders = {
77- "{out_dir}" : str (out_dir .resolve ()),
78- "{config_dir}" : str (config_path .resolve ().parent ),
79- "{project_dir}" : str (Path .cwd ().resolve ()),
80- }
81- run_cwd = str (config_path .resolve ().parent )
82-
83- for formatter in formatters :
84- name = formatter ["name" ]
85- command = _expand_tokens (formatter ["cmd" ], placeholders )
86- continue_on_error = formatter ["continue_on_error" ]
87-
88- print (f"[bindgen] formatter { name } : { ' ' .join (command )} " )
89- try :
90- result = subprocess .run (
91- command ,
92- cwd = run_cwd ,
93- capture_output = True ,
94- text = True ,
95- )
96- except FileNotFoundError as exc :
97- message = (
98- f"[bindgen] formatter { name } failed: command not found: { command [0 ]} "
99- )
100- if continue_on_error :
101- print (f"{ message } (continuing)" )
102- continue
103- raise RuntimeError (message ) from exc
104- except OSError as exc :
105- message = f"[bindgen] formatter { name } failed: { exc } "
106- if continue_on_error :
107- print (f"{ message } (continuing)" )
108- continue
109- raise RuntimeError (message ) from exc
110-
111- if result .stdout .strip ():
112- print (result .stdout .rstrip ())
113-
114- if result .returncode != 0 :
115- stderr = result .stderr .strip ()
116- suffix = f": { stderr } " if stderr else ""
117- message = (
118- f"[bindgen] formatter { name } exited with { result .returncode } { suffix } "
119- )
120- if continue_on_error :
121- print (f"{ message } (continuing)" )
122- continue
123- raise RuntimeError (message )
24+ """Run post-generation formatter commands. Delegates to post_formatters module."""
25+ run_post_formatters (cfg , out_dir , config_path )
12426
12527
12628def _build_file_context (
12729 file_path : str ,
12830 mapped_file : MappedFile ,
129- global_context : Dict [ str , Any ] ,
31+ ctx : "TemplateContext" ,
13032) -> Dict [str , Any ]:
131- """Build context for a single file template."""
33+ """Build context dict for a single file Jinja2 template."""
13234 from pathlib import PurePosixPath
13335
13436 def _iter_callable_bridges ():
135- for fn in mapped_file .functions :
136- yield fn
37+ yield from mapped_file .functions
13738 for cls in mapped_file .classes :
138- for method in cls .methods :
139- yield method
39+ yield from cls .methods
14040
14141 callables = list (_iter_callable_bridges ())
14242 uses_ui = any (
@@ -157,21 +57,21 @@ def _iter_callable_bridges():
15757 p = PurePosixPath (file_path )
15858
15959 return {
160- # Global context
161- "module" : global_context [ " module" ] ,
162- "files" : global_context [ " files" ] ,
163- "file_paths" : global_context [ " file_paths" ] ,
164- "mapping" : global_context [ " mapping" ] ,
165- "namer" : global_context [ " namer" ] ,
166- "raw" : global_context [ " raw" ] ,
60+ # Global context (flattened for Jinja2)
61+ "module" : ctx . module ,
62+ "files" : ctx . files ,
63+ "file_paths" : ctx . file_paths ,
64+ "mapping" : ctx . mapping ,
65+ "namer" : ctx . namer ,
66+ "raw" : ctx . raw ,
16767 # All items (for cross-referencing)
168- "all_items" : global_context [ " items" ] ,
169- "all_types" : global_context [ " types" ] ,
170- "all_enums" : global_context [ " enums" ] ,
171- "all_functions" : global_context [ " functions" ] ,
172- "all_classes" : global_context [ " classes" ] ,
173- "all_constants" : global_context [ " constants" ] ,
174- "all_aliases" : global_context [ " aliases" ] ,
68+ "all_items" : ctx . items ,
69+ "all_types" : ctx . types ,
70+ "all_enums" : ctx . enums ,
71+ "all_functions" : ctx . functions ,
72+ "all_classes" : ctx . classes ,
73+ "all_constants" : ctx . constants ,
74+ "all_aliases" : ctx . aliases ,
17575 # Current file items (mapped)
17676 "file" : mapped_file ,
17777 "file_path" : file_path ,
@@ -206,20 +106,32 @@ def _compute_output_path(
206106 template_name : str ,
207107 out_dir : Path ,
208108 namer : NameTransformer ,
109+ strip_prefix : str = "" ,
209110) -> Path :
210111 """
211112 Compute output path from IR file path and template name.
212113
114+ If *strip_prefix* is given, the first occurrence of it in the IR file
115+ path is removed before computing the output sub-directory. This allows
116+ stripping an outer prefix such as ``Sources/CNativeAPI/src`` so that
117+ the output lands directly under *out_dir*.
118+
213119 Rules:
214- - IR path: src/foundation/geometry.h
215- - Template: file/dart.j2 -> out/src /foundation/geometry.dart
216- - Template: file/rs .j2 -> out/src/foundation/ geometry.rs
120+ - IR path: src/foundation/geometry.h, strip_prefix="src/"
121+ -> out/foundation/geometry.dart
122+ - Template: file/dart .j2 -> out/geometry.dart (stem "dart")
217123
218124 File name is transformed according to naming config.
219125 """
220126 from pathlib import PurePosixPath
221127
222- ir_path = PurePosixPath (ir_file_path )
128+ adjusted = ir_file_path
129+ if strip_prefix :
130+ idx = adjusted .find (strip_prefix )
131+ if idx >= 0 :
132+ adjusted = adjusted [idx + len (strip_prefix ):]
133+
134+ ir_path = PurePosixPath (adjusted )
223135 # Template stem becomes the new extension
224136 template_stem = Path (template_name ).stem # e.g., "dart" from "dart.j2"
225137
@@ -262,31 +174,37 @@ def generate_bindings(
262174 _register_filters (env )
263175
264176 # Build context with preprocessed (mapped) data
265- global_context = build_context (module , cfg .mapping )
266- namer = global_context [ " namer" ]
177+ ctx = build_context (module , cfg .mapping )
178+ namer = ctx . namer
267179
268180 # 1. Render global templates (template/*.j2)
269181 for template_path in config_template_root .glob ("*.j2" ):
270182 template = env .get_template (template_path .name )
271183 output_name = template_path .stem
272- rendered = template .render (** global_context )
184+ rendered = template .render (** vars ( ctx ) )
273185 (out_dir / output_name ).write_text (rendered + "\n " , encoding = "utf-8" )
274186
275187 # 2. Render per-file templates (template/file/*.j2)
276188 file_template_dir = config_template_root / "file"
277189 if file_template_dir .exists ():
278190 file_templates = list (file_template_dir .glob ("*.j2" ))
279191
280- for ir_file_path in global_context [ " file_paths" ] :
281- mapped_file = global_context [ " files" ] [ir_file_path ]
192+ for ir_file_path in ctx . file_paths :
193+ mapped_file = ctx . files [ir_file_path ]
282194 file_context = _build_file_context (
283- ir_file_path , mapped_file , global_context
195+ ir_file_path , mapped_file , ctx
196+ )
197+
198+ # Resolve output path prefix to strip from IR file paths
199+ output_strip_prefix = (
200+ (cfg .mapping .options or {}).get ("output_path_prefix" , "" )
284201 )
285202
286203 for template_path in file_templates :
287204 template = env .get_template (f"file/{ template_path .name } " )
288205 output_path = _compute_output_path (
289- ir_file_path , template_path .name , out_dir , namer
206+ ir_file_path , template_path .name , out_dir , namer ,
207+ strip_prefix = output_strip_prefix ,
290208 )
291209
292210 # Create output directory if needed
@@ -300,62 +218,45 @@ def generate_bindings(
300218
301219
302220def _register_filters (env ) -> None :
303- """Register custom Jinja2 filters for naming conventions."""
304- import re
305-
306- def snake_case (s : str ) -> str :
307- """Convert to snake_case."""
308- s = re .sub (r"([A-Z]+)([A-Z][a-z])" , r"\1_\2" , s )
309- s = re .sub (r"([a-z\d])([A-Z])" , r"\1_\2" , s )
310- return s .lower ().replace ("-" , "_" )
311-
312- def camel_case (s : str ) -> str :
313- """Convert to camelCase."""
314- parts = re .split (r"[_\-\s]+" , s )
315- if not parts :
316- return s
317- return parts [0 ].lower () + "" .join (p .title () for p in parts [1 :])
318-
319- def pascal_case (s : str ) -> str :
320- """Convert to PascalCase."""
321- parts = re .split (r"[_\-\s]+" , s )
322- return "" .join (p .title () for p in parts )
323-
324- def screaming_snake_case (s : str ) -> str :
325- """Convert to SCREAMING_SNAKE_CASE."""
326- return snake_case (s ).upper ()
327-
328- def kebab_case (s : str ) -> str :
329- """Convert to kebab-case."""
330- return snake_case (s ).replace ("_" , "-" )
331-
332- def strip_prefix (s : str , prefix : str ) -> str :
221+ """Register custom Jinja2 filters for naming conventions.
222+
223+ Delegates to naming.py to avoid duplicating naming logic.
224+ """
225+ from .naming import (
226+ to_snake_case ,
227+ to_camel_case ,
228+ to_pascal_case ,
229+ to_screaming_snake_case ,
230+ to_kebab_case ,
231+ )
232+
233+ def _strip_prefix (s : str , prefix : str ) -> str :
333234 """Strip prefix from string."""
334235 if s .startswith (prefix ):
335236 return s [len (prefix ) :]
336237 return s
337238
338- def strip_suffix (s : str , suffix : str ) -> str :
239+ def _strip_suffix (s : str , suffix : str ) -> str :
339240 """Strip suffix from string."""
340241 if s .endswith (suffix ):
341242 return s [: - len (suffix )]
342243 return s
343244
344- def add_prefix (s : str , prefix : str ) -> str :
245+ def _add_prefix (s : str , prefix : str ) -> str :
345246 """Add prefix to string."""
346247 return prefix + s
347248
348- def add_suffix (s : str , suffix : str ) -> str :
249+ def _add_suffix (s : str , suffix : str ) -> str :
349250 """Add suffix to string."""
350251 return s + suffix
351252
352253 # Register naming filters
353- env .filters ["snake_case" ] = snake_case
354- env .filters ["camel_case" ] = camel_case
355- env .filters ["pascal_case" ] = pascal_case
356- env .filters ["screaming_snake_case" ] = screaming_snake_case
357- env .filters ["kebab_case" ] = kebab_case
358- env .filters ["strip_prefix" ] = strip_prefix
359- env .filters ["strip_suffix" ] = strip_suffix
360- env .filters ["add_prefix" ] = add_prefix
361- env .filters ["add_suffix" ] = add_suffix
254+ env .filters ["snake_case" ] = to_snake_case
255+ env .filters ["camel_case" ] = to_camel_case
256+ env .filters ["pascal_case" ] = to_pascal_case
257+ env .filters ["screaming_snake_case" ] = to_screaming_snake_case
258+ env .filters ["kebab_case" ] = to_kebab_case
259+ env .filters ["strip_prefix" ] = _strip_prefix
260+ env .filters ["strip_suffix" ] = _strip_suffix
261+ env .filters ["add_prefix" ] = _add_prefix
262+ env .filters ["add_suffix" ] = _add_suffix
0 commit comments