diff --git a/tdom/placeholders.py b/tdom/placeholders.py index d5094ca..08f039a 100644 --- a/tdom/placeholders.py +++ b/tdom/placeholders.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass, field import random import re import string @@ -5,59 +6,67 @@ 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 @@ -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.") diff --git a/tdom/placeholders_test.py b/tdom/placeholders_test.py index 1f6b93c..80a7e8b 100644 --- a/tdom/placeholders_test.py +++ b/tdom/placeholders_test.py @@ -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) @@ -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)}") diff --git a/tdom/processor_test.py b/tdom/processor_test.py index 9f2c289..937863e 100644 --- a/tdom/processor_test.py +++ b/tdom/processor_test.py @@ -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 # -------------------------------------------------------------------------- @@ -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( '
', ) 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'
' + str(node) == f'
' )