11from __future__ import annotations
22
33from pathlib import Path
4+ from typing import Any , Dict , List
45
56from ..config import BindgenConfig
6- from ..ir .model import IRModule
7+ from ..ir .model import IRFile , IRModule
78from .context import build_context
89
910
1011def _load_jinja ():
1112 try :
12- from jinja2 import Environment , FileSystemLoader # type: ignore
13+ from jinja2 import BaseLoader , Environment , FileSystemLoader , TemplateNotFound
1314 except Exception as exc : # pragma: no cover
1415 raise RuntimeError (
1516 "jinja2 is required. Install with `pip install jinja2`."
1617 ) from exc
17- return Environment , FileSystemLoader
18+ return Environment , FileSystemLoader , BaseLoader , TemplateNotFound
19+
20+
21+ def _build_file_context (
22+ file_path : str ,
23+ ir_file : IRFile ,
24+ global_context : Dict [str , Any ],
25+ ) -> Dict [str , Any ]:
26+ """Build context for a single file template."""
27+ from pathlib import PurePosixPath
28+
29+ p = PurePosixPath (file_path )
30+
31+ return {
32+ # Global context
33+ "module" : global_context ["module" ],
34+ "files" : global_context ["files" ],
35+ "file_paths" : global_context ["file_paths" ],
36+ "mapping" : global_context ["mapping" ],
37+ # All items (for cross-referencing)
38+ "all_types" : global_context ["types" ],
39+ "all_enums" : global_context ["enums" ],
40+ "all_functions" : global_context ["functions" ],
41+ "all_classes" : global_context ["classes" ],
42+ "all_constants" : global_context ["constants" ],
43+ "all_aliases" : global_context ["aliases" ],
44+ # Current file items
45+ "file" : ir_file ,
46+ "file_path" : file_path ,
47+ "file_dir" : str (p .parent ) if p .parent != PurePosixPath ("." ) else "" ,
48+ "file_name" : p .name ,
49+ "file_stem" : p .stem ,
50+ "file_ext" : p .suffix ,
51+ "types" : ir_file .types ,
52+ "enums" : ir_file .enums ,
53+ "functions" : ir_file .functions ,
54+ "classes" : ir_file .classes ,
55+ "constants" : ir_file .constants ,
56+ "aliases" : ir_file .aliases ,
57+ # Convenience flags
58+ "is_empty" : (
59+ not ir_file .types
60+ and not ir_file .enums
61+ and not ir_file .functions
62+ and not ir_file .classes
63+ and not ir_file .constants
64+ and not ir_file .aliases
65+ ),
66+ "has_types" : bool (ir_file .types ),
67+ "has_enums" : bool (ir_file .enums ),
68+ "has_functions" : bool (ir_file .functions ),
69+ "has_classes" : bool (ir_file .classes ),
70+ "has_constants" : bool (ir_file .constants ),
71+ "has_aliases" : bool (ir_file .aliases ),
72+ }
73+
74+
75+ def _register_partials (env , partials_dir : Path ) -> None :
76+ """Register partial templates as macros that can be included."""
77+ if not partials_dir .exists ():
78+ return
79+
80+ # Partials are available via {% include 'partials/xxx.j2' %}
81+ # No special registration needed since we use FileSystemLoader
82+
83+
84+ def _compute_output_path (
85+ ir_file_path : str ,
86+ template_name : str ,
87+ out_dir : Path ,
88+ ) -> Path :
89+ """
90+ Compute output path from IR file path and template name.
91+
92+ Rules:
93+ - IR path: src/foundation/geometry.h
94+ - Template: file/dart.j2 -> out/src/foundation/geometry.dart
95+ - Template: file/rs.j2 -> out/src/foundation/geometry.rs
96+ """
97+ from pathlib import PurePosixPath
98+
99+ ir_path = PurePosixPath (ir_file_path )
100+ # Template stem becomes the new extension
101+ template_stem = Path (template_name ).stem # e.g., "dart" from "dart.j2"
102+
103+ # Build output path: out_dir / ir_dir / ir_stem.template_stem
104+ output_name = f"{ ir_path .stem } .{ template_stem } "
105+ output_path = out_dir / ir_path .parent / output_name
106+
107+ return output_path
18108
19109
20110def generate_bindings (
21111 module : IRModule , cfg : BindgenConfig , out_dir : Path , config_path : Path
22112) -> None :
113+ """
114+ Generate bindings using templates.
115+
116+ Template directory structure:
117+ - template/*.j2 - Global templates (rendered once)
118+ - template/file/*.j2 - Per-file templates (rendered for each IR file)
119+ - template/partials/*.j2 - Partial templates (type.j2, enum.j2, function.j2, etc.)
120+
121+ Partials can be included in file templates:
122+ {% include 'partials/type.j2' %}
123+ """
23124 # Templates are discovered from config.yaml sibling directory:
24125 # <config_dir>/template/*.j2
25126 config_template_root = config_path .resolve ().parent / "template"
@@ -29,14 +130,102 @@ def generate_bindings(
29130 mapping = dict (cfg .mapping )
30131 mapping .setdefault ("language" , "default" )
31132
32- Environment , FileSystemLoader = _load_jinja ()
133+ Environment , FileSystemLoader , BaseLoader , TemplateNotFound = _load_jinja ()
33134 env = Environment (
34135 loader = FileSystemLoader (str (config_template_root )), autoescape = False
35136 )
36- context = build_context (module , mapping )
37137
138+ # Register any custom filters here if needed
139+ _register_filters (env )
140+
141+ global_context = build_context (module , mapping )
142+
143+ # 1. Render global templates (template/*.j2)
38144 for template_path in config_template_root .glob ("*.j2" ):
39145 template = env .get_template (template_path .name )
40146 output_name = template_path .stem
41- rendered = template .render (** context )
147+ rendered = template .render (** global_context )
42148 (out_dir / output_name ).write_text (rendered + "\n " , encoding = "utf-8" )
149+
150+ # 2. Render per-file templates (template/file/*.j2)
151+ file_template_dir = config_template_root / "file"
152+ if file_template_dir .exists ():
153+ file_templates = list (file_template_dir .glob ("*.j2" ))
154+
155+ for ir_file_path in global_context ["file_paths" ]:
156+ ir_file = global_context ["files" ][ir_file_path ]
157+ file_context = _build_file_context (ir_file_path , ir_file , global_context )
158+
159+ for template_path in file_templates :
160+ template = env .get_template (f"file/{ template_path .name } " )
161+ output_path = _compute_output_path (
162+ ir_file_path , template_path .name , out_dir
163+ )
164+
165+ # Create output directory if needed
166+ output_path .parent .mkdir (parents = True , exist_ok = True )
167+
168+ rendered = template .render (** file_context )
169+ output_path .write_text (rendered + "\n " , encoding = "utf-8" )
170+
171+
172+ def _register_filters (env ) -> None :
173+ """Register custom Jinja2 filters."""
174+ import re
175+
176+ def snake_case (s : str ) -> str :
177+ """Convert to snake_case."""
178+ s = re .sub (r"([A-Z]+)([A-Z][a-z])" , r"\1_\2" , s )
179+ s = re .sub (r"([a-z\d])([A-Z])" , r"\1_\2" , s )
180+ return s .lower ().replace ("-" , "_" )
181+
182+ def camel_case (s : str ) -> str :
183+ """Convert to camelCase."""
184+ parts = re .split (r"[_\-\s]+" , s )
185+ if not parts :
186+ return s
187+ return parts [0 ].lower () + "" .join (p .title () for p in parts [1 :])
188+
189+ def pascal_case (s : str ) -> str :
190+ """Convert to PascalCase."""
191+ parts = re .split (r"[_\-\s]+" , s )
192+ return "" .join (p .title () for p in parts )
193+
194+ def screaming_snake_case (s : str ) -> str :
195+ """Convert to SCREAMING_SNAKE_CASE."""
196+ return snake_case (s ).upper ()
197+
198+ def kebab_case (s : str ) -> str :
199+ """Convert to kebab-case."""
200+ return snake_case (s ).replace ("_" , "-" )
201+
202+ def strip_prefix (s : str , prefix : str ) -> str :
203+ """Strip prefix from string."""
204+ if s .startswith (prefix ):
205+ return s [len (prefix ) :]
206+ return s
207+
208+ def strip_suffix (s : str , suffix : str ) -> str :
209+ """Strip suffix from string."""
210+ if s .endswith (suffix ):
211+ return s [: - len (suffix )]
212+ return s
213+
214+ def add_prefix (s : str , prefix : str ) -> str :
215+ """Add prefix to string."""
216+ return prefix + s
217+
218+ def add_suffix (s : str , suffix : str ) -> str :
219+ """Add suffix to string."""
220+ return s + suffix
221+
222+ # Register all filters
223+ env .filters ["snake_case" ] = snake_case
224+ env .filters ["camel_case" ] = camel_case
225+ env .filters ["pascal_case" ] = pascal_case
226+ env .filters ["screaming_snake_case" ] = screaming_snake_case
227+ env .filters ["kebab_case" ] = kebab_case
228+ env .filters ["strip_prefix" ] = strip_prefix
229+ env .filters ["strip_suffix" ] = strip_suffix
230+ env .filters ["add_prefix" ] = add_prefix
231+ env .filters ["add_suffix" ] = add_suffix
0 commit comments