Skip to content

Commit e15ca75

Browse files
authored
Merge pull request #1 from flinst/empty-selector
fix handling of some malformed css
2 parents 2faa328 + 1ee4500 commit e15ca75

File tree

3 files changed

+66
-29
lines changed

3 files changed

+66
-29
lines changed

src/shady-css/parser.ts

Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ class Parser {
8888
} else if (token.is(TokenType.comment)) {
8989
return this.parseComment(tokenizer);
9090

91-
} else if (token.is(TokenType.word)) {
91+
} else if (token.is(TokenType.word) ||
92+
token.is(TokenType.openBrace)) {
9293
return this.parseDeclarationOrRuleset(tokenizer);
9394

9495
} else if (token.is(TokenType.propertyBoundary)) {
@@ -241,13 +242,13 @@ class Parser {
241242
* @param tokenizer A Tokenizer node.
242243
*/
243244
parseDeclarationOrRuleset(tokenizer: Tokenizer): Declaration|Ruleset|null {
244-
let ruleStart = null;
245-
let ruleEnd = null;
246-
let colon = null;
245+
if (!tokenizer.currentToken) {
246+
return null;
247+
}
247248

248-
// This code is not obviously correct. e.g. there's what looks to be a
249-
// null-dereference if the declaration starts with an open brace or
250-
// property boundary.. though that may be impossible.
249+
let ruleStart = tokenizer.currentToken;
250+
let ruleEnd = ruleStart.previous;
251+
let colon = null;
251252

252253
while (tokenizer.currentToken) {
253254
if (tokenizer.currentToken.is(TokenType.whitespace)) {
@@ -266,25 +267,15 @@ class Parser {
266267
if (tokenizer.currentToken.is(TokenType.colon)) {
267268
colon = tokenizer.currentToken;
268269
}
269-
270-
if (ruleStart === null) {
271-
ruleStart = tokenizer.advance();
272-
ruleEnd = ruleStart;
273-
} else {
274-
ruleEnd = tokenizer.advance();
275-
}
270+
ruleEnd = tokenizer.advance();
276271
}
277272
}
278273

279-
if (tokenizer.currentToken === null) {
280-
// terminated early
281-
return null;
282-
}
283-
284274
// A ruleset never contains or ends with a semi-colon.
285-
if (tokenizer.currentToken.is(TokenType.propertyBoundary)) {
275+
if (!tokenizer.currentToken ||
276+
tokenizer.currentToken.is(TokenType.propertyBoundary)) {
286277
const nameRange =
287-
tokenizer.getRange(ruleStart!, colon ? colon.previous : ruleEnd);
278+
tokenizer.getRange(ruleStart, colon ? colon.previous : ruleEnd);
288279
const declarationName =
289280
tokenizer.cssText.slice(nameRange.start, nameRange.end);
290281

@@ -298,12 +289,13 @@ class Parser {
298289
this.nodeFactory.expression(expressionValue, expressionRange);
299290
}
300291

301-
if (tokenizer.currentToken.is(TokenType.semicolon)) {
292+
if (tokenizer.currentToken &&
293+
tokenizer.currentToken.is(TokenType.semicolon)) {
302294
tokenizer.advance();
303295
}
304296

305297
const range = tokenizer.trimRange(tokenizer.getRange(
306-
ruleStart!,
298+
ruleStart,
307299
tokenizer.currentToken && tokenizer.currentToken.previous ||
308300
ruleEnd));
309301

@@ -313,33 +305,34 @@ class Parser {
313305
} else if (colon && colon === ruleEnd) {
314306
const rulelist = this.parseRulelist(tokenizer);
315307

316-
if (tokenizer.currentToken.is(TokenType.semicolon)) {
308+
if (tokenizer.currentToken &&
309+
tokenizer.currentToken.is(TokenType.semicolon)) {
317310
tokenizer.advance();
318311
}
319312

320-
const nameRange = tokenizer.getRange(ruleStart!, ruleEnd.previous);
313+
const nameRange = tokenizer.getRange(ruleStart, ruleEnd.previous);
321314
const declarationName =
322315
tokenizer.cssText.slice(nameRange.start, nameRange.end);
323316

324317
const range = tokenizer.trimRange(tokenizer.getRange(
325-
ruleStart!,
318+
ruleStart,
326319
tokenizer.currentToken && tokenizer.currentToken.previous ||
327320
ruleEnd));
328321

329322
return this.nodeFactory.declaration(
330323
declarationName, rulelist, nameRange, range);
331324
// Otherwise, this is a ruleset:
332325
} else {
333-
const selectorRange = tokenizer.getRange(ruleStart!, ruleEnd);
326+
const selectorRange = tokenizer.getRange(ruleStart, ruleEnd);
334327
const selector =
335328
tokenizer.cssText.slice(selectorRange.start, selectorRange.end);
336329
const rulelist = this.parseRulelist(tokenizer);
337-
const start = ruleStart!.start;
330+
const start = ruleStart.start;
338331
let end;
339332
if (tokenizer.currentToken) {
340333
end = tokenizer.currentToken.previous ?
341334
tokenizer.currentToken.previous.end :
342-
ruleStart!.end;
335+
ruleStart.end;
343336
} else {
344337
// no current token? must have reached the end of input, so go up
345338
// until there

src/test/parser-test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,42 @@ describe('Parser', () => {
147147
nodeFactory.discarded(';')
148148
]));
149149
});
150+
151+
it('can parse empty selectors', () => {
152+
expect(parser.parse('{ empty-a } { empty-b } empty-c { empty-d }'))
153+
.to.containSubset(nodeFactory.stylesheet([
154+
nodeFactory.ruleset(
155+
'', nodeFactory.rulelist([
156+
nodeFactory.declaration('empty-a', undefined)])),
157+
nodeFactory.ruleset(
158+
'', nodeFactory.rulelist([
159+
nodeFactory.declaration('empty-b', undefined)])),
160+
nodeFactory.ruleset(
161+
'empty-c', nodeFactory.rulelist([
162+
nodeFactory.declaration('empty-d', undefined)])),
163+
]));
164+
});
165+
166+
it('can parse unclosed blocks', () => {
167+
expect(parser.parse('uncl-a { uncl-b: uncl-c uncl-d'))
168+
.to.containSubset(nodeFactory.stylesheet([
169+
nodeFactory.ruleset(
170+
'uncl-a', nodeFactory.rulelist([
171+
nodeFactory.declaration(
172+
'uncl-b', nodeFactory.expression('uncl-c uncl-d'))
173+
]))
174+
]));
175+
});
176+
177+
it('can parse unclosed blocks without a colon', () => {
178+
expect(parser.parse('uncol-a { uncol-b'))
179+
.to.containSubset(nodeFactory.stylesheet([
180+
nodeFactory.ruleset(
181+
'uncol-a', nodeFactory.rulelist([
182+
nodeFactory.declaration('uncol-b', undefined)
183+
]))
184+
]));
185+
});
150186
});
151187

152188
describe('when extracting ranges', () => {

src/test/stringifier-test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,14 @@ describe('Stringifier', () => {
107107
expect(cssText).to.be.eql(':root{--qux:vim;--foo:{bar:baz;};}');
108108
});
109109

110+
it('can stringify empty selectors', () => {
111+
const cssText =
112+
stringifier.stringify(parser.parse(
113+
'{ empty-a } { empty-b } empty-c { empty-d }'));
114+
expect(cssText).to.be
115+
.eql('{empty-a;}{empty-b;}empty-c{empty-d;}');
116+
});
117+
110118
describe('with discarded nodes', () => {
111119
it('stringifies to a corrected stylesheet', () => {
112120
const cssText =

0 commit comments

Comments
 (0)