Coverage for /Users/davegaeddert/Developer/dropseed/plain/plain-htmx/plain/htmx/templates.py: 25%
88 statements
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-23 11:16 -0600
« prev ^ index » next coverage.py v7.6.9, created at 2024-12-23 11:16 -0600
1import jinja2
2from jinja2 import meta, nodes
3from jinja2.ext import Extension
5from plain.runtime import settings
6from plain.templates import register_template_extension
7from plain.templates.jinja.extensions import InclusionTagExtension
10@register_template_extension
11class HTMXJSExtension(InclusionTagExtension):
12 tags = {"htmx_js"}
13 template_name = "htmx/js.html"
15 def get_context(self, context, *args, **kwargs):
16 return {
17 "csrf_token": context["csrf_token"],
18 "DEBUG": settings.DEBUG,
19 "extensions": kwargs.get("extensions", []),
20 }
23@register_template_extension
24class HTMXFragmentExtension(Extension):
25 tags = {"htmxfragment"}
27 def __init__(self, environment):
28 super().__init__(environment)
29 environment.extend(htmx_fragment_nodes={})
31 def parse(self, parser):
32 lineno = next(parser.stream).lineno
34 fragment_name = parser.parse_expression()
36 kwargs = []
38 while parser.stream.current.type != "block_end":
39 if parser.stream.current.type == "name":
40 key = parser.stream.current.value
41 parser.stream.skip()
42 parser.stream.expect("assign")
43 value = parser.parse_expression()
44 kwargs.append(nodes.Keyword(key, value))
46 body = parser.parse_statements(["name:endhtmxfragment"], drop_needle=True)
48 call = self.call_method(
49 "_render_htmx_fragment",
50 args=[fragment_name, jinja2.nodes.ContextReference()],
51 kwargs=kwargs,
52 )
54 node = jinja2.nodes.CallBlock(call, [], [], body).set_lineno(lineno)
56 # Store a reference to the node for later
57 self.environment.htmx_fragment_nodes.setdefault(parser.name, {})[
58 fragment_name.value
59 ] = node
61 return node
63 def _render_htmx_fragment(self, fragment_name, context, caller, **kwargs):
64 def attrs_to_str(attrs):
65 parts = []
66 for k, v in attrs.items():
67 if v == "":
68 parts.append(k)
69 else:
70 parts.append(f'{k}="{v}"')
71 return " ".join(parts)
73 render_lazy = kwargs.get("lazy", False)
74 as_element = kwargs.get("as", "div")
75 attrs = {}
76 for k, v in kwargs.items():
77 if k.startswith("hx_"):
78 attrs[k.replace("_", "-")] = v
79 else:
80 attrs[k] = v
82 if render_lazy:
83 attrs.setdefault("hx-swap", "outerHTML")
84 attrs.setdefault("hx-target", "this")
85 attrs.setdefault("hx-indicator", "this")
86 attrs_str = attrs_to_str(attrs)
87 return f'<{as_element} plain-hx-fragment="{fragment_name}" hx-get hx-trigger="plainhtmx:load from:body" {attrs_str}></{as_element}>'
88 else:
89 # Swap innerHTML so we can re-run hx calls inside the fragment automatically
90 # (render_template_fragment won't render this part of the node again, just the inner nodes)
91 attrs.setdefault("hx-swap", "innerHTML")
92 attrs.setdefault("hx-target", "this")
93 attrs.setdefault("hx-indicator", "this")
94 # Add an id that you can use to target the fragment from outside the fragment
95 attrs.setdefault("id", f"plain-hx-fragment-{fragment_name}")
96 attrs_str = attrs_to_str(attrs)
97 return f'<{as_element} plain-hx-fragment="{fragment_name}" {attrs_str}>{caller()}</{as_element}>'
100def render_template_fragment(*, template, fragment_name, context):
101 template = find_template_fragment(template, fragment_name)
102 return template.render(context)
105def find_template_fragment(template: jinja2.Template, fragment_name: str):
106 # Look in this template for the fragment
107 callblock_node = template.environment.htmx_fragment_nodes.get(
108 template.name, {}
109 ).get(fragment_name)
111 if not callblock_node:
112 # Look in other templates for this fragment
113 matching_callblock_nodes = []
114 for fragments in template.environment.htmx_fragment_nodes.values():
115 if fragment_name in fragments:
116 matching_callblock_nodes.append(fragments[fragment_name])
118 if len(matching_callblock_nodes) == 0:
119 # If we still haven't found anything, it's possible that we're
120 # in a different/new worker/process and haven't parsed the related templates yet
121 ast = template.environment.parse(
122 template.environment.loader.get_source(
123 template.environment, template.name
124 )[0]
125 )
126 for ref in meta.find_referenced_templates(ast):
127 if ref not in template.environment.htmx_fragment_nodes:
128 # Trigger them to parse
129 template.environment.get_template(ref)
131 # Now look again
132 for fragments in template.environment.htmx_fragment_nodes.values():
133 if fragment_name in fragments:
134 matching_callblock_nodes.append(fragments[fragment_name])
136 if len(matching_callblock_nodes) == 1:
137 callblock_node = matching_callblock_nodes[0]
138 elif len(matching_callblock_nodes) > 1:
139 raise jinja2.TemplateNotFound(
140 f"Fragment {fragment_name} found in multiple templates. Use a more specific name."
141 )
142 else:
143 raise jinja2.TemplateNotFound(
144 f"Fragment {fragment_name} not found in any templates"
145 )
147 if not callblock_node:
148 raise jinja2.TemplateNotFound(
149 f"Fragment {fragment_name} not found in template {template.name}"
150 )
152 # Create a new template from the node
153 template_node = jinja2.nodes.Template(callblock_node.body)
154 return template.environment.from_string(template_node)