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
« 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
5from plain.runtime import settings
6from plain.templates.jinja.extensions import InclusionTagExtension
9class HTMXJSExtension(InclusionTagExtension):
10 tags = {"htmx_js"}
11 template_name = "htmx/js.html"
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 }
21class HTMXFragmentExtension(Extension):
22 tags = {"htmxfragment"}
24 def __init__(self, environment):
25 super().__init__(environment)
26 environment.extend(htmx_fragment_nodes={})
28 def parse(self, parser):
29 lineno = next(parser.stream).lineno
31 fragment_name = parser.parse_expression()
33 kwargs = []
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))
43 body = parser.parse_statements(["name:endhtmxfragment"], drop_needle=True)
45 call = self.call_method(
46 "_render_htmx_fragment",
47 args=[fragment_name, jinja2.nodes.ContextReference()],
48 kwargs=kwargs,
49 )
51 node = jinja2.nodes.CallBlock(call, [], [], body).set_lineno(lineno)
53 # Store a reference to the node for later
54 self.environment.htmx_fragment_nodes.setdefault(parser.name, {})[
55 fragment_name.value
56 ] = node
58 return node
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)
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
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}>'
97def render_template_fragment(*, template, fragment_name, context):
98 template = find_template_fragment(template, fragment_name)
99 return template.render(context)
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)
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])
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)
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])
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 )
144 if not callblock_node:
145 raise jinja2.TemplateNotFound(
146 f"Fragment {fragment_name} not found in template {template.name}"
147 )
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)
154extensions = [
155 HTMXJSExtension,
156 HTMXFragmentExtension,
157]