From 413deab118932d10c6e67050395851146377984a Mon Sep 17 00:00:00 2001 From: Henning Weller <111561130+mundanevision20@users.noreply.github.com> Date: Wed, 14 Jan 2026 21:21:23 +0000 Subject: [PATCH 1/5] Fix bug in bitflags assignment --- soupsieve/css_parser.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/soupsieve/css_parser.py b/soupsieve/css_parser.py index 20292ea..4836867 100644 --- a/soupsieve/css_parser.py +++ b/soupsieve/css_parser.py @@ -1091,16 +1091,20 @@ def parse_selectors( # Some patterns require additional logic, such as default. We try to make these the # last pattern, and append the appropriate flag to that selector which communicates # to the matcher what additional logic is required. + # Preserve any flags that were set during parsing (e.g. :empty, :root) + # by combining them with these special processing flags. Using + # assignment here would overwrite previously-set flags and break + # matching logic that depends on combined bitflags. if is_default: - selectors[-1].flags = ct.SEL_DEFAULT + selectors[-1].flags |= ct.SEL_DEFAULT if is_indeterminate: - selectors[-1].flags = ct.SEL_INDETERMINATE + selectors[-1].flags |= ct.SEL_INDETERMINATE if is_in_range: - selectors[-1].flags = ct.SEL_IN_RANGE + selectors[-1].flags |= ct.SEL_IN_RANGE if is_out_of_range: - selectors[-1].flags = ct.SEL_OUT_OF_RANGE + selectors[-1].flags |= ct.SEL_OUT_OF_RANGE if is_placeholder_shown: - selectors[-1].flags = ct.SEL_PLACEHOLDER_SHOWN + selectors[-1].flags |= ct.SEL_PLACEHOLDER_SHOWN # Return selector list return ct.SelectorList([s.freeze() for s in selectors], is_not, is_html) From c4ebe2802be041891971b4f33c263d94682216fa Mon Sep 17 00:00:00 2001 From: Henning Weller <111561130+mundanevision20@users.noreply.github.com> Date: Wed, 14 Jan 2026 21:22:07 +0000 Subject: [PATCH 2/5] Fix bug in validate_week --- soupsieve/css_match.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/soupsieve/css_match.py b/soupsieve/css_match.py index d24ed61..15e3917 100644 --- a/soupsieve/css_match.py +++ b/soupsieve/css_match.py @@ -447,9 +447,18 @@ def validate_day(year: int, month: int, day: int) -> bool: def validate_week(year: int, week: int) -> bool: """Validate week.""" - max_week = datetime.strptime(f"{12}-{31}-{year}", "%m-%d-%Y").isocalendar()[1] - if max_week == 1: - max_week = 53 + # Validate an ISO week number for `year`. + # + # Per ISO 8601 rules, the last ISO week of a year is the week + # containing Dec 28. Using Dec 28 guarantees we obtain the + # correct ISO week-number for the final week of `year`, even in + # years where Dec 31 falls in ISO week 01 of the following year. + # + # Example: if Dec 31 is a Thursday the year's last ISO week will + # be week 53; if Dec 31 is a Monday and that week is counted as + # week 1 of the next year, Dec 28 still belongs to the final + # week of the current ISO year and yields the correct max week. + max_week = datetime(year, 12, 28).isocalendar()[1] return 1 <= week <= max_week @staticmethod From 38a5012b4ea124326b5878928b13249022878529 Mon Sep 17 00:00:00 2001 From: Henning Weller <111561130+mundanevision20@users.noreply.github.com> Date: Wed, 14 Jan 2026 21:22:33 +0000 Subject: [PATCH 3/5] Enhance namespace validation --- soupsieve/css_types.py | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/soupsieve/css_types.py b/soupsieve/css_types.py index 71a6519..6cbd125 100644 --- a/soupsieve/css_types.py +++ b/soupsieve/css_types.py @@ -156,10 +156,21 @@ def _validate(self, arg: dict[str, str] | Iterable[tuple[str, str]]) -> None: """Validate arguments.""" if isinstance(arg, dict): - if not all(isinstance(v, str) for v in arg.values()): - raise TypeError(f'{self.__class__.__name__} values must be hashable') - elif not all(isinstance(k, str) and isinstance(v, str) for k, v in arg): - raise TypeError(f'{self.__class__.__name__} keys and values must be Unicode strings') + if not all( + isinstance(k, str) and isinstance(v, str) + for k, v in arg.items() + ): + raise TypeError( + f'{self.__class__.__name__} keys and values must be Unicode strings' + ) + elif not all( + isinstance(k, str) and isinstance(v, str) + for k, v in arg + ): + raise TypeError( + f'{self.__class__.__name__} keys and values ' + 'must be Unicode strings' + ) class CustomSelectors(ImmutableDict): @@ -174,10 +185,21 @@ def _validate(self, arg: dict[str, str] | Iterable[tuple[str, str]]) -> None: """Validate arguments.""" if isinstance(arg, dict): - if not all(isinstance(v, str) for v in arg.values()): - raise TypeError(f'{self.__class__.__name__} values must be hashable') - elif not all(isinstance(k, str) and isinstance(v, str) for k, v in arg): - raise TypeError(f'{self.__class__.__name__} keys and values must be Unicode strings') + if not all( + isinstance(k, str) and isinstance(v, str) + for k, v in arg.items() + ): + raise TypeError( + f'{self.__class__.__name__} keys and values must be Unicode strings' + ) + elif not all( + isinstance(k, str) and isinstance(v, str) + for k, v in arg + ): + raise TypeError( + f'{self.__class__.__name__} keys and values ' + 'must be Unicode strings' + ) class Selector(Immutable): From 12a0b803d6d1eac12723dcf7af45a239d55c0136 Mon Sep 17 00:00:00 2001 From: Henning Weller <111561130+mundanevision20@users.noreply.github.com> Date: Wed, 14 Jan 2026 21:23:27 +0000 Subject: [PATCH 4/5] Fix bug to prevent never ending loop --- soupsieve/pretty.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/soupsieve/pretty.py b/soupsieve/pretty.py index 193db05..eb41c93 100644 --- a/soupsieve/pretty.py +++ b/soupsieve/pretty.py @@ -136,4 +136,9 @@ def pretty(obj: Any) -> str: # pragma: no cover output.append(f'{m.group(1)} ') break + # prevent never-ending loop + if m is None: + # Skip character that doesn't match any token + index += 1 + return ''.join(output) From 1876e4351e05ee3183f1b1f16fa64bbd1d9070c8 Mon Sep 17 00:00:00 2001 From: Henning Weller <111561130+mundanevision20@users.noreply.github.com> Date: Wed, 14 Jan 2026 21:24:05 +0000 Subject: [PATCH 5/5] Fix offset in pattern context output --- soupsieve/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/soupsieve/util.py b/soupsieve/util.py index 9b2e64d..0168035 100644 --- a/soupsieve/util.py +++ b/soupsieve/util.py @@ -95,7 +95,7 @@ def get_pattern_context(pattern: str, index: int) -> tuple[str, int, int]: col = index - last + 1 elif last <= index < m.end(0): indent = '--> ' - offset = (-1 if index > m.start(0) else 0) + 3 + offset = (-1 if index > m.start(0) else 0) + 2 col = index - last + 1 else: indent = ' '