"""Protected phrase extraction, storage, and runtime matching.""" from __future__ import annotations import re from dataclasses import dataclass JSON_OBJECT_RE = re.compile(r"\{.*\}", re.DOTALL) @dataclass(frozen=True, slots=True) class NormalizedToken: """A normalized token plus its source character span.""" text: str start_char: int end_char: int def normalize_text(text: str) -> str: """Normalize text for phrase storage and lookup. Args: text (str): Raw text to normalize. Returns: str: Normalized tokens joined by single spaces. """ return " ".join(token.text for token in tokenize_with_offsets(text)) def tokenize(text: str) -> list[str]: """Normalize and split text into phrase-detection tokens. Args: text (str): Raw text to tokenize. Returns: list[str]: Normalized token strings. """ return [token.text for token in tokenize_with_offsets(text)] def tokenize_with_offsets(text: str) -> list[NormalizedToken]: """Normalize text into tokens while preserving original character offsets. Args: text (str): Raw text to tokenize. Returns: list[NormalizedToken]: Normalized tokens with their source character spans. """ tokens: list[NormalizedToken] = [] current: list[str] = [] start_char: int | None = None for index, char in enumerate(text): normalized = normalize_char(char) if normalized == " ": if current and start_char is not None: tokens.append(NormalizedToken(text="".join(current), start_char=start_char, end_char=index)) current = [] start_char = None continue if start_char is None: start_char = index current.append(normalized) if current and start_char is not None: tokens.append(NormalizedToken(text="".join(current), start_char=start_char, end_char=len(text))) return tokens def normalize_char(char: str) -> str: """Normalize one character into a token character or a separator. Args: char (str): Single source character to normalize. Returns: str: The normalized token character, or a space acting as a separator. """ if char in {"\u2019", "\u2018"}: return "'" if char in {"-", "\u2013", "\u2014"}: return " " lowered = char.lower() if lowered in "abcdefghijklmnopqrstuvwxyz0123456789'": return lowered return " "