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
12 changes: 12 additions & 0 deletions postcss-selector-parser.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ declare namespace parser {
attribute: string;
operator?: AttributeOperator;
insensitive?: boolean;
sensitive?: boolean;
quoteMark?: QuoteMark;
/** @deprecated Use quoteMark instead. */
quoted?: boolean;
Expand All @@ -404,6 +405,8 @@ declare namespace parser {
operator?: string;
value?: string;
insensitive?: string;
insensitiveFlag?: string;
sensitiveFlag?: string;
spaces?: {
attribute?: Partial<Spaces>;
operator?: Partial<Spaces>;
Expand All @@ -417,6 +420,7 @@ declare namespace parser {
attribute: string;
operator?: AttributeOperator;
insensitive?: boolean;
sensitive?: boolean;
quoteMark: QuoteMark;
quoted?: boolean;
spaces: {
Expand All @@ -435,6 +439,8 @@ declare namespace parser {
/** The value of the attribute with quotes and escapes. */
value?: string;
insensitive?: string;
insensitiveFlag?: string;
sensitiveFlag?: string;
spaces?: {
attribute?: Partial<Spaces>;
operator?: Partial<Spaces>;
Expand All @@ -453,6 +459,12 @@ declare namespace parser {
*/
readonly insensitiveFlag : 'i' | '';

/**
* The explicit case sensitivity flag (Selectors Level 4 `s`) or an empty
* string depending on whether this attribute is explicitly case sensitive.
*/
readonly sensitiveFlag : 's' | '';

/**
* Returns the attribute's value quoted such that it would be legal to use
* in the value of a css file. The original value's quotation setting
Expand Down
37 changes: 37 additions & 0 deletions src/__tests__/attributes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,43 @@ test("non standard modifiers", '[href="foo" y]', (t, tree) => {
t.deepEqual(tree.toString(), '[href="foo" y]');
});

// https://github.com/postcss/postcss-selector-parser/issues/309
test("case sensitive attribute selector", '[href="foo" s]', (t, tree) => {
let attr = tree.atPosition(1, 13);
t.deepEqual(attr.sensitive, true);
t.deepEqual(attr.sensitiveFlag, "s");
t.deepEqual(attr.insensitive, false);
t.deepEqual(attr.insensitiveFlag, "");
// The `s` flag must be modeled explicitly, not mis-filed into raws.insensitiveFlag.
t.is(attr.raws.insensitiveFlag, undefined);
t.deepEqual(tree.toString(), '[href="foo" s]');
});

test("capitalized case sensitive attribute selector", '[href="foo" S]', (t, tree) => {
let attr = tree.atPosition(1, 13);
t.deepEqual(attr.sensitive, true);
t.deepEqual(attr.sensitiveFlag, "s");
t.deepEqual(attr.insensitive, false);
t.is(attr.raws.insensitiveFlag, undefined);
t.deepEqual(attr.raws.sensitiveFlag, "S");
t.deepEqual(tree.toString(), '[href="foo" S]');

// Clearing the flag must erase the original "S" notation from raws.
attr.sensitive = false;
t.is(attr.raws.sensitiveFlag, undefined);
t.deepEqual(tree.toString(), '[href="foo" ]');
});

test("case sensitive attribute selector (unquoted)", "[href=test s]", (t, tree) => {
let attr = tree.nodes[0].nodes[0];
t.deepEqual(attr.value, "test");
t.deepEqual(attr.sensitive, true);

attr.sensitive = false;

t.deepEqual(tree.toString(), "[href=test ]");
});

test("comment after insensitive(non space)", '[href="foo" i/**/]', (t, tree) => {
// https://github.com/postcss/postcss-selector-parser/issues/150
let attr = tree.atPosition(1, 13);
Expand Down
11 changes: 10 additions & 1 deletion src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -319,12 +319,21 @@ export default class Parser {
lastAdded = "value";
} else {
let insensitive = content === "i" || content === "I";
let sensitive = content === "s" || content === "S";
if (
(node.value || node.value === "") &&
(node.quoteMark || spaceAfterMeaningfulToken)
) {
node.insensitive = insensitive;
if (!insensitive || content === "I") {
node.sensitive = sensitive;
if (sensitive) {
// "s" is the canonical case-sensitive flag; store the original
// notation in "raws.sensitiveFlag" only for the "S" variant.
if (content === "S") {
ensureObject(node, "raws");
node.raws.sensitiveFlag = content;
}
} else if (!insensitive || content === "I") {
ensureObject(node, "raws");
node.raws.insensitiveFlag = content;
}
Expand Down
36 changes: 35 additions & 1 deletion src/selectors/attribute.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,10 @@ export default class Attribute extends Namespace {
return this.insensitive ? "i" : "";
}

get sensitiveFlag() {
return this.sensitive ? "s" : "";
}

get value() {
return this._value;
}
Expand Down Expand Up @@ -273,6 +277,31 @@ export default class Attribute extends Namespace {
this._insensitive = insensitive;
}

get sensitive() {
return this._sensitive;
}

/**
* Set the explicit case sensitive flag (Selectors Level 4 `s`).
* If the case sensitive flag changes, the raw (escaped) value at `attr.raws.sensitiveFlag`
* of the attribute is updated accordingly.
*
* @param {true | false} sensitive true if the attribute should match case-sensitively.
*/
set sensitive(sensitive) {
if (!sensitive) {
this._sensitive = false;

// "s" and "S" can be used in "this.raws.sensitiveFlag" to store the original notation.
// When setting `attr.sensitive = false` both should be erased to ensure correct serialization.
if (this.raws && (this.raws.sensitiveFlag === "S" || this.raws.sensitiveFlag === "s")) {
this.raws.sensitiveFlag = undefined;
}
}

this._sensitive = sensitive;
}

/**
* Before 3.0, the value had to be set to an escaped value including any wrapped
* quote marks. In 3.0, the semantics of `Attribute.value` changed so that the value
Expand Down Expand Up @@ -403,8 +432,13 @@ export default class Attribute extends Namespace {
if (this.operator && (this.value || this.value === "")) {
selector.push(this._stringFor("operator"));
selector.push(this._stringFor("value"));
// The case-sensitivity flag occupies a single slot after the value.
// Prefer the explicit case-sensitive flag ("s"/"S") when present,
// otherwise fall back to the case-insensitive flag ("i"/"I") or any
// non-standard flag preserved in `raws.insensitiveFlag`.
let flagProperty = this.sensitive ? "sensitiveFlag" : "insensitiveFlag";
selector.push(
this._stringFor("insensitiveFlag", "insensitive", (attrValue, attrSpaces) => {
this._stringFor(flagProperty, "insensitive", (attrValue, attrSpaces) => {
if (
attrValue.length > 0 &&
!this.quoted &&
Expand Down