Skip to content

Commit

Permalink
fix: identifier parsing for ids, classes, pseudo-classes and pseudo-e…
Browse files Browse the repository at this point in the history
…lements
  • Loading branch information
mdevils committed Nov 21, 2023
1 parent de9a3ed commit d222dfd
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 18 deletions.
44 changes: 31 additions & 13 deletions src/parser.ts
Expand Up @@ -264,7 +264,10 @@ export function createParser(
return result;
}

function parseIdentifier(): string {
function parseIdentifier(): string | null {
if (!isIdentStart(chr)) {
return null;
}
let result = '';
while (pos < l) {
if (isIdent(chr)) {
Expand Down Expand Up @@ -334,23 +337,28 @@ export function createParser(
if (is('|')) {
assert(namespaceEnabled, 'Namespaces are not enabled.');
next();
const name = parseIdentifier();
assert(name, 'Expected attribute name.');
attr = {
type: 'Attribute',
name: parseIdentifier(),
name: name,
namespace: {type: 'NoNamespace'}
};
} else if (is('*')) {
assert(namespaceEnabled, 'Namespaces are not enabled.');
assert(namespaceWildcardEnabled, 'Wildcard namespace is not enabled.');
next();
pass('|');
const name = parseIdentifier();
assert(name, 'Expected attribute name.');
attr = {
type: 'Attribute',
name: parseIdentifier(),
name,
namespace: {type: 'WildcardNamespace'}
};
} else {
const identifier = parseIdentifier();
assert(identifier, 'Expected attribute name.');
attr = {
type: 'Attribute',
name: identifier
Expand All @@ -360,9 +368,11 @@ export function createParser(
next();
if (isIdentStart(chr)) {
assert(namespaceEnabled, 'Namespaces are not enabled.');
const name = parseIdentifier();
assert(name, 'Expected attribute name.');
attr = {
type: 'Attribute',
name: parseIdentifier(),
name,
namespace: {type: 'NamespaceName', name: identifier}
};
} else {
Expand All @@ -389,25 +399,28 @@ export function createParser(
};
} else if (substitutesEnabled && is('$')) {
next();
const name = parseIdentifier();
assert(name, 'Expected substitute name.');
attr.value = {
type: 'Substitution',
name: parseIdentifier()
name
};
assert(attr.value.name, 'Expected substitute name.');
} else {
const value = parseIdentifier();
assert(value, 'Expected attribute value.');
attr.value = {
type: 'String',
value: parseIdentifier()
value
};
assert(attr.value.value, 'Expected attribute value.');
}
skipWhitespace();
if (isEof() && !strict) {
return attr;
}
if (!is(']')) {
attr.caseSensitivityModifier = parseIdentifier();
assert(attr.caseSensitivityModifier, 'Expected end of attribute selector.');
const caseSensitivityModifier = parseIdentifier();
assert(caseSensitivityModifier, 'Expected end of attribute selector.');
attr.caseSensitivityModifier = caseSensitivityModifier;
assert(
attributesCaseSensitivityModifiersEnabled,
'Attribute case sensitivity modifiers are not enabled.'
Expand Down Expand Up @@ -505,11 +518,12 @@ export function createParser(
skipWhitespace();
if (substitutesEnabled && is('$')) {
next();
const name = parseIdentifier();
assert(name, 'Expected substitute name.');
argument = {
type: 'Substitution',
name: parseIdentifier()
name
};
assert(argument.name, 'Expected substitute name.');
} else if (signature.type === 'String') {
argument = {
type: 'String',
Expand Down Expand Up @@ -560,9 +574,11 @@ export function createParser(
return {type: 'WildcardTag'};
} else if (isIdentStart(chr)) {
assert(tagNameEnabled, 'Tag names are not enabled.');
const name = parseIdentifier();
assert(name, 'Expected tag name.');
return {
type: 'TagName',
name: parseIdentifier()
name
};
} else {
return fail('Expected tag name.');
Expand Down Expand Up @@ -595,6 +611,7 @@ export function createParser(
return tagName;
} else if (isIdentStart(chr)) {
const identifier = parseIdentifier();
assert(identifier, 'Expected tag name.');
if (!is('|')) {
assert(tagNameEnabled, 'Tag names are not enabled.');
return {
Expand Down Expand Up @@ -677,6 +694,7 @@ export function createParser(

assert(isDoubleColon || pseudoName, 'Expected pseudo-class name.');
assert(!isDoubleColon || pseudoName, 'Expected pseudo-element name.');
assert(pseudoName, 'Expected pseudo-class name.');
assert(
!isDoubleColon ||
pseudoElementsAcceptUnknown ||
Expand Down
17 changes: 13 additions & 4 deletions test/parser.test.ts
Expand Up @@ -275,6 +275,7 @@ describe('parse()', () => {
});
it('should fail on empty class name', () => {
expect(() => parse('.')).toThrow('Expected class name.');
expect(() => parse('.1')).toThrow('Expected class name.');
});
it('should fail if not enabled', () => {
expect(() => createParser({syntax: {}})('.class')).toThrow('Class names are not enabled.');
Expand Down Expand Up @@ -387,6 +388,7 @@ describe('parse()', () => {
});
it('should fail on empty ID name', () => {
expect(() => parse('#')).toThrow('Expected ID name.');
expect(() => parse('#1')).toThrow('Expected ID name.');
});
it('should fail if not enabled', () => {
expect(() => createParser({syntax: {}})('#id')).toThrow('IDs are not enabled.');
Expand Down Expand Up @@ -548,7 +550,12 @@ describe('parse()', () => {
);
});
it('should fail if attribute name is empty', () => {
expect(() => parse('[=1]')).toThrow('Expected attribute name.');
expect(() => parse('[=a1]')).toThrow('Expected attribute name.');
expect(() => parse('[1=a1]')).toThrow('Expected attribute name.');
});
it('should fail if attribute value is empty', () => {
expect(() => parse('[a=]')).toThrow('Expected attribute value.');
expect(() => parse('[a=1]')).toThrow('Expected attribute value.');
});
it('should fail if substitutions are not enabled', () => {
expect(() => parse('[attr=$value]')).toThrow('Expected attribute value.');
Expand Down Expand Up @@ -834,7 +841,7 @@ describe('parse()', () => {
operators: ['=']
}
}
})('[attr=1 i]')
})('[attr=a1 i]')
).toThrow('Attribute case sensitivity modifiers are not enabled.');
});
it('should fail if case sensitivity modifiers is specified without a comparison operator', () => {
Expand All @@ -857,7 +864,7 @@ describe('parse()', () => {
unknownCaseSensitivityModifiers: 'accept'
}
}
})('[attr=1 i]')
})('[attr=a1 i]')
).toEqual(
ast.selector({
rules: [
Expand All @@ -866,7 +873,7 @@ describe('parse()', () => {
ast.attribute({
name: 'attr',
operator: '=',
value: ast.string({value: '1'}),
value: ast.string({value: 'a1'}),
caseSensitivityModifier: 'i'
})
]
Expand Down Expand Up @@ -1205,6 +1212,7 @@ describe('parse()', () => {
});
it('should fail if name is empty', () => {
expect(() => parse(':')).toThrow('Expected pseudo-class name.');
expect(() => parse(':1')).toThrow('Expected pseudo-class name.');
});
it('should fail if argument was not specified', () => {
expect(() => parse(':lang')).toThrow('Argument is required for pseudo-class "lang".');
Expand Down Expand Up @@ -1388,6 +1396,7 @@ describe('parse()', () => {
});
it('should fail on empty pseudo-element name', () => {
expect(() => parse('::')).toThrow('Expected pseudo-element name.');
expect(() => parse('::1')).toThrow('Expected pseudo-element name.');
});
it('should fail on unknown pseudo elements', () => {
expect(() => createParser({syntax: {pseudoElements: {}}})('::before')).toThrow(
Expand Down
2 changes: 1 addition & 1 deletion test/render.test.ts
Expand Up @@ -83,7 +83,7 @@ const testCases = {
'.\\20wow': '.\\20 wow',
'tag\\n\\\\name\\.\\[': 'tagn\\\\name\\.\\[',
'.cls\\n\\\\name\\.\\[': '.clsn\\\\name\\.\\[',
'[attr\\n\\\\name\\.\\[=1]': '[attrn\\\\name\\.\\[="1"]',
'[attr\\n\\\\name\\.\\[=a1]': '[attrn\\\\name\\.\\[="a1"]',
':pseudo\\n\\\\name\\.\\[\\((123)': ':pseudon\\\\name\\.\\[\\((\\31 23)',
'[attr="val\nval"]': '[attr="val\\nval"]',
'[attr="val\\"val"]': '[attr="val\\"val"]',
Expand Down

0 comments on commit d222dfd

Please sign in to comment.