feat(selector): support special chars in class names (#150)#162
Conversation
There was a problem hiding this comment.
Pull request overview
This PR extends the selector parsing + definition lookup logic to better support Tailwind-style class names containing special characters (notably : and /) and adds fixtures/tests to cover these cases (including a Unicode class name).
Changes:
- Update
findSelectorword-boundary scanning so/can be part of a class token (e.g.bg-red-500/50). - Update
findDefinitionregex construction to escape selector values correctly and to match compiled CSS escapes like\.md\:flex. - Add Tailwind-oriented HTML/CSS fixtures plus new unit tests for
md:flex,bg-red-500/50,style:sm, andcafé.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| server/src/core/findSelector.ts | Adjusts selector token expansion rules to allow / inside class names. |
| server/src/core/findDefinition.ts | Escapes selector values for regex matching and broadens chained-suffix matching for Tailwind escapes. |
| tests/src/findSelector.test.ts | Adds fixture-driven tests for special-character and Unicode class extraction. |
| tests/src/findDefinition.test.ts | Adds definition-lookup tests for escaped Tailwind-style selectors. |
| tests/fixture/tailwind.html | New HTML fixture containing Tailwind-like and Unicode class names. |
| tests/fixture/tailwind.css | New CSS fixture with escaped selector forms (e.g. .md\\:flex, .bg-red-500\\/50). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // expand selection to this word specifically | ||
| // NOTE: `/` is intentionally not a boundary so Tailwind-style class names | ||
| // like `bg-red-500/50` (and escaped forms like `md\:flex`) are captured. | ||
| while ( | ||
| start > 0 && | ||
| text.charAt(start - 1) !== " " && | ||
| text.charAt(start - 1) !== "'" && | ||
| text.charAt(start - 1) !== '"' && | ||
| text.charAt(start - 1) !== "\n" && | ||
| text.charAt(start - 1) !== "/" && | ||
| text.charAt(start - 1) !== "<" | ||
| ) |
There was a problem hiding this comment.
Fixed in 7e05e2a. The backward scan now treats / as a boundary only when the previous-previous char is < — i.e. only in the </ closing-tag sequence — so </div> resolves to div and aligns with the HTML scanner's EndTag offset, while Tailwind slash-values like bg-red-500/50 keep flowing through. Added a </h1> test case to findSelector.test.ts (run across html/jsx/php fixtures) to lock the behavior down.
| // Suffix matcher: allow chained selectors, including class/id names that | ||
| // contain CSS-escaped chars like `\:` or `\/` (Tailwind). | ||
| selection += | ||
| "(\\[[^\\]]*\\]|:{1,2}[\\w-()]+|\\.[\\w\\\\:/-]+|#[\\w\\\\:/-]+)*\\s*"; |
There was a problem hiding this comment.
Fixed in 7e05e2a. The chained-suffix matcher now uses a Unicode-aware identifier class ([\p{L}\p{N}_\\:/-]) with the u flag on both symbolRegexp and fileRegexp, so chained selectors with non-ASCII names like .foo.café and h1.café resolve correctly. Two ancillary tweaks required by switching to u: (a) escapeSelectorForRegex no longer emits a backslash before : or / (they're not regex meta-chars, and identity escapes are a SyntaxError under u); (b) the pseudo-class char range was reordered from [\w-()] to [\w()-] so \w- isn't read as a range. Added .foo.café and h1.café rules to tailwind.css plus matching tests resolving foo and h1 against those chained rules.
…ctors Address PR #162 review feedback: - findSelector: treat `</` as a backward-scan boundary (only when `/` is preceded by `<`), so `</div>` resolves to tag `div` and aligns with the HTML scanner's EndTag offset. Tailwind slash-values like `bg-red-500/50` keep working because `/` is only a boundary in the closing-tag sequence. - findDefinition: switch the chained-suffix matcher to Unicode property escapes (`\p{L}\p{N}_`) with the `u` flag, so chained class/id names containing non-ASCII chars (e.g. `.foo.café`, `h1.café`) match. Adjust `escapeSelectorForRegex` to emit `:` and `/` unescaped (they are not regex meta-chars, and identity escapes are SyntaxErrors under `u`). Reorder the pseudo-class char range so `\w-` is not interpreted as a range under `u`. Tests: - Add `</h1>` closing-tag case to `findSelector` for html/jsx/php. - Add chained Unicode fixtures (`.foo.café`, `h1.café`) and matching `findDefinition` cases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tailwind-style class names containing `:`, `/`, or non-ASCII characters
(e.g. `md:flex`, `bg-red-500/50`, `café`) were not captured by the
word-boundary scan in findSelector, and compiled CSS using backslash
escapes (`.md\:flex`) was not matched by findDefinition.
- findSelector: drop `/` from the start boundary so slash-value modifiers
are captured (the existing loop already passed through `:` and
non-ASCII chars).
- findDefinition: regex-escape the selector value and accept an optional
backslash before `:` or `/` so source classes like `md:flex` match
compiled symbols like `.md\:flex`. Extend the chained-suffix character
class to include `\\`, `:`, and `/`.
- Add tailwind.{css,html} fixtures and tests covering `md:flex`,
`bg-red-500/50`, `café`, and `style:sm` for both findSelector and
findDefinition.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ctors Address PR #162 review feedback: - findSelector: treat `</` as a backward-scan boundary (only when `/` is preceded by `<`), so `</div>` resolves to tag `div` and aligns with the HTML scanner's EndTag offset. Tailwind slash-values like `bg-red-500/50` keep working because `/` is only a boundary in the closing-tag sequence. - findDefinition: switch the chained-suffix matcher to Unicode property escapes (`\p{L}\p{N}_`) with the `u` flag, so chained class/id names containing non-ASCII chars (e.g. `.foo.café`, `h1.café`) match. Adjust `escapeSelectorForRegex` to emit `:` and `/` unescaped (they are not regex meta-chars, and identity escapes are SyntaxErrors under `u`). Reorder the pseudo-class char range so `\w-` is not interpreted as a range under `u`. Tests: - Add `</h1>` closing-tag case to `findSelector` for html/jsx/php. - Add chained Unicode fixtures (`.foo.café`, `h1.café`) and matching `findDefinition` cases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7e05e2a to
36064fc
Compare
Summary
Closes #150.
Tailwind-style class names containing
:,/, or non-ASCII characters were not handled correctly:findSelectorstopped at/, sobg-red-500/50was cut off mid-word.findDefinitionbuilt a regex from the raw selector value, somd:flexwould not match.md\:flexin compiled CSS (and:was unescaped at the regex level).[\w-], which excluded escaped chars like\:or\/in subsequent class/id components.Changes
server/src/core/findSelector.ts— drop/from the start-of-word boundary set; the existing loop already passed through:and non-ASCII chars.server/src/core/findDefinition.ts— introduceescapeSelectorForRegexthat escapes regex meta-chars and emits\\?:/\\?/so source classes likemd:flexmatch compiled symbols like.md\:flex. Extend the chained-suffix character class to include\\,:, and/.tests/fixture/tailwind.css+tests/fixture/tailwind.html— new fixtures with Tailwind-style classes and a Unicode class name.tests/src/findSelector.test.ts+tests/src/findDefinition.test.ts— 8 new tests coveringmd:flex,bg-red-500/50,café, andstyle:sm(the example from the issue).Test plan
yarn buildpasses with zero TS errors.yarn test— full suite (32 tests, including 8 new ones) passes.yarn lintclean.:hover,::before) still recognized as pseudo, not class.🤖 Generated with Claude Code