Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
43 changes: 43 additions & 0 deletions src/parser/selectorMatches.spec.ts
Original file line number Diff line number Diff line change
@@ -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(
'<svg xmlns="http://www.w3.org/2000/svg"><rect class="st1" /></svg>',
);
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(
'<svg xmlns="http://www.w3.org/2000/svg"><rect class="st1" /></svg>',
);
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(
'<svg xmlns="http://www.w3.org/2000/svg"><rect class="st12 st1" /></svg>',
);
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(
'<svg xmlns="http://www.w3.org/2000/svg"><rect id="a1" /></svg>',
);
expect(selectorMatches(element, '#a1')).toBe(true);
expect(selectorMatches(element, '#a12')).toBe(false);
});
});
7 changes: 6 additions & 1 deletion src/parser/selectorMatches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading