Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 44 additions & 35 deletions tdom/placeholders.py
Original file line number Diff line number Diff line change
@@ -1,63 +1,72 @@
from dataclasses import dataclass, field
import random
import re
import string

from .template_utils import TemplateRef


_PLACEHOLDER_PREFIX = f"t🐍{''.join(random.choices(string.ascii_lowercase, k=2))}-"
_PLACEHOLDER_SUFFIX = f"-{''.join(random.choices(string.ascii_lowercase, k=2))}🐍t"
_PLACEHOLDER_PATTERN = re.compile(
re.escape(_PLACEHOLDER_PREFIX) + r"(\d+)" + re.escape(_PLACEHOLDER_SUFFIX)
)
def make_placeholder_config() -> PlaceholderConfig:
prefix = f"t🐍{''.join(random.choices(string.ascii_lowercase, k=2))}-"
suffix = f"-{''.join(random.choices(string.ascii_lowercase, k=2))}🐍t"
return PlaceholderConfig(
prefix=prefix,
suffix=suffix,
pattern=re.compile(re.escape(prefix) + r"(\d+)" + re.escape(suffix)),
)


def make_placeholder(i: int) -> str:
"""Generate a placeholder for the i-th interpolation."""
return f"{_PLACEHOLDER_PREFIX}{i}{_PLACEHOLDER_SUFFIX}"
@dataclass(frozen=True)
class PlaceholderConfig:
"""String operations for working with a placeholder pattern."""

prefix: str
suffix: str
pattern: re.Pattern

def match_placeholders(s: str) -> list[re.Match[str]]:
"""Find all placeholders in a string."""
return list(_PLACEHOLDER_PATTERN.finditer(s))
def make_placeholder(self, i: int) -> str:
"""Generate a placeholder for the i-th interpolation."""
return f"{self.prefix}{i}{self.suffix}"

def match_placeholders(self, s: str) -> list[re.Match[str]]:
"""Find all placeholders in a string."""
return list(self.pattern.finditer(s))

def find_placeholders(s: str) -> TemplateRef:
"""
Find all placeholders in a string and return a TemplateRef.
def find_placeholders(self, s: str) -> TemplateRef:
"""
Find all placeholders in a string and return a TemplateRef.

If no placeholders are found, returns a static TemplateRef.
"""
matches = match_placeholders(s)
if not matches:
return TemplateRef.literal(s)
If no placeholders are found, returns a static TemplateRef.
"""
matches = self.match_placeholders(s)
if not matches:
return TemplateRef.literal(s)

strings: list[str] = []
i_indexes: list[int] = []
last_index = 0
for match in matches:
start, end = match.span()
strings.append(s[last_index:start])
i_indexes.append(int(match[1]))
last_index = end
strings.append(s[last_index:])
strings: list[str] = []
i_indexes: list[int] = []
last_index = 0
for match in matches:
start, end = match.span()
strings.append(s[last_index:start])
i_indexes.append(int(match[1]))
last_index = end
strings.append(s[last_index:])

return TemplateRef(tuple(strings), tuple(i_indexes))
return TemplateRef(tuple(strings), tuple(i_indexes))


@dataclass
class PlaceholderState:
known: set[int]
known: set[int] = field(default_factory=set)
config: PlaceholderConfig = field(default_factory=make_placeholder_config)
"""Collection of currently 'known and active' placeholder indexes."""

def __init__(self):
self.known = set()

@property
def is_empty(self) -> bool:
return len(self.known) == 0

def add_placeholder(self, index: int) -> str:
placeholder = make_placeholder(index)
placeholder = self.config.make_placeholder(index)
self.known.add(index)
return placeholder

Expand All @@ -69,7 +78,7 @@ def remove_placeholders(self, text: str) -> TemplateRef:

If no placeholders are found, returns a static PlaceholderRef.
"""
pt = find_placeholders(text)
pt = self.config.find_placeholders(text)
for index in pt.i_indexes:
if index not in self.known:
raise ValueError(f"Unknown placeholder index {index} found in text.")
Expand Down
36 changes: 18 additions & 18 deletions tdom/placeholders_test.py
Original file line number Diff line number Diff line change
@@ -1,52 +1,52 @@
import pytest

from .placeholders import (
_PLACEHOLDER_PREFIX,
_PLACEHOLDER_SUFFIX,
make_placeholder_config,
PlaceholderState,
find_placeholders,
make_placeholder,
match_placeholders,
)


def test_make_placeholder() -> None:
assert make_placeholder(0) == f"{_PLACEHOLDER_PREFIX}0{_PLACEHOLDER_SUFFIX}"
assert make_placeholder(42) == f"{_PLACEHOLDER_PREFIX}42{_PLACEHOLDER_SUFFIX}"
config = make_placeholder_config()
assert config.make_placeholder(0) == f"{config.prefix}0{config.suffix}"
assert config.make_placeholder(42) == f"{config.prefix}42{config.suffix}"


def test_match_placeholders() -> None:
s = f"Start {_PLACEHOLDER_PREFIX}0{_PLACEHOLDER_SUFFIX} middle {_PLACEHOLDER_PREFIX}1{_PLACEHOLDER_SUFFIX} end"
matches = match_placeholders(s)
config = make_placeholder_config()
s = f"Start {config.prefix}0{config.suffix} middle {config.prefix}1{config.suffix} end"
matches = config.match_placeholders(s)
assert len(matches) == 2
assert matches[0].group(0) == f"{_PLACEHOLDER_PREFIX}0{_PLACEHOLDER_SUFFIX}"
assert matches[0].group(0) == f"{config.prefix}0{config.suffix}"
assert matches[0][1] == "0"
assert matches[1].group(0) == f"{_PLACEHOLDER_PREFIX}1{_PLACEHOLDER_SUFFIX}"
assert matches[1].group(0) == f"{config.prefix}1{config.suffix}"
assert matches[1][1] == "1"


def test_find_placeholders() -> None:
s = f"Hello {_PLACEHOLDER_PREFIX}0{_PLACEHOLDER_SUFFIX}, today is {_PLACEHOLDER_PREFIX}1{_PLACEHOLDER_SUFFIX}."
pt = find_placeholders(s)
config = make_placeholder_config()
s = f"Hello {config.prefix}0{config.suffix}, today is {config.prefix}1{config.suffix}."
pt = config.find_placeholders(s)
assert pt.strings == ("Hello ", ", today is ", ".")
assert pt.i_indexes == (0, 1)

literal_s = "No placeholders here."
literal_pt = find_placeholders(literal_s)
literal_pt = config.find_placeholders(literal_s)
assert literal_pt.strings == (literal_s,)
assert literal_pt.i_indexes == ()


def test_placeholder_state() -> None:
state = PlaceholderState()
config = make_placeholder_config()
state = PlaceholderState(config=config)
assert state.is_empty

p0 = state.add_placeholder(0)
assert p0 == make_placeholder(0)
assert p0 == config.make_placeholder(0)
assert not state.is_empty

p1 = state.add_placeholder(1)
assert p1 == make_placeholder(1)
assert p1 == config.make_placeholder(1)

text = f"Values: {p0}, {p1}"
pt = state.remove_placeholders(text)
Expand All @@ -55,4 +55,4 @@ def test_placeholder_state() -> None:
assert state.is_empty

with pytest.raises(ValueError):
state.remove_placeholders(f"Unknown placeholder: {make_placeholder(2)}")
state.remove_placeholders(f"Unknown placeholder: {config.make_placeholder(2)}")
12 changes: 6 additions & 6 deletions tdom/processor_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from markupsafe import Markup

from .nodes import Comment, DocumentType, Element, Fragment, Node, Text
from .placeholders import _PLACEHOLDER_PREFIX, _PLACEHOLDER_SUFFIX
from .placeholders import make_placeholder_config
from .processor import html

# --------------------------------------------------------------------------
Expand Down Expand Up @@ -597,25 +597,25 @@ def test_interpolated_attribute_value_tricky_multiple_placeholders():


def test_placeholder_collision_avoidance():
config = make_placeholder_config()
# This test is to ensure that our placeholder detection avoids collisions
# even with content that might look like a placeholder.
tricky = "123"
template = Template(
'<div data-tricky="',
_PLACEHOLDER_PREFIX,
config.prefix,
Interpolation(tricky, "tricky"),
_PLACEHOLDER_SUFFIX,
config.suffix,
'"></div>',
)
node = html(template)
assert node == Element(
"div",
attrs={"data-tricky": _PLACEHOLDER_PREFIX + tricky + _PLACEHOLDER_SUFFIX},
attrs={"data-tricky": config.prefix + tricky + config.suffix},
children=[],
)
assert (
str(node)
== f'<div data-tricky="{_PLACEHOLDER_PREFIX}{tricky}{_PLACEHOLDER_SUFFIX}"></div>'
str(node) == f'<div data-tricky="{config.prefix}{tricky}{config.suffix}"></div>'
)


Expand Down