11import json
22import logging
33import os
4- from typing import Dict , Any , Optional , List , Tuple
4+ import re
5+ from typing import Any , Dict , List
56
67from jinja2 import Environment , FileSystemLoader , Template , select_autoescape
78
89# Get the directory where our templates are stored
910TEMPLATE_DIR = os .path .join (os .path .dirname (os .path .abspath (__file__ )))
1011
1112
13+ def escape_raw_newlines_in_json_strings (raw_json : str ) -> str :
14+ def fix_string (match ):
15+ s = match .group (1 )
16+ return s .replace ("\n " , "\\ n" ) # Escape raw newlines in strings
17+
18+ STRING_LITERAL_RE = re .compile (r'("(?:\\.|[^"\\])*")' , flags = re .DOTALL )
19+ return STRING_LITERAL_RE .sub (fix_string , raw_json )
20+
21+
1222class SlackTemplateLoader :
1323 """
1424 Loads and renders Jinja2 templates for Slack messages.
@@ -28,10 +38,10 @@ def __init__(self):
2838 def get_template (self , template_name : str ) -> Template :
2939 """
3040 Get a template by name, loading from file if not already cached.
31-
41+
3242 Args:
3343 template_name: The name of the template file (e.g., "header.j2")
34-
44+
3545 Returns:
3646 A Jinja2 Template object
3747 """
@@ -42,41 +52,73 @@ def get_template(self, template_name: str) -> Template:
4252 logging .error (f"Error loading template { template_name } : { e } " )
4353 # Return a simple default template as fallback
4454 return Template ("Template loading error" )
45-
55+
4656 return self ._templates [template_name ]
4757
4858 def render_to_blocks (self , template_name : str , context : Dict [str , Any ]) -> List [Dict [str , Any ]]:
4959 """
5060 Render a template using the provided context and parse the result as JSON to get Slack blocks.
51-
61+
5262 Args:
5363 template_name: The name of the template file
5464 context: Dictionary of variables to pass to the template
55-
65+
5666 Returns:
5767 List of Slack block objects (dictionaries)
5868 """
5969 template = self .get_template (template_name )
60-
70+
6171 try :
6272 rendered = template .render (** context )
63-
73+
6474 # Split by newlines to get multiple blocks and parse each as JSON
6575 blocks = []
76+ blocks = []
6677 for block_str in rendered .strip ().split ("\n \n " ):
67- if block_str .strip ():
68- try :
69- block = json .loads (block_str )
70- blocks .append (block )
71- except json .JSONDecodeError as e :
72- logging .error (f"Error parsing JSON from template output: { e } " )
73- logging .debug (f"Problematic JSON: { block_str } " )
74-
78+ if not block_str .strip ():
79+ continue
80+
81+ try :
82+ block_str_fixed = escape_raw_newlines_in_json_strings (block_str )
83+ block = json .loads (block_str_fixed )
84+ blocks .append (block )
85+
86+ except json .JSONDecodeError as e :
87+ logging .exception (f"Error parsing JSON from template output: { e } " )
88+ logging .warning (f"Problematic JSON (repr): { repr (block_str )} " )
89+
7590 return blocks
7691 except Exception as e :
7792 logging .error (f"Error rendering template { template_name } : { e } " )
7893 return []
7994
95+ def render_custom_or_file_template_to_blocks (self , template_name : str , context : Dict [str , Any ], custom_template : str = None ) -> List [Dict [str , Any ]]:
96+ """
97+ Render a custom Jinja template string (if provided) or a file-based template to Slack blocks.
98+ Args:
99+ template_name: The name of the file-based template (e.g., "header.j2")
100+ context: Dictionary of variables to pass to the template
101+ custom_template: Optional Jinja template string to use instead of file-based template
102+ Returns:
103+ List of Slack block objects (dictionaries)
104+ """
105+ if custom_template :
106+ try :
107+ template = Template (custom_template )
108+ rendered_blocks = []
109+ for block_str in template .render (** context ).strip ().split ("\n \n " ):
110+ if block_str .strip ():
111+ block_str_fixed = escape_raw_newlines_in_json_strings (block_str )
112+ block = json .loads (block_str_fixed )
113+ rendered_blocks .append (block )
114+ return rendered_blocks
115+ except Exception as e :
116+ logging .error (f"Error rendering custom template: { e } " )
117+ # Fall back to file-based template
118+
119+ # Use file-based template
120+ return self .render_to_blocks (template_name , context )
121+
80122
81123# Singleton instance
82- template_loader = SlackTemplateLoader ()
124+ template_loader = SlackTemplateLoader ()
0 commit comments