77import jinja2
88
99__all__ = [
10- "process_template_data" ,
1110 "create_env" ,
11+ "process_template_data" ,
1212]
1313
14-
15- def highlight (text , style = None ):
16- """Replaces <highlight>text</highlight> with highlighted tspan elements."""
17- if not text :
18- return text
19-
20- if not style or "highlight_colour" not in style :
21- raise ValueError (
22- "highlight_colour must be configured in style to use <highlight> tags"
23- )
24-
25- def replacer (match ):
26- return (
27- f'<tspan style="fill:{ style ["highlight_colour" ]} ">{ match .group (1 )} </tspan>'
28- )
29-
30- return re .sub (r"<highlight>(.*?)</highlight>" , replacer , text )
14+ # Fields that need line counting for positioning
15+ LINE_COUNT_FIELDS = {"text" , "title" }
3116
3217
3318def process_style (style , theme ):
@@ -39,38 +24,100 @@ def process_style(style, theme):
3924
4025
4126def process_text_with_jinja (env , text , template_data ):
42- """Process text through Jinja templating."""
27+ """Process text through Jinja templating and apply highlighting ."""
4328 if text is None :
4429 return None
4530
4631 template = env .from_string (text )
4732 processed = template .render (** template_data )
48- if "style" in template_data :
49- processed = highlight (processed , template_data ["style" ])
33+
34+ if "style" in template_data and "highlight_colour" in template_data ["style" ]:
35+
36+ def replacer (match ):
37+ return (
38+ f'<tspan style="fill:{ template_data ["style" ]["highlight_colour" ]} ">'
39+ f"{ match .group (1 )} </tspan>"
40+ )
41+
42+ processed = re .sub (r"<highlight>(.*?)</highlight>" , replacer , processed )
43+
5044 return processed
5145
5246
53- def process_list_item_texts (env , items , template_data ):
54- """Process text fields in list items through Jinja."""
55- for item in items :
47+ def process_list_items (list_items , template_data ):
48+ """Process a list of text items.
49+
50+ Applies Jinja templating and calculates positions for SVG layout.
51+ """
52+ env = jinja2 .Environment ()
53+ previous_lines = 0
54+
55+ for i , item in enumerate (list_items ):
56+ # Process text fields
5657 if "text" in item :
5758 item ["text" ] = process_text_with_jinja (env , item ["text" ], template_data )
5859 if "title" in item :
5960 item ["title" ] = process_text_with_jinja (env , item ["title" ], template_data )
60- return items
61-
6261
63- def process_list_items (list_items ):
64- """Process a list of text items to calculate line positions."""
65- previous_lines = 0
66- for i , item in enumerate (list_items ):
62+ # Calculate positions for SVG layout
6763 item ["lines" ] = previous_lines
6864 item ["position" ] = i
69- if item .get ("text" ) is not None :
70- previous_lines = item ["text" ].count ("\n " ) + previous_lines + 1
65+ # Count lines from both text and title fields
66+ for field in LINE_COUNT_FIELDS :
67+ if item .get (field ) is not None :
68+ previous_lines = item [field ].count ("\n " ) + previous_lines + 1
69+
7170 return list_items
7271
7372
73+ def process_nested_text (template , data = None ):
74+ """Process text fields in any nested dictionary or list structure.
75+
76+ Args:
77+ template: The template structure to process
78+ data: Optional data to use for rendering. If None, uses template as data.
79+ """
80+ env = jinja2 .Environment ()
81+ render_data = data if data is not None else template
82+
83+ if isinstance (template , dict ):
84+ return {
85+ key : process_nested_text (value , render_data )
86+ if isinstance (value , dict | list )
87+ else process_text_with_jinja (env , value , render_data )
88+ if isinstance (value , str )
89+ else value
90+ for key , value in template .items ()
91+ }
92+ if isinstance (template , list ):
93+ return [
94+ process_nested_text (item , render_data )
95+ if isinstance (item , dict | list )
96+ else process_text_with_jinja (env , item , render_data )
97+ if isinstance (item , str )
98+ else item
99+ for item in template
100+ ]
101+
102+ return template
103+
104+
105+ def process_nested_lists (data , template_data ):
106+ """Process any nested lists of items that have text fields."""
107+ if isinstance (data , dict ):
108+ for key , value in data .items ():
109+ if isinstance (value , list ) and value and isinstance (value [0 ], dict ):
110+ # Check if any item in the list has text or title fields
111+ if any ("text" in item or "title" in item for item in value ):
112+ data [key ] = process_list_items (value , template_data )
113+ elif isinstance (value , dict | list ):
114+ process_nested_lists (value , template_data )
115+ elif isinstance (data , list ):
116+ for item in data :
117+ if isinstance (item , dict | list ):
118+ process_nested_lists (item , template_data )
119+
120+
74121def process_template_data (template_data , defaults , images_dir = None ):
75122 """Process and enhance template data with images, list items, and styling."""
76123 # Process style first
@@ -83,30 +130,11 @@ def process_template_data(template_data, defaults, images_dir=None):
83130
84131 template_data ["style" ] = process_style (template_data ["style" ], defaults ["theme" ])
85132
86- # Create single Jinja environment for all text processing
87- env = jinja2 .Environment ()
88-
89133 # Process all text fields through Jinja
90- for key in template_data :
91- if (key == "text" or key .startswith ("text_" )) and template_data [
92- key
93- ] is not None :
94- template_data [key ] = process_text_with_jinja (
95- env , template_data [key ], template_data
96- )
134+ template_data = process_nested_text (template_data )
97135
98- # Process list items
99- if template_data .get ("list_items" ) is not None :
100- template_data ["list_items" ] = process_list_item_texts (
101- env , template_data ["list_items" ], template_data
102- )
103- template_data ["list_items" ] = process_list_items (template_data ["list_items" ])
104-
105- if template_data .get ("specs_items" ) is not None :
106- template_data ["specs_items" ] = process_list_item_texts (
107- env , template_data ["specs_items" ], template_data
108- )
109- template_data ["specs_items" ] = process_list_items (template_data ["specs_items" ])
136+ # Process any nested lists of items that have text fields
137+ process_nested_lists (template_data , template_data )
110138
111139 # Process images
112140 if template_data .get ("images" ) is not None :
@@ -124,29 +152,9 @@ def create_env(templates_dir=None):
124152 loader = jinja2 .FileSystemLoader (str (templates_dir )),
125153 autoescape = jinja2 .select_autoescape (),
126154 )
127- env .filters ["space_bullets" ] = space_bullets
128155 return env
129156
130157
131- def space_bullets (text ):
132- """Add spacing after each bullet point while preserving line formatting."""
133- if not text :
134- return text
135-
136- lines = text .split ("\n " )
137- result = []
138-
139- for i , line in enumerate (lines ):
140- if line .strip ().startswith ("•" ):
141- if i > 0 and not lines [i - 1 ].strip () == "" :
142- result .append ("" )
143- result .append (line )
144- else :
145- result .append (line )
146-
147- return "\n " .join (result )
148-
149-
150158def encode_image (filename , images_dir ):
151159 """Encode an image file to a base64 data URI."""
152160 image_path = os .path .join (images_dir , filename )
0 commit comments