mirror of
https://github.com/django-components/django-components.git
synced 2025-08-16 04:00:14 +00:00
refactor: use component IDs as keys for slot fill llokup
This commit is contained in:
parent
c1369ab2c7
commit
ce5b5c40d8
5 changed files with 210 additions and 115 deletions
|
@ -20,18 +20,19 @@ from django.views import View
|
||||||
# way the two modules depend on one another.
|
# way the two modules depend on one another.
|
||||||
from django_components.component_registry import registry # NOQA
|
from django_components.component_registry import registry # NOQA
|
||||||
from django_components.component_registry import AlreadyRegistered, ComponentRegistry, NotRegistered, register # NOQA
|
from django_components.component_registry import AlreadyRegistered, ComponentRegistry, NotRegistered, register # NOQA
|
||||||
|
from django_components.context import capture_root_context, set_root_context, get_root_context, set_slot_component_association
|
||||||
from django_components.logger import logger, trace_msg
|
from django_components.logger import logger, trace_msg
|
||||||
from django_components.middleware import is_dependency_middleware_active
|
from django_components.middleware import is_dependency_middleware_active
|
||||||
|
from django_components.node import walk_nodelist
|
||||||
from django_components.slots import (
|
from django_components.slots import (
|
||||||
FillContent,
|
FillContent,
|
||||||
FillNode,
|
FillNode,
|
||||||
SlotName,
|
SlotName,
|
||||||
SlotNode,
|
SlotNode,
|
||||||
render_component_template_with_slots,
|
render_component_template_with_slots,
|
||||||
OUTER_CONTEXT_CONTEXT_KEY,
|
|
||||||
DEFAULT_SLOT_KEY,
|
DEFAULT_SLOT_KEY,
|
||||||
)
|
)
|
||||||
from django_components.utils import search, walk_nodelist, gen_id
|
from django_components.utils import search, gen_id
|
||||||
|
|
||||||
RENDERED_COMMENT_TEMPLATE = "<!-- _RENDERED {name} -->"
|
RENDERED_COMMENT_TEMPLATE = "<!-- _RENDERED {name} -->"
|
||||||
|
|
||||||
|
@ -233,6 +234,12 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
return mark_safe(f"<script>{self.js}</script>")
|
return mark_safe(f"<script>{self.js}</script>")
|
||||||
return mark_safe("\n".join(self.media.render_js()))
|
return mark_safe("\n".join(self.media.render_js()))
|
||||||
|
|
||||||
|
# NOTE: When the template is taken from a file (AKA
|
||||||
|
# specified via `template_name`), then we leverage
|
||||||
|
# Django's template caching. This means that the same
|
||||||
|
# instance of Template is reused. This is important to keep
|
||||||
|
# in mind, because the implication is that we should
|
||||||
|
# treat Templates AND their nodelists as IMMUTABLE.
|
||||||
def get_template(self, context: Mapping) -> Template:
|
def get_template(self, context: Mapping) -> Template:
|
||||||
template_string = self.get_template_string(context)
|
template_string = self.get_template_string(context)
|
||||||
if template_string is not None:
|
if template_string is not None:
|
||||||
|
@ -263,7 +270,7 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
def on_node(node: Node) -> None:
|
def on_node(node: Node) -> None:
|
||||||
if isinstance(node, SlotNode):
|
if isinstance(node, SlotNode):
|
||||||
trace_msg("ASSOC", "SLOT", node.name, node.node_id, component_id=self.component_id)
|
trace_msg("ASSOC", "SLOT", node.name, node.node_id, component_id=self.component_id)
|
||||||
node.component_id = self.component_id
|
set_slot_component_association(context, node.node_id, self.component_id)
|
||||||
|
|
||||||
walk_nodelist(template.nodelist, on_node)
|
walk_nodelist(template.nodelist, on_node)
|
||||||
|
|
||||||
|
@ -337,9 +344,7 @@ class ComponentNode(Node):
|
||||||
|
|
||||||
# If this is the outer-/top-most component node, then save the outer context,
|
# If this is the outer-/top-most component node, then save the outer context,
|
||||||
# so it can be used by nested Slots.
|
# so it can be used by nested Slots.
|
||||||
root_ctx_already_defined = OUTER_CONTEXT_CONTEXT_KEY in context
|
capture_root_context(context)
|
||||||
if not root_ctx_already_defined:
|
|
||||||
context.push({OUTER_CONTEXT_CONTEXT_KEY: context.__copy__()})
|
|
||||||
|
|
||||||
# Resolve FilterExpressions and Variables that were passed as args to the
|
# Resolve FilterExpressions and Variables that were passed as args to the
|
||||||
# component, then call component's context method
|
# component, then call component's context method
|
||||||
|
@ -373,9 +378,9 @@ class ComponentNode(Node):
|
||||||
# Even if contexts are isolated, we still need to pass down the
|
# Even if contexts are isolated, we still need to pass down the
|
||||||
# original context so variables in slots can be rendered using
|
# original context so variables in slots can be rendered using
|
||||||
# the original context.
|
# the original context.
|
||||||
orig_ctx = context
|
root_ctx = get_root_context(context)
|
||||||
context = context.new()
|
context = context.new()
|
||||||
context.push({OUTER_CONTEXT_CONTEXT_KEY: orig_ctx})
|
set_root_context(context, root_ctx)
|
||||||
|
|
||||||
with context.update(component_context):
|
with context.update(component_context):
|
||||||
rendered_component = component.render(context)
|
rendered_component = component.render(context)
|
||||||
|
@ -390,6 +395,6 @@ class ComponentNode(Node):
|
||||||
|
|
||||||
|
|
||||||
def safe_resolve(context_item: FilterExpression, context: Context) -> Any:
|
def safe_resolve(context_item: FilterExpression, context: Context) -> Any:
|
||||||
"""Resolve FilterExpressions and Variables in context if possible. Return other items unchanged."""
|
"""Resolve FilterExpressions and Variables in context if possible. Return other items unchanged."""
|
||||||
|
|
||||||
return context_item.resolve(context) if hasattr(context_item, "resolve") else context_item
|
return context_item.resolve(context) if hasattr(context_item, "resolve") else context_item
|
||||||
|
|
121
src/django_components/context.py
Normal file
121
src/django_components/context.py
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
"""
|
||||||
|
This file cetralizes various ways we use Django's Context class
|
||||||
|
pass data across components, nodes, slots, and contexts.
|
||||||
|
|
||||||
|
You can think of the Context as our storage system.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
|
from django.template import Context
|
||||||
|
|
||||||
|
from django_components.logger import trace_msg
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from django_components.slots import FillContent
|
||||||
|
|
||||||
|
|
||||||
|
_FILLED_SLOTS_CONTENT_CONTEXT_KEY = "_DJANGO_COMPONENTS_FILLED_SLOTS"
|
||||||
|
_OUTER_CONTEXT_CONTEXT_KEY = "_DJANGO_COMPONENTS_OUTER_CONTEXT"
|
||||||
|
_SLOT_COMPONENT_ASSOC_KEY = "_DJANGO_COMPONENTS_SLOT_COMP_ASSOC"
|
||||||
|
|
||||||
|
|
||||||
|
def get_slot_fill(context: Context, component_id: str, slot_name: str) -> Optional["FillContent"]:
|
||||||
|
"""
|
||||||
|
Use this function to obtain a slot fill from the current context.
|
||||||
|
|
||||||
|
See `set_slot_fill` for more details.
|
||||||
|
"""
|
||||||
|
trace_msg("GET", "FILL", slot_name, component_id)
|
||||||
|
slot_key = (_FILLED_SLOTS_CONTENT_CONTEXT_KEY, component_id, slot_name)
|
||||||
|
return context.get(slot_key, None)
|
||||||
|
|
||||||
|
|
||||||
|
def set_slot_fill(context: Context, component_id: str, slot_name: str, value: "FillContent") -> None:
|
||||||
|
"""
|
||||||
|
Use this function to set a slot fill for the current context.
|
||||||
|
|
||||||
|
Note that we make use of the fact that Django's Context is a stack - we can push and pop
|
||||||
|
extra contexts on top others.
|
||||||
|
|
||||||
|
For the slot fills to be pushed/popped wth stack layer, they need to have keys defined
|
||||||
|
directly on the Context object.
|
||||||
|
"""
|
||||||
|
trace_msg("SET", "FILL", slot_name, component_id)
|
||||||
|
slot_key = (_FILLED_SLOTS_CONTENT_CONTEXT_KEY, component_id, slot_name)
|
||||||
|
context[slot_key] = value
|
||||||
|
|
||||||
|
|
||||||
|
def get_root_context(context: Context) -> Optional[Context]:
|
||||||
|
"""
|
||||||
|
Use this function to get the root context.
|
||||||
|
|
||||||
|
Root context is the top-most context, AKA the context that was passed to
|
||||||
|
the initial `Template.render()`.
|
||||||
|
We pass through the root context to allow configure how slot fills should be rendered.
|
||||||
|
|
||||||
|
See the `SLOT_CONTEXT_BEHAVIOR` setting.
|
||||||
|
"""
|
||||||
|
return context.get(_OUTER_CONTEXT_CONTEXT_KEY)
|
||||||
|
|
||||||
|
|
||||||
|
def set_root_context(context: Context, root_ctx: Context) -> None:
|
||||||
|
"""
|
||||||
|
Use this function to set the root context.
|
||||||
|
|
||||||
|
Root context is the top-most context, AKA the context that was passed to
|
||||||
|
the initial `Template.render()`.
|
||||||
|
We pass through the root context to allow configure how slot fills should be rendered.
|
||||||
|
|
||||||
|
See the `SLOT_CONTEXT_BEHAVIOR` setting.
|
||||||
|
"""
|
||||||
|
context.push({_OUTER_CONTEXT_CONTEXT_KEY: root_ctx})
|
||||||
|
|
||||||
|
|
||||||
|
def capture_root_context(context: Context) -> None:
|
||||||
|
"""
|
||||||
|
Set the root context if it was not set before.
|
||||||
|
|
||||||
|
Root context is the top-most context, AKA the context that was passed to
|
||||||
|
the initial `Template.render()`.
|
||||||
|
We pass through the root context to allow configure how slot fills should be rendered.
|
||||||
|
|
||||||
|
See the `SLOT_CONTEXT_BEHAVIOR` setting.
|
||||||
|
"""
|
||||||
|
root_ctx_already_defined = _OUTER_CONTEXT_CONTEXT_KEY in context
|
||||||
|
if not root_ctx_already_defined:
|
||||||
|
set_root_context(context, context.__copy__())
|
||||||
|
|
||||||
|
|
||||||
|
def set_slot_component_association(context: Context, slot_id: str, component_id: str) -> None:
|
||||||
|
"""
|
||||||
|
Set association between a Slot and a Component in the current context.
|
||||||
|
|
||||||
|
We use SlotNodes to render slot fills. SlotNodes are created only at Template parse time.
|
||||||
|
However, when we are using components with slots in (another) template, we can render
|
||||||
|
the same component multiple time. So we can have multiple FillNodes intended to be used
|
||||||
|
with the same SlotNode.
|
||||||
|
|
||||||
|
So how do we tell the SlotNode which FillNode to render? We do so by tagging the ComponentNode
|
||||||
|
and FillNodes with a unique component_id, which ties them together. And then we tell SlotNode
|
||||||
|
which component_id to use to be able to find the correct Component/Fill.
|
||||||
|
|
||||||
|
We don't want to store this info on the Nodes themselves, as we need to treat them as
|
||||||
|
immutable due to caching of Templates by Django.
|
||||||
|
|
||||||
|
Hence, we use the Context to store the associations of SlotNode <-> Component for
|
||||||
|
the current context stack.
|
||||||
|
"""
|
||||||
|
key = (_SLOT_COMPONENT_ASSOC_KEY, slot_id)
|
||||||
|
context[key] = component_id
|
||||||
|
|
||||||
|
|
||||||
|
def get_slot_component_association(context: Context, slot_id: str) -> str:
|
||||||
|
"""
|
||||||
|
Given a slot ID, get the component ID that this slot is associated with
|
||||||
|
in this context.
|
||||||
|
|
||||||
|
See `set_slot_component_association` for more details.
|
||||||
|
"""
|
||||||
|
key = (_SLOT_COMPONENT_ASSOC_KEY, slot_id)
|
||||||
|
return context[key]
|
38
src/django_components/node.py
Normal file
38
src/django_components/node.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from django.template.base import Node, NodeList, TextNode
|
||||||
|
from django.template.defaulttags import CommentNode
|
||||||
|
|
||||||
|
|
||||||
|
def nodelist_has_content(nodelist: NodeList) -> bool:
|
||||||
|
for node in nodelist:
|
||||||
|
if isinstance(node, TextNode) and node.s.isspace():
|
||||||
|
pass
|
||||||
|
elif isinstance(node, CommentNode):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def walk_nodelist(nodes: NodeList, callback: Callable[[Node], None]) -> None:
|
||||||
|
"""Recursively walk a NodeList, calling `callback` for each Node."""
|
||||||
|
node_queue = [*nodes]
|
||||||
|
while len(node_queue):
|
||||||
|
node: Node = node_queue.pop()
|
||||||
|
callback(node)
|
||||||
|
node_queue.extend(get_node_children(node))
|
||||||
|
|
||||||
|
|
||||||
|
def get_node_children(node: Node) -> NodeList:
|
||||||
|
"""
|
||||||
|
Get child Nodes from Node's nodelist atribute.
|
||||||
|
|
||||||
|
This function is taken from `get_nodes_by_type` method of `django.template.base.Node`.
|
||||||
|
"""
|
||||||
|
nodes = NodeList()
|
||||||
|
for attr in node.child_nodelists:
|
||||||
|
nodelist = getattr(node, attr, [])
|
||||||
|
if nodelist:
|
||||||
|
nodes.extend(nodelist)
|
||||||
|
return nodes
|
|
@ -1,12 +1,7 @@
|
||||||
import difflib
|
import difflib
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
from typing import Dict, List, NamedTuple, Optional, Sequence, Set, Tuple, Type, Union
|
from typing import Dict, List, NamedTuple, Optional, Sequence, Set, Type, Union
|
||||||
|
|
||||||
if sys.version_info[:2] < (3, 9):
|
|
||||||
from typing import ChainMap
|
|
||||||
else:
|
|
||||||
from collections import ChainMap
|
|
||||||
|
|
||||||
if sys.version_info[:2] < (3, 10):
|
if sys.version_info[:2] < (3, 10):
|
||||||
from typing_extensions import TypeAlias
|
from typing_extensions import TypeAlias
|
||||||
|
@ -20,10 +15,12 @@ from django.template.exceptions import TemplateSyntaxError
|
||||||
from django.utils.safestring import SafeString, mark_safe
|
from django.utils.safestring import SafeString, mark_safe
|
||||||
|
|
||||||
from django_components.app_settings import SlotContextBehavior, app_settings
|
from django_components.app_settings import SlotContextBehavior, app_settings
|
||||||
|
from django_components.context import get_root_context, get_slot_fill, set_slot_fill, get_slot_component_association
|
||||||
|
from django_components.node import nodelist_has_content
|
||||||
|
from django_components.logger import trace_msg
|
||||||
|
from django_components.utils import gen_id
|
||||||
|
|
||||||
FILLED_SLOTS_CONTENT_CONTEXT_KEY = "_DJANGO_COMPONENTS_FILLED_SLOTS"
|
|
||||||
DEFAULT_SLOT_KEY = "_DJANGO_COMPONENTS_DEFAULT_SLOT"
|
DEFAULT_SLOT_KEY = "_DJANGO_COMPONENTS_DEFAULT_SLOT"
|
||||||
OUTER_CONTEXT_CONTEXT_KEY = "_DJANGO_COMPONENTS_OUTER_CONTEXT"
|
|
||||||
|
|
||||||
# Type aliases
|
# Type aliases
|
||||||
|
|
||||||
|
@ -38,10 +35,6 @@ class FillContent(NamedTuple):
|
||||||
alias: Optional[AliasName]
|
alias: Optional[AliasName]
|
||||||
|
|
||||||
|
|
||||||
FilledSlotsKey = Tuple[SlotName, Template]
|
|
||||||
FilledSlotsContext = ChainMap[FilledSlotsKey, FillContent]
|
|
||||||
|
|
||||||
|
|
||||||
class UserSlotVar:
|
class UserSlotVar:
|
||||||
"""
|
"""
|
||||||
Extensible mechanism for offering 'fill' blocks in template access to properties
|
Extensible mechanism for offering 'fill' blocks in template access to properties
|
||||||
|
@ -115,16 +108,21 @@ class SlotNode(Node):
|
||||||
return f"<Slot Node: {self.name}. Contents: {repr(self.nodelist)}. Options: {self.active_flags}>"
|
return f"<Slot Node: {self.name}. Contents: {repr(self.nodelist)}. Options: {self.active_flags}>"
|
||||||
|
|
||||||
def render(self, context: Context) -> SafeString:
|
def render(self, context: Context) -> SafeString:
|
||||||
slot_fill_content = get_slot_fill(context, self.component_id, self.name, callee_node_name=f"SlotNode '{self.name}'")
|
component_id = get_slot_component_association(context, self.node_id)
|
||||||
trace_msg("RENDR", "SLOT", self.name, self.node_id, component_id=component_id)
|
trace_msg("RENDR", "SLOT", self.name, self.node_id, component_id=component_id)
|
||||||
|
|
||||||
|
slot_fill_content = get_slot_fill(context, component_id, self.name)
|
||||||
extra_context = {}
|
extra_context = {}
|
||||||
|
|
||||||
|
# Slot fill was NOT found. Will render the default fill
|
||||||
if slot_fill_content is None:
|
if slot_fill_content is None:
|
||||||
if self.is_required:
|
if self.is_required:
|
||||||
raise TemplateSyntaxError(
|
raise TemplateSyntaxError(
|
||||||
f"Slot '{self.name}' is marked as 'required' (i.e. non-optional), " f"yet no fill is provided. "
|
f"Slot '{self.name}' is marked as 'required' (i.e. non-optional), " f"yet no fill is provided. "
|
||||||
)
|
)
|
||||||
nodelist = self.nodelist
|
nodelist = self.nodelist
|
||||||
|
|
||||||
|
# Slot fill WAS found
|
||||||
else:
|
else:
|
||||||
nodelist, alias = slot_fill_content
|
nodelist, alias = slot_fill_content
|
||||||
if alias:
|
if alias:
|
||||||
|
@ -145,7 +143,7 @@ class SlotNode(Node):
|
||||||
|
|
||||||
See SlotContextBehavior for the description of each option.
|
See SlotContextBehavior for the description of each option.
|
||||||
"""
|
"""
|
||||||
root_ctx: Context = context.get(OUTER_CONTEXT_CONTEXT_KEY, Context())
|
root_ctx = get_root_context(context) or Context()
|
||||||
|
|
||||||
if app_settings.SLOT_CONTEXT_BEHAVIOR == SlotContextBehavior.ALLOW_OVERRIDE:
|
if app_settings.SLOT_CONTEXT_BEHAVIOR == SlotContextBehavior.ALLOW_OVERRIDE:
|
||||||
return context
|
return context
|
||||||
|
@ -230,7 +228,7 @@ class IfSlotFilledConditionBranchNode(_IfSlotFilledBranchNode, ComponentIdMixin)
|
||||||
super().__init__(nodelist)
|
super().__init__(nodelist)
|
||||||
|
|
||||||
def evaluate(self, context: Context) -> bool:
|
def evaluate(self, context: Context) -> bool:
|
||||||
slot_fill = get_slot_fill(context, self.component_id, self.slot_name, callee_node_name=type(self).__name__)
|
slot_fill = get_slot_fill(context, self.component_id, self.slot_name)
|
||||||
is_filled = slot_fill is not None
|
is_filled = slot_fill is not None
|
||||||
# Make polarity switchable.
|
# Make polarity switchable.
|
||||||
# i.e. if slot name is NOT filled and is_positive=False,
|
# i.e. if slot name is NOT filled and is_positive=False,
|
||||||
|
@ -267,21 +265,6 @@ class IfSlotFilledNode(Node):
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def get_slot_fill(
|
|
||||||
context: Context,
|
|
||||||
component_id: str,
|
|
||||||
slot_name: str,
|
|
||||||
callee_node_name: str,
|
|
||||||
) -> Optional[FillContent]:
|
|
||||||
try:
|
|
||||||
filled_slots_map: FilledSlotsContext = context[FILLED_SLOTS_CONTENT_CONTEXT_KEY]
|
|
||||||
except KeyError:
|
|
||||||
raise TemplateSyntaxError(f"Attempted to render {callee_node_name} outside a parent component.")
|
|
||||||
|
|
||||||
slot_key = (component_id, slot_name)
|
|
||||||
return filled_slots_map.get(slot_key, None)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_slot_fill_nodes_from_component_nodelist(
|
def parse_slot_fill_nodes_from_component_nodelist(
|
||||||
component_nodelist: NodeList,
|
component_nodelist: NodeList,
|
||||||
ComponentNodeCls: Type[Node],
|
ComponentNodeCls: Type[Node],
|
||||||
|
@ -305,7 +288,7 @@ def parse_slot_fill_nodes_from_component_nodelist(
|
||||||
and `fill "second_fill"`.
|
and `fill "second_fill"`.
|
||||||
"""
|
"""
|
||||||
fill_nodes: Sequence[FillNode] = []
|
fill_nodes: Sequence[FillNode] = []
|
||||||
if _block_has_content(component_nodelist):
|
if nodelist_has_content(component_nodelist):
|
||||||
for parse_fn in (
|
for parse_fn in (
|
||||||
_try_parse_as_default_fill,
|
_try_parse_as_default_fill,
|
||||||
_try_parse_as_named_fill_tag_set,
|
_try_parse_as_named_fill_tag_set,
|
||||||
|
@ -373,17 +356,6 @@ def _try_parse_as_default_fill(
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def _block_has_content(nodelist: NodeList) -> bool:
|
|
||||||
for node in nodelist:
|
|
||||||
if isinstance(node, TextNode) and node.s.isspace():
|
|
||||||
pass
|
|
||||||
elif isinstance(node, CommentNode):
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def render_component_template_with_slots(
|
def render_component_template_with_slots(
|
||||||
component_id: str,
|
component_id: str,
|
||||||
template: Template,
|
template: Template,
|
||||||
|
@ -392,49 +364,29 @@ def render_component_template_with_slots(
|
||||||
registered_name: Optional[str],
|
registered_name: Optional[str],
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Given a template, context, and slot fills, this function first prepares
|
This function first prepares the template to be able to render the fills
|
||||||
the template to be able to render the fills in the place of slots, and then
|
in the place of slots, and then renders the template with given context.
|
||||||
renders the template with given context.
|
|
||||||
|
|
||||||
NOTE: The template is mutated in the process!
|
NOTE: The nodes in the template are mutated in the process!
|
||||||
"""
|
"""
|
||||||
prev_filled_slots_context: Optional[FilledSlotsContext] = context.get(FILLED_SLOTS_CONTENT_CONTEXT_KEY)
|
# ---- Prepare slot fills ----
|
||||||
updated_filled_slots_context = _prepare_component_template_filled_slot_context(
|
|
||||||
component_id,
|
|
||||||
template,
|
|
||||||
fill_content,
|
|
||||||
prev_filled_slots_context,
|
|
||||||
registered_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
with context.update({FILLED_SLOTS_CONTENT_CONTEXT_KEY: updated_filled_slots_context}):
|
|
||||||
return template.render(context)
|
|
||||||
|
|
||||||
|
|
||||||
def _prepare_component_template_filled_slot_context(
|
|
||||||
component_id: str,
|
|
||||||
template: Template,
|
|
||||||
fill_content: Dict[str, FillContent],
|
|
||||||
slots_context: Optional[FilledSlotsContext],
|
|
||||||
registered_name: Optional[str],
|
|
||||||
) -> FilledSlotsContext:
|
|
||||||
slot_name2fill_content = _collect_slot_fills_from_component_template(template, fill_content, registered_name)
|
slot_name2fill_content = _collect_slot_fills_from_component_template(template, fill_content, registered_name)
|
||||||
|
|
||||||
# Give slot nodes knowledge of their parent component.
|
# Give slot nodes knowledge of their parent component.
|
||||||
|
for node in template.nodelist.get_nodes_by_type(IfSlotFilledConditionBranchNode):
|
||||||
|
if isinstance(node, IfSlotFilledConditionBranchNode):
|
||||||
trace_msg("ASSOC", "IFSB", node.slot_name, node.node_id, component_id=component_id)
|
trace_msg("ASSOC", "IFSB", node.slot_name, node.node_id, component_id=component_id)
|
||||||
node.component_id = component_id
|
node.component_id = component_id
|
||||||
|
|
||||||
# Return updated FILLED_SLOTS_CONTEXT map
|
with context.update({}):
|
||||||
filled_slots_map: Dict[FilledSlotsKey, FillContent] = {
|
for slot_name, content_data in slot_name2fill_content.items():
|
||||||
(component_id, slot_name): content_data
|
# Slots whose content is None (i.e. unfilled) are dropped.
|
||||||
for slot_name, content_data in slot_name2fill_content.items()
|
if not content_data:
|
||||||
if content_data # Slots whose content is None (i.e. unfilled) are dropped.
|
continue
|
||||||
}
|
set_slot_fill(context, component_id, slot_name, content_data)
|
||||||
|
|
||||||
if slots_context is not None:
|
# ---- Render ----
|
||||||
return slots_context.new_child(filled_slots_map)
|
return template.render(context)
|
||||||
else:
|
|
||||||
return ChainMap(filled_slots_map)
|
|
||||||
|
|
||||||
|
|
||||||
def _collect_slot_fills_from_component_template(
|
def _collect_slot_fills_from_component_template(
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import glob
|
import glob
|
||||||
import random
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, List, NamedTuple, Optional
|
from typing import List, NamedTuple, Optional
|
||||||
|
|
||||||
from django.template.base import Node, NodeList
|
|
||||||
from django.template.engine import Engine
|
from django.template.engine import Engine
|
||||||
|
|
||||||
from django_components.template_loader import Loader
|
from django_components.template_loader import Loader
|
||||||
|
@ -39,34 +37,15 @@ def search(search_glob: Optional[str] = None, engine: Optional[Engine] = None) -
|
||||||
return SearchResult(searched_dirs=dirs, matched_files=component_filenames)
|
return SearchResult(searched_dirs=dirs, matched_files=component_filenames)
|
||||||
|
|
||||||
|
|
||||||
def walk_nodelist(nodes: NodeList, callback: Callable[[Node], None]) -> None:
|
# Global counter to ensure that all IDs generated by `gen_id` WILL be unique
|
||||||
"""Recursively walk a NodeList, calling `callback` for each Node."""
|
_id = 0
|
||||||
node_queue = [*nodes]
|
|
||||||
while len(node_queue):
|
|
||||||
node: Node = node_queue.pop()
|
|
||||||
callback(node)
|
|
||||||
node_queue.extend(get_node_children(node))
|
|
||||||
|
|
||||||
|
|
||||||
def get_node_children(node: Node) -> NodeList:
|
|
||||||
"""
|
|
||||||
Get child Nodes from Node's nodelist atribute.
|
|
||||||
|
|
||||||
This function is taken from `get_nodes_by_type` method of `django.template.base.Node`.
|
|
||||||
"""
|
|
||||||
nodes = NodeList()
|
|
||||||
for attr in node.child_nodelists:
|
|
||||||
nodelist = getattr(node, attr, [])
|
|
||||||
if nodelist:
|
|
||||||
nodes.extend(nodelist)
|
|
||||||
return nodes
|
|
||||||
|
|
||||||
|
|
||||||
def gen_id(length: int = 5) -> str:
|
def gen_id(length: int = 5) -> str:
|
||||||
# Generate random value
|
"""Generate a unique ID that can be associated with a Node"""
|
||||||
# See https://stackoverflow.com/questions/2782229
|
# Global counter to avoid conflicts
|
||||||
value = random.randrange(16**length)
|
global _id
|
||||||
|
_id += 1
|
||||||
|
|
||||||
# Signed hexadecimal (lowercase).
|
# Pad the ID with `0`s up to 4 digits, e.g. `0007`
|
||||||
# See https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting
|
return f"{_id:04}"
|
||||||
return f"{value:x}"
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue