diff --git a/CHANGELOG.md b/CHANGELOG.md index 06c54f0f54e..bae570de8c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [next] +- fix(parser): match class/id selectors exactly so `.st12` is not lost when `.st1` also exists [#8838](https://github.com/fabricjs/fabric.js/issues/8838) - fix(config): resolve device pixel ratio through env [#11040](https://github.com/fabricjs/fabric.js/pull/11040) - refactor(env): move runtime env setup to packages [#11039](https://github.com/fabricjs/fabric.js/pull/11039) - chore(): Monorepo follow up steps [#11033](https://github.com/fabricjs/fabric.js/pull/11033) diff --git a/src/parser/selectorMatches.spec.ts b/src/parser/selectorMatches.spec.ts new file mode 100644 index 00000000000..ae56e2c77e9 --- /dev/null +++ b/src/parser/selectorMatches.spec.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest'; +import { getFabricWindow } from '../env'; +import { selectorMatches } from './selectorMatches'; + +function getElement(svg: string) { + const parser = new (getFabricWindow().DOMParser)(); + const doc = parser.parseFromString(svg.trim(), 'text/xml'); + return doc.documentElement.getElementsByTagName('*')[0]; +} + +describe('selectorMatches', () => { + it('matches a class selector', () => { + const element = getElement( + '', + ); + expect(selectorMatches(element, '.st1')).toBe(true); + }); + + it('does not treat a class as a prefix of a longer class ending in a digit', () => { + // element only has class `st1`, so it must NOT match the `.st12` selector + const element = getElement( + '', + ); + expect(selectorMatches(element, '.st12')).toBe(false); + }); + + it('matches a class whose name is a numeric-suffixed superset of another class', () => { + // regression for #8838: `.st12` was lost because `.st1` matched inside it + const element = getElement( + '', + ); + expect(selectorMatches(element, '.st12')).toBe(true); + expect(selectorMatches(element, '.st1')).toBe(true); + }); + + it('matches an id selector without treating it as a prefix', () => { + const element = getElement( + '', + ); + expect(selectorMatches(element, '#a1')).toBe(true); + expect(selectorMatches(element, '#a12')).toBe(false); + }); +}); diff --git a/src/parser/selectorMatches.ts b/src/parser/selectorMatches.ts index bc87caa60ee..27cc9c7432c 100644 --- a/src/parser/selectorMatches.ts +++ b/src/parser/selectorMatches.ts @@ -5,7 +5,12 @@ export function selectorMatches( const nodeName = element.nodeName; const classNames = element.getAttribute('class'); const id = element.getAttribute('id'); - const azAz = '(?![a-zA-Z\\-]+)'; + // negative lookahead ensuring the matched id/class name is not merely a + // prefix of a longer name. CSS identifiers can continue with letters, + // digits, hyphens or underscores, so all of them must be excluded here + // (previously digits and underscores were missing, so `.st1` wrongly + // matched inside `.st12`). + const azAz = '(?![\\w\\-])'; let matcher; // i check if a selector matches slicing away part from it. // if i get empty string i should match