Commit b26fe801 authored by David Sveningsson's avatar David Sveningsson

fix(parser): fix conditional comments pushing elements into tree

The lexer would yield tokens for all the content even when inside a comment (unlike a regular non-ie browser).
This was useful to validate the markup inside the condition but would trigger false positives with the content model.

Until branching support in the DOM tree is present the content inside the comment should be ignored.

Fixes #51.
parent cbd77960
Pipeline #81688254 passed with stages
in 6 minutes and 58 seconds
......@@ -3,7 +3,7 @@
exports[`docs/rules/no-conditional-comment.md inline validation: incorrect 1`] = `
Array [
Object {
"errorCount": 2,
"errorCount": 4,
"filePath": "inline",
"messages": Array [
Object {
......@@ -16,21 +16,43 @@ Array [
"severity": 2,
"size": 12,
},
Object {
"column": 1,
"context": undefined,
"line": 3,
"message": "Use of conditional comments are deprecated",
"offset": 53,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 12,
},
Object {
"column": 1,
"context": undefined,
"line": 5,
"message": "Use of conditional comments are deprecated",
"offset": 44,
"offset": 67,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 11,
},
Object {
"column": 1,
"context": undefined,
"line": 7,
"message": "Use of conditional comments are deprecated",
"offset": 123,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 10,
},
],
"source": "<![if IE 6]>
<style>
/* ... */
</style>
"source": "<!--[if IE]>
<p>You are using Internet Explorer.</p>
<![endif]-->
<![if !IE]>
<p>You are not using Internet Explorer.</p>
<![endif]>",
"warningCount": 0,
},
......
import HtmlValidate from "../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["incorrect"] = `<![if IE 6]>
<style>
/* ... */
</style>
markup["incorrect"] = `<!--[if IE]>
<p>You are using Internet Explorer.</p>
<![endif]-->
<![if !IE]>
<p>You are not using Internet Explorer.</p>
<![endif]>`;
describe("docs/rules/no-conditional-comment.md", () => {
......
......@@ -17,9 +17,12 @@ Microsoft Internet Explorer previously supported using special HTML comments
Examples of **incorrect** code for this rule:
<validate name="incorrect" rules="no-conditional-comment">
<![if IE 6]>
<style>
/* ... */
</style>
<!--[if IE]>
<p>You are using Internet Explorer.</p>
<![endif]-->
<![if !IE]>
<p>You are not using Internet Explorer.</p>
<![endif]>
</validate>
......@@ -3,3 +3,110 @@
exports[`regression tests test-files/issues/issue27-disable-block.html 1`] = `Array []`;
exports[`regression tests test-files/issues/issue35-dynamic-values.html 1`] = `Array []`;
exports[`regression tests test-files/issues/issue51-cond-comment.html 1`] = `
Array [
Object {
"errorCount": 8,
"filePath": "test-files/issues/issue51-cond-comment.html",
"messages": Array [
Object {
"column": 1,
"context": undefined,
"line": 2,
"message": "Use of conditional comments are deprecated",
"offset": 16,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 17,
},
Object {
"column": 60,
"context": undefined,
"line": 2,
"message": "Use of conditional comments are deprecated",
"offset": 75,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 12,
},
Object {
"column": 1,
"context": undefined,
"line": 3,
"message": "Use of conditional comments are deprecated",
"offset": 88,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 14,
},
Object {
"column": 60,
"context": undefined,
"line": 3,
"message": "Use of conditional comments are deprecated",
"offset": 147,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 12,
},
Object {
"column": 1,
"context": undefined,
"line": 4,
"message": "Use of conditional comments are deprecated",
"offset": 160,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 14,
},
Object {
"column": 60,
"context": undefined,
"line": 4,
"message": "Use of conditional comments are deprecated",
"offset": 219,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 12,
},
Object {
"column": 1,
"context": undefined,
"line": 5,
"message": "Use of conditional comments are deprecated",
"offset": 232,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 17,
},
Object {
"column": 5,
"context": undefined,
"line": 7,
"message": "Use of conditional comments are deprecated",
"offset": 290,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 12,
},
],
"source": "<!doctype html>
<!--[if lt IE 7]> <html class=\\"no-js ie6 oldie\\" lang=\\"en\\"> <![endif]-->
<!--[if IE 7]> <html class=\\"no-js ie7 oldie\\" lang=\\"en\\"> <![endif]-->
<!--[if IE 8]> <html class=\\"no-js ie8 oldie\\" lang=\\"en\\"> <![endif]-->
<!--[if gt IE 8]><!-->
<html class=\\"no-js\\" lang=\\"en\\">
<!--<![endif]-->
<head>
<title>whatever</title>
</head>
<body>
<p>whatever</p>
</body>
</html>
",
"warningCount": 0,
},
]
`;
......@@ -658,56 +658,19 @@ describe("lexer", () => {
expect(token.next().done).toBeTruthy();
});
describe("browser conditional", () => {
it("downlevel-hidden", () => {
const token = lexer.tokenize(
inlineSource("<!--[if IE 6]>foo<![endif]-->")
);
expect(token.next()).toBeToken({
type: TokenType.CONDITIONAL,
data: ["<!--[if IE 6]>", "if IE 6"],
});
expect(token.next()).toBeToken({ type: TokenType.TEXT, data: ["foo"] });
expect(token.next()).toBeToken({
type: TokenType.CONDITIONAL,
data: ["<![endif]-->", "endif"],
});
expect(token.next()).toBeToken({ type: TokenType.EOF });
expect(token.next().done).toBeTruthy();
});
it("downlevel-reveal", () => {
const token = lexer.tokenize(inlineSource("<![if IE 6]>foo<![endif]>"));
expect(token.next()).toBeToken({
type: TokenType.CONDITIONAL,
data: ["<![if IE 6]>", "if IE 6"],
});
expect(token.next()).toBeToken({ type: TokenType.TEXT, data: ["foo"] });
expect(token.next()).toBeToken({
type: TokenType.CONDITIONAL,
data: ["<![endif]>", "endif"],
});
expect(token.next()).toBeToken({ type: TokenType.EOF });
/* downlevel reveal is a non-standard "tag", handled separately */
it("browser conditional downlevel-reveal", () => {
const token = lexer.tokenize(inlineSource("<![if IE 6]>foo<![endif]>"));
expect(token.next()).toBeToken({
type: TokenType.CONDITIONAL,
data: ["<![if IE 6]>", "if IE 6"],
});
it("nested comment", () => {
const token = lexer.tokenize(
inlineSource("<!--[if IE 6]><!-- foo --><![endif]-->")
);
expect(token.next()).toBeToken({
type: TokenType.CONDITIONAL,
data: ["<!--[if IE 6]>", "if IE 6"],
});
expect(token.next()).toBeToken({
type: TokenType.COMMENT,
data: ["<!-- foo -->", " foo "],
});
expect(token.next()).toBeToken({
type: TokenType.CONDITIONAL,
data: ["<![endif]-->", "endif"],
});
expect(token.next()).toBeToken({ type: TokenType.EOF });
expect(token.next()).toBeToken({ type: TokenType.TEXT, data: ["foo"] });
expect(token.next()).toBeToken({
type: TokenType.CONDITIONAL,
data: ["<![endif]>", "endif"],
});
expect(token.next()).toBeToken({ type: TokenType.EOF });
});
});
});
......@@ -35,7 +35,7 @@ const MATCH_SCRIPT_DATA = /^[^]*?(?=<\/script)/;
const MATCH_SCRIPT_END = /^<(\/)(script)/;
const MATCH_DIRECTIVE = /^<!--\s\[html-validate-(.*?)]\s-->/;
const MATCH_COMMENT = /^<!--([^]*?)-->/;
const MATCH_CONDITIONAL = /^<!(?:--)?\[([^\]]*?)\](?:--)?>/;
const MATCH_CONDITIONAL = /^<!\[([^\]]*?)\]>/;
export class InvalidTokenError extends Error {
public location: Location;
......
import { Location } from "../context";
import { parseConditionalComment } from "./conditional-comment";
function mockLocation(text: string): Location {
return {
filename: "mock",
line: 1,
column: 1,
offset: 0,
size: text.length,
};
}
describe("parseConditionalComment()", () => {
it("should find conditions", () => {
const comment = "<!--[if gt IE 6]><!-->foo<!--<![endif]-->";
const location = mockLocation(comment);
const conditions = Array.from(parseConditionalComment(comment, location));
expect(conditions).toEqual([
{
expression: "if gt IE 6",
location: expect.objectContaining({
line: 1,
column: 1,
offset: 0,
size: 17,
}),
},
{
expression: "endif",
location: expect.objectContaining({
line: 1,
column: 30,
offset: 29,
size: 12,
}),
},
]);
});
});
import { Location, sliceLocation } from "../context";
export interface ConditionalComment {
expression: string;
location: Location;
}
const regexp = /<!(?:--)?\[(.*?)\](?:--)?>/g;
export function* parseConditionalComment(
comment: string,
commentLocation: Location
): IterableIterator<ConditionalComment> {
let match: RegExpExecArray;
// tslint:disable-next-line no-conditional-assignment
while ((match = regexp.exec(comment)) !== null) {
const expression = match[1];
const begin = match.index;
const end = begin + match[0].length;
const location = sliceLocation(commentLocation, begin, end, comment);
yield {
expression,
location,
};
}
}
......@@ -19,6 +19,7 @@ import {
import { Lexer, Token, TokenStream, TokenType } from "../lexer";
import { MetaTable } from "../meta";
import { AttributeData } from "./attribute-data";
import { parseConditionalComment } from "./conditional-comment";
/**
* Parse HTML document into a DOM tree.
......@@ -45,6 +46,7 @@ export class Parser {
* @param source - HTML markup.
* @returns DOM tree representing the HTML markup.
*/
// eslint-disable-next-line complexity
public parseHtml(source: string | Source): DOMTree {
if (typeof source === "string") {
source = {
......@@ -100,6 +102,10 @@ export class Parser {
});
break;
case TokenType.COMMENT:
this.consumeComment(token);
break;
case TokenType.DOCTYPE_OPEN:
this.consumeDoctype(token, tokenStream);
break;
......@@ -421,6 +427,24 @@ export class Parser {
});
}
/**
* Consumes comment token.
*
* Tries to find IE conditional comments and emits conditional token if found.
*/
protected consumeComment(token: Token): void {
const comment = token.data[0];
for (const conditional of parseConditionalComment(
comment,
token.location
)) {
this.trigger("conditional", {
condition: conditional.expression,
location: conditional.location,
});
}
}
/**
* Consumes doctype tokens. Emits doctype event.
*/
......
......@@ -10,7 +10,7 @@ Object {
exports[`rule no-conditional-comment smoketest 1`] = `
Array [
Object {
"errorCount": 4,
"errorCount": 24,
"filePath": "test-files/rules/no-conditional-comment.html",
"messages": Array [
Object {
......@@ -18,17 +18,77 @@ Array [
"context": undefined,
"line": 3,
"message": "Use of conditional comments are deprecated",
"offset": 17,
"offset": 43,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 12,
},
Object {
"column": 52,
"context": undefined,
"line": 3,
"message": "Use of conditional comments are deprecated",
"offset": 94,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 12,
},
Object {
"column": 1,
"context": undefined,
"line": 4,
"message": "Use of conditional comments are deprecated",
"offset": 107,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 11,
},
Object {
"column": 55,
"context": undefined,
"line": 4,
"message": "Use of conditional comments are deprecated",
"offset": 161,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 10,
},
Object {
"column": 1,
"context": undefined,
"line": 6,
"message": "Use of conditional comments are deprecated",
"offset": 173,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 14,
},
Object {
"column": 53,
"context": undefined,
"line": 6,
"message": "Use of conditional comments are deprecated",
"offset": 225,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 12,
},
Object {
"column": 1,
"context": undefined,
"line": 7,
"message": "Use of conditional comments are deprecated",
"offset": 57,
"offset": 238,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 17,
},
Object {
"column": 53,
"context": undefined,
"line": 7,
"message": "Use of conditional comments are deprecated",
"offset": 290,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 12,
......@@ -38,31 +98,179 @@ Array [
"context": undefined,
"line": 9,
"message": "Use of conditional comments are deprecated",
"offset": 71,
"offset": 304,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 12,
"size": 18,
},
Object {
"column": 16,
"column": 56,
"context": undefined,
"line": 9,
"message": "Use of conditional comments are deprecated",
"offset": 86,
"offset": 359,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 12,
},
Object {
"column": 1,
"context": undefined,
"line": 10,
"message": "Use of conditional comments are deprecated",
"offset": 372,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 16,
},
Object {
"column": 57,
"context": undefined,
"line": 10,
"message": "Use of conditional comments are deprecated",
"offset": 428,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 12,
},
Object {
"column": 1,
"context": undefined,
"line": 11,
"message": "Use of conditional comments are deprecated",
"offset": 441,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 32,
},
Object {
"column": 69,
"context": undefined,
"line": 11,
"message": "Use of conditional comments are deprecated",
"offset": 509,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 12,
},
Object {
"column": 1,
"context": undefined,
"line": 12,
"message": "Use of conditional comments are deprecated",
"offset": 522,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 19,
},
Object {
"column": 76,
"context": undefined,
"line": 12,
"message": "Use of conditional comments are deprecated",
"offset": 597,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 12,
},
Object {
"column": 1,
"context": undefined,
"line": 14,
"message": "Use of conditional comments are deprecated",
"offset": 611,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 14,
},
Object {
"column": 57,
"context": undefined,
"line": 14,
"message": "Use of conditional comments are deprecated",
"offset": 667,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 12,
},
Object {
"column": 1,
"context": undefined,
"line": 15,
"message": "Use of conditional comments are deprecated",
"offset": 680,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 13,
},
Object {
"column": 57,
"context": undefined,
"line": 15,
"message": "Use of conditional comments are deprecated",
"offset": 736,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 10,
},
Object {
"column": 1,
"context": undefined,
"line": 17,
"message": "Use of conditional comments are deprecated",
"offset": 748,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 14,
},
Object {
"column": 15,
"context": undefined,
"line": 17,
"message": "Use of conditional comments are deprecated",
"offset": 762,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 12,
},
Object {
"column": 75,
"context": undefined,
"line": 17,
"message": "Use of conditional comments are deprecated",
"offset": 822,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 10,
},
Object {
"column": 85,
"context": undefined,
"line": 17,
"message": "Use of conditional comments are deprecated",
"offset": 832,
"ruleId": "no-conditional-comment",
"severity": 2,
"size": 12,
},
],
"source": "<!-- foobar -->
"source": "<!-- examples from MSDN documentation -->
<!--[if IE]><p>You are using Internet Explorer.</p><![endif]-->
<![if !IE]><p>You are not using Internet Explorer.</p><![endif]>
<!--[if IE 7]><p>Welcome to Internet Explorer 7!</p><![endif]-->
<!--[if !(IE 7)]><p>You are not using version 7.</p><![endif]-->
<!--[if gte IE 7]><p>You are using IE 7 or greater.</p><![endif]-->
<!--[if (IE 5)]><p>You are using IE 5 (any version).</p><![endif]-->
<!--[if (gte IE 5.5)&(lt IE 7)]><p>You are using IE 5.5 or IE 6.</p><![endif]-->
<!--[if lt IE 5.5]><p>Please upgrade your version of Internet Explorer.</p><![endif]-->
<!--[if IE 6]>
foo
<!-- spam -->
bar
<![endif]-->
<!--[if true]>You are using an <em>uplevel</em> browser.<![endif]-->
<![if false]>You are using a <em>downlevel</em> browser.<![endif]>
<![if IE 6]>foo<![endif]>
<!--[if true]><![if IE 7]><p>This nested comment is displayed in IE 7.</p><![endif]><![endif]-->
",
"warningCount": 0,
},
......
......@@ -15,8 +15,8 @@ describe("rule no-conditional-comment", () => {
expect(report).toBeValid();
});
it("should report error when <![...]> is used", () => {
const report = htmlvalidate.validateString("<![if foo]>");
it("should report error when <!--[...]--> is used", () => {
const report = htmlvalidate.validateString("<!--[if foo]-->");
expect(report).toBeInvalid();
expect(report).toHaveError(
"no-conditional-comment",
......@@ -24,17 +24,8 @@ describe("rule no-conditional-comment", () => {
);
});
it("should report error when <!--[...]> is used", () => {
const report = htmlvalidate.validateString("<!--[if foo]>");
expect(report).toBeInvalid();
expect(report).toHaveError(
"no-conditional-comment",
"Use of conditional comments are deprecated"
);
});
it("should report error when <![...]--> is used", () => {
const report = htmlvalidate.validateString("<![endif]-->");
it("should report error when <![...]> is used", () => {
const report = htmlvalidate.validateString("<!-- foo <![if bar]> baz -->");
expect(report).toBeInvalid();
expect(report).toHaveError(
"no-conditional-comment",
......
<!doctype html>
<!--[if lt IE 7]> <html class="no-js ie6 oldie" lang="en"> <![endif]-->
<!--[if IE 7]> <html class="no-js ie7 oldie" lang="en"> <![endif]-->
<!--[if IE 8]> <html class="no-js ie8 oldie" lang="en"> <![endif]-->
<!--[if gt IE 8]><!-->
<html class="no-js" lang="en">
<!--<![endif]-->
<head>
<title>whatever</title>
</head>
<body>
<p>whatever</p>
</body>