|
40 | 40 | from .library import Library
|
41 | 41 | from .smartif import IfParser, Literal
|
42 | 42 |
|
| 43 | +partial_start_tag_re = re.compile(r"\{%\s*(partialdef)\s+([\w-]+)(\s+inline)?\s*%}") |
| 44 | +partial_end_tag_re = re.compile(r"\{%\s*endpartialdef\s*%}") |
| 45 | + |
43 | 46 | register = Library()
|
44 | 47 |
|
45 | 48 |
|
@@ -1564,3 +1567,186 @@ def do_with(parser, token):
|
1564 | 1567 | nodelist = parser.parse(("endwith",))
|
1565 | 1568 | parser.delete_first_token()
|
1566 | 1569 | return WithNode(None, None, nodelist, extra_context=extra_context)
|
| 1570 | + |
| 1571 | + |
| 1572 | +class TemplateProxy: |
| 1573 | + """ |
| 1574 | + A lightweight Template lookalike used for template partials. |
| 1575 | +
|
| 1576 | + Wraps nodelist as partial, in order to bind context. |
| 1577 | + """ |
| 1578 | + |
| 1579 | + def __init__(self, nodelist, origin, name): |
| 1580 | + self.nodelist = nodelist |
| 1581 | + self.origin = origin |
| 1582 | + self.name = name |
| 1583 | + |
| 1584 | + def get_exception_info(self, exception, token): |
| 1585 | + template = self.origin.loader.get_template(self.origin.template_name) |
| 1586 | + return template.get_exception_info(exception, token) |
| 1587 | + |
| 1588 | + def find_partial_source(self, full_source, partial_name): |
| 1589 | + result = "" |
| 1590 | + pos = 0 |
| 1591 | + for m in partial_start_tag_re.finditer(full_source, pos): |
| 1592 | + sspos, sepos = m.span() |
| 1593 | + starter, name, inline = m.groups() |
| 1594 | + endm = partial_end_tag_re.search(full_source, sepos + 1) |
| 1595 | + assert endm, "End tag must be present" |
| 1596 | + espos, eepos = endm.span() |
| 1597 | + if name == partial_name: |
| 1598 | + # Include the full partial definition from opening to closing tag |
| 1599 | + result = full_source[sspos:eepos] |
| 1600 | + break |
| 1601 | + pos = eepos + 1 |
| 1602 | + return result |
| 1603 | + |
| 1604 | + @property |
| 1605 | + def source(self): |
| 1606 | + template = self.origin.loader.get_template(self.origin.template_name) |
| 1607 | + return self.find_partial_source(template.source, self.name) |
| 1608 | + |
| 1609 | + def _render(self, context): |
| 1610 | + return self.nodelist.render(context) |
| 1611 | + |
| 1612 | + def render(self, context): |
| 1613 | + "Display stage -- can be called many times" |
| 1614 | + with context.render_context.push_state(self): |
| 1615 | + if context.template is None: |
| 1616 | + with context.bind_template(self): |
| 1617 | + context.template_name = self.name |
| 1618 | + return self._render(context) |
| 1619 | + else: |
| 1620 | + return self._render(context) |
| 1621 | + |
| 1622 | + |
| 1623 | +class DefinePartialNode(Node): |
| 1624 | + def __init__(self, partial_name, inline, nodelist): |
| 1625 | + self.partial_name = partial_name |
| 1626 | + self.inline = inline |
| 1627 | + self.nodelist = nodelist |
| 1628 | + |
| 1629 | + def render(self, context): |
| 1630 | + """Set content into context and return empty string""" |
| 1631 | + if self.inline: |
| 1632 | + return self.nodelist.render(context) |
| 1633 | + else: |
| 1634 | + return "" |
| 1635 | + |
| 1636 | + |
| 1637 | +class RenderPartialNode(Node): |
| 1638 | + def __init__(self, partial_name, partial_mapping): |
| 1639 | + # Defer lookup of nodelist to runtime. |
| 1640 | + self.partial_name = partial_name |
| 1641 | + self.partial_mapping = partial_mapping |
| 1642 | + |
| 1643 | + def render(self, context): |
| 1644 | + return self.partial_mapping[self.partial_name].render(context) |
| 1645 | + |
| 1646 | + |
| 1647 | +@register.tag(name="partialdef") |
| 1648 | +def partialdef_func(parser, token): |
| 1649 | + """ |
| 1650 | + Declare a partial that can be used later in the template. |
| 1651 | +
|
| 1652 | + Usage:: |
| 1653 | +
|
| 1654 | + {% partialdef partial_name %} |
| 1655 | + Partial content goes here |
| 1656 | + {% endpartialdef %} |
| 1657 | +
|
| 1658 | + Stores the nodelist in the context under the key "partial_contents" and can |
| 1659 | + be retrieved using the {% partial %} tag. |
| 1660 | +
|
| 1661 | + The optional ``inline`` argument will render the contents of the partial |
| 1662 | + where it is defined. |
| 1663 | + """ |
| 1664 | + # Parse the tag |
| 1665 | + tokens = token.split_contents() |
| 1666 | + |
| 1667 | + # check we have the expected number of tokens before trying to assign them |
| 1668 | + # via indexes |
| 1669 | + if len(tokens) not in (2, 3): |
| 1670 | + raise TemplateSyntaxError( |
| 1671 | + "%r tag requires 2-3 arguments" % token.contents.split()[0] |
| 1672 | + ) |
| 1673 | + |
| 1674 | + partial_name = tokens[1] |
| 1675 | + |
| 1676 | + try: |
| 1677 | + inline = tokens[2] |
| 1678 | + except IndexError: |
| 1679 | + # the inline argument is optional, so fallback to not using it |
| 1680 | + inline = False |
| 1681 | + |
| 1682 | + if inline and inline != "inline": |
| 1683 | + warnings.warn( |
| 1684 | + "The 'inline' argument does not have any parameters; " |
| 1685 | + "either use 'inline' or remove it completely.", |
| 1686 | + DeprecationWarning, |
| 1687 | + ) |
| 1688 | + |
| 1689 | + # Parse the content until the end tag |
| 1690 | + acceptable_endpartials = ("endpartialdef", f"endpartialdef {partial_name}") |
| 1691 | + nodelist = parser.parse(acceptable_endpartials) |
| 1692 | + endpartial = parser.next_token() |
| 1693 | + if endpartial.contents not in acceptable_endpartials: |
| 1694 | + parser.invalid_block_tag(endpartial, "endpartialdef", acceptable_endpartials) |
| 1695 | + |
| 1696 | + # Store the partial nodelist in the parser.extra_data attribute, |
| 1697 | + parser.extra_data.setdefault("template-partials", {}) |
| 1698 | + parser.extra_data["template-partials"][partial_name] = TemplateProxy( |
| 1699 | + nodelist, parser.origin, partial_name |
| 1700 | + ) |
| 1701 | + |
| 1702 | + return DefinePartialNode(partial_name, inline, nodelist) |
| 1703 | + |
| 1704 | + |
| 1705 | +class SubDictionaryWrapper: |
| 1706 | + """ |
| 1707 | + Wrap a parent dictionary, allowing deferred access to a sub-dictionary by key. |
| 1708 | + The parser.extra_data storage may not yet be populated when a partial node |
| 1709 | + is defined, so defer access until rendering. |
| 1710 | + """ |
| 1711 | + |
| 1712 | + def __init__(self, parent_dict, lookup_key): |
| 1713 | + self.parent_dict = parent_dict |
| 1714 | + self.lookup_key = lookup_key |
| 1715 | + |
| 1716 | + def __getitem__(self, key): |
| 1717 | + try: |
| 1718 | + partials_content = self.parent_dict[self.lookup_key] |
| 1719 | + except KeyError: |
| 1720 | + raise TemplateSyntaxError( |
| 1721 | + f"No partials are defined. You are trying to access '{key}' partial" |
| 1722 | + ) |
| 1723 | + |
| 1724 | + try: |
| 1725 | + return partials_content[key] |
| 1726 | + except KeyError: |
| 1727 | + raise TemplateSyntaxError( |
| 1728 | + f"You are trying to access an undefined partial '{key}'" |
| 1729 | + ) |
| 1730 | + |
| 1731 | + |
| 1732 | +# Define the partial tag to render the partial content. |
| 1733 | +@register.tag(name="partial") |
| 1734 | +def partial_func(parser, token): |
| 1735 | + """ |
| 1736 | + Render a partial that was previously declared using |
| 1737 | + the {% partialdef %} tag. |
| 1738 | +
|
| 1739 | + Usage:: |
| 1740 | +
|
| 1741 | + {% partial partial_name %} |
| 1742 | + """ |
| 1743 | + # Parse the tag |
| 1744 | + try: |
| 1745 | + tag_name, partial_name = token.split_contents() |
| 1746 | + except ValueError: |
| 1747 | + raise TemplateSyntaxError("%r tag requires a single argument" % tag_name) |
| 1748 | + |
| 1749 | + extra_data = getattr(parser, "extra_data") |
| 1750 | + partial_mapping = SubDictionaryWrapper(extra_data, "template-partials") |
| 1751 | + |
| 1752 | + return RenderPartialNode(partial_name, partial_mapping=partial_mapping) |
0 commit comments