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
19 changes: 19 additions & 0 deletions src/__tests__/attributes.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { test, nodeVersionAtLeast, nodeVersionBefore } from "./util/helpers.mjs";
import ava from "./util/runner.mjs";
import parser from "../../dist/index.js";

test("attribute selector", "[href]", (t, tree) => {
t.deepEqual(tree.nodes[0].nodes[0].attribute, "href");
Expand Down Expand Up @@ -556,3 +558,20 @@ testDeprecation("set Attribute#quoteMark", "[data-foo=bar]", (t, tree) => {
attr.quoteMark = '"';
t.deepEqual(attr.toString(), '[data-foo="has space"]');
});

// A trailing `* $ ^ ~ |` immediately before the closing bracket used to look
// ahead past the end of the token stream and throw a raw TypeError. None of
// them is a valid operator without a following `=`, so the token is dropped,
// which matches how `[href=]` already drops an operator that has no value.
// Asserted with `ava` rather than `test` because the output is intentionally
// not a round-trip, and pinning it with `test` would assert the lossy form is
// correct.
for (const trailing of ["*", "$", "^", "~", "|"]) {
ava(`attribute name followed by a trailing ${trailing}`, (t) => {
const tree = parser().astSync(`[href${trailing}]`);
const attr = tree.nodes[0].nodes[0];
t.deepEqual(attr.attribute, "href");
t.deepEqual(attr.operator, undefined);
t.deepEqual(tree.toString(), "[href]");
});
}
31 changes: 31 additions & 0 deletions src/__tests__/exceptions.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,37 @@
"Expected a closing parenthesis.",
);

// An attribute selector whose last token before `]` is one of `* $ ^ ~ |`.
// The `attribute` token loop looked ahead to `next` without checking it
// existed, so these threw a raw TypeError. Asserted by message for the same
// reason as the cases above.
throws(
"namespaced attribute name is an asterisk",
"[ns|*]",
"Expected an attribute.",
);
throws("default namespace with asterisk name", "[|*]", "Expected an attribute.");
throws("any namespace with asterisk name", "[*|*]", "Expected an attribute.");
throws(
"namespaced attribute ending in a dollar",
"[ns|$]",
"Expected an attribute.",
);
throws(
"namespaced attribute ending in a caret",
"[ns|^]",
"Expected an attribute.",
);

// A bracket pair that contains no attribute name at all. `toString` used to
// interpolate the string "undefined" into the output.
throws("universal selector as an attribute", "[ * ]", "Expected an attribute.");
throws(
"asterisk name with trailing space",
"[ns|* ]",
"Expected an attribute.",
);

throws("no opening parenthesis", ")");
throws("no opening parenthesis (2)", ":global.foo)");
throws("no opening parenthesis (3)", "h1:not(h2:not(h3)))");
Expand All @@ -44,10 +75,10 @@
throws("bad syntax (2)", "! .body");

throws("missing backslash for semicolon", ".;");
throws("missing backslash for semicolon (2)", ".\;");

Check warning on line 78 in src/__tests__/exceptions.mjs

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, lts/*)

eslint(no-useless-escape)

Unnecessary escape character ';'

Check warning on line 78 in src/__tests__/exceptions.mjs

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, latest)

eslint(no-useless-escape)

Unnecessary escape character ';'

Check warning on line 78 in src/__tests__/exceptions.mjs

View workflow job for this annotation

GitHub Actions / test (windows-latest, latest)

eslint(no-useless-escape)

Unnecessary escape character ';'

Check warning on line 78 in src/__tests__/exceptions.mjs

View workflow job for this annotation

GitHub Actions / test (windows-latest, lts/*)

eslint(no-useless-escape)

Unnecessary escape character ';'
throws(
"unexpected / foo",
"-Option\/root",

Check warning on line 81 in src/__tests__/exceptions.mjs

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, lts/*)

eslint(no-useless-escape)

Unnecessary escape character '/'

Check warning on line 81 in src/__tests__/exceptions.mjs

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest, latest)

eslint(no-useless-escape)

Unnecessary escape character '/'

Check warning on line 81 in src/__tests__/exceptions.mjs

View workflow job for this annotation

GitHub Actions / test (windows-latest, latest)

eslint(no-useless-escape)

Unnecessary escape character '/'

Check warning on line 81 in src/__tests__/exceptions.mjs

View workflow job for this annotation

GitHub Actions / test (windows-latest, lts/*)

eslint(no-useless-escape)

Unnecessary escape character '/'
"Unexpected '/'. Escaping special characters with \\ may help.",
);
throws(
Expand Down
16 changes: 12 additions & 4 deletions src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ export default class Parser {
}
break;
case tokens.asterisk:
if (next[TOKEN.TYPE] === tokens.equals) {
if (next && next[TOKEN.TYPE] === tokens.equals) {
node.operator = content;
lastAdded = "operator";
} else if (
Expand Down Expand Up @@ -256,22 +256,22 @@ export default class Parser {
}
// Falls through
case tokens.caret:
if (next[TOKEN.TYPE] === tokens.equals) {
if (next && next[TOKEN.TYPE] === tokens.equals) {
node.operator = content;
lastAdded = "operator";
}
spaceAfterMeaningfulToken = false;
break;
case tokens.combinator:
if (content === "~" && next[TOKEN.TYPE] === tokens.equals) {
if (content === "~" && next && next[TOKEN.TYPE] === tokens.equals) {
node.operator = content;
lastAdded = "operator";
}
if (content !== "|") {
spaceAfterMeaningfulToken = false;
break;
}
if (next[TOKEN.TYPE] === tokens.equals) {
if (next && next[TOKEN.TYPE] === tokens.equals) {
node.operator = content;
lastAdded = "operator";
} else if (!node.namespace && !node.attribute) {
Expand Down Expand Up @@ -415,6 +415,14 @@ export default class Parser {
}
pos++;
}
if (!node.attribute) {
// Every token inside the brackets was consumed without one of them
// supplying an attribute name. Stringifying now would interpolate the
// string "undefined" into the output, so report it as a parse error
// instead. Points at the opening bracket, which is where the author
// needs to look.
return this.expected("attribute", startingToken[TOKEN.START_POS]);
}
unescapeProp(node, "attribute");
unescapeProp(node, "namespace");
this.newNode(new Attribute(node));
Expand Down
Loading