Coverage for /Users/davegaeddert/Development/dropseed/plain/plain-htmx/plain/htmx/jinja.py: 23%

86 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-10-16 22:04 -0500

1import jinja2 

2from jinja2 import meta, nodes 

3from jinja2.ext import Extension 

4 

5from plain.runtime import settings 

6from plain.templates.jinja.extensions import InclusionTagExtension 

7 

8 

9class HTMXJSExtension(InclusionTagExtension): 

10 tags = {"htmx_js"} 

11 template_name = "htmx/js.html" 

12 

13 def get_context(self, context, *args, **kwargs): 

14 return { 

15 "csrf_token": context["csrf_token"], 

16 "DEBUG": settings.DEBUG, 

17 "extensions": kwargs.get("extensions", []), 

18 } 

19 

20 

21class HTMXFragmentExtension(Extension): 

22 tags = {"htmxfragment"} 

23 

24 def __init__(self, environment): 

25 super().__init__(environment) 

26 environment.extend(htmx_fragment_nodes={}) 

27 

28 def parse(self, parser): 

29 lineno = next(parser.stream).lineno 

30 

31 fragment_name = parser.parse_expression() 

32 

33 kwargs = [] 

34 

35 while parser.stream.current.type != "block_end": 

36 if parser.stream.current.type == "name": 

37 key = parser.stream.current.value 

38 parser.stream.skip() 

39 parser.stream.expect("assign") 

40 value = parser.parse_expression() 

41 kwargs.append(nodes.Keyword(key, value)) 

42 

43 body = parser.parse_statements(["name:endhtmxfragment"], drop_needle=True) 

44 

45 call = self.call_method( 

46 "_render_htmx_fragment", 

47 args=[fragment_name, jinja2.nodes.ContextReference()], 

48 kwargs=kwargs, 

49 ) 

50 

51 node = jinja2.nodes.CallBlock(call, [], [], body).set_lineno(lineno) 

52 

53 # Store a reference to the node for later 

54 self.environment.htmx_fragment_nodes.setdefault(parser.name, {})[ 

55 fragment_name.value 

56 ] = node 

57 

58 return node 

59 

60 def _render_htmx_fragment(self, fragment_name, context, caller, **kwargs): 

61 def attrs_to_str(attrs): 

62 parts = [] 

63 for k, v in attrs.items(): 

64 if v == "": 

65 parts.append(k) 

66 else: 

67 parts.append(f'{k}="{v}"') 

68 return " ".join(parts) 

69 

70 render_lazy = kwargs.get("lazy", False) 

71 as_element = kwargs.get("as", "div") 

72 attrs = {} 

73 for k, v in kwargs.items(): 

74 if k.startswith("hx_"): 

75 attrs[k.replace("_", "-")] = v 

76 else: 

77 attrs[k] = v 

78 

79 if render_lazy: 

80 attrs.setdefault("hx-swap", "outerHTML") 

81 attrs.setdefault("hx-target", "this") 

82 attrs.setdefault("hx-indicator", "this") 

83 attrs_str = attrs_to_str(attrs) 

84 return f'<{as_element} plain-hx-fragment="{fragment_name}" hx-get hx-trigger="plainhtmx:load from:body" {attrs_str}></{as_element}>' 

85 else: 

86 # Swap innerHTML so we can re-run hx calls inside the fragment automatically 

87 # (render_template_fragment won't render this part of the node again, just the inner nodes) 

88 attrs.setdefault("hx-swap", "innerHTML") 

89 attrs.setdefault("hx-target", "this") 

90 attrs.setdefault("hx-indicator", "this") 

91 # Add an id that you can use to target the fragment from outside the fragment 

92 attrs.setdefault("id", f"plain-hx-fragment-{fragment_name}") 

93 attrs_str = attrs_to_str(attrs) 

94 return f'<{as_element} plain-hx-fragment="{fragment_name}" {attrs_str}>{caller()}</{as_element}>' 

95 

96 

97def render_template_fragment(*, template, fragment_name, context): 

98 template = find_template_fragment(template, fragment_name) 

99 return template.render(context) 

100 

101 

102def find_template_fragment(template: jinja2.Template, fragment_name: str): 

103 # Look in this template for the fragment 

104 callblock_node = template.environment.htmx_fragment_nodes.get( 

105 template.name, {} 

106 ).get(fragment_name) 

107 

108 if not callblock_node: 

109 # Look in other templates for this fragment 

110 matching_callblock_nodes = [] 

111 for fragments in template.environment.htmx_fragment_nodes.values(): 

112 if fragment_name in fragments: 

113 matching_callblock_nodes.append(fragments[fragment_name]) 

114 

115 if len(matching_callblock_nodes) == 0: 

116 # If we still haven't found anything, it's possible that we're 

117 # in a different/new worker/process and haven't parsed the related templates yet 

118 ast = template.environment.parse( 

119 template.environment.loader.get_source( 

120 template.environment, template.name 

121 )[0] 

122 ) 

123 for ref in meta.find_referenced_templates(ast): 

124 if ref not in template.environment.htmx_fragment_nodes: 

125 # Trigger them to parse 

126 template.environment.get_template(ref) 

127 

128 # Now look again 

129 for fragments in template.environment.htmx_fragment_nodes.values(): 

130 if fragment_name in fragments: 

131 matching_callblock_nodes.append(fragments[fragment_name]) 

132 

133 if len(matching_callblock_nodes) == 1: 

134 callblock_node = matching_callblock_nodes[0] 

135 elif len(matching_callblock_nodes) > 1: 

136 raise jinja2.TemplateNotFound( 

137 f"Fragment {fragment_name} found in multiple templates. Use a more specific name." 

138 ) 

139 else: 

140 raise jinja2.TemplateNotFound( 

141 f"Fragment {fragment_name} not found in any templates" 

142 ) 

143 

144 if not callblock_node: 

145 raise jinja2.TemplateNotFound( 

146 f"Fragment {fragment_name} not found in template {template.name}" 

147 ) 

148 

149 # Create a new template from the node 

150 template_node = jinja2.nodes.Template(callblock_node.body) 

151 return template.environment.from_string(template_node) 

152 

153 

154extensions = [ 

155 HTMXJSExtension, 

156 HTMXFragmentExtension, 

157]