Skip to content

Commit 25f9ea8

Browse files
committed
feat: attribute selector +case sensitivity support
1 parent 7643c11 commit 25f9ea8

File tree

4 files changed

+121
-21
lines changed

4 files changed

+121
-21
lines changed

float-pigment-css/src/query.rs

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,44 @@ impl StyleNodeClass for (String, Option<NonZeroUsize>) {
100100
}
101101
}
102102

103+
pub enum StyleNodeAttributeCaseSensitivity {
104+
CaseSensitive,
105+
CaseInsensitive,
106+
}
107+
108+
impl StyleNodeAttributeCaseSensitivity {
109+
pub fn eq(&self, a: &str, b: &str) -> bool {
110+
match self {
111+
Self::CaseSensitive => a == b,
112+
Self::CaseInsensitive => a.eq_ignore_ascii_case(b),
113+
}
114+
}
115+
116+
pub fn starts_with(&self, a: &str, b: &str) -> bool {
117+
// FIXME: reduce memory allocation
118+
match self {
119+
Self::CaseSensitive => a.starts_with(b),
120+
Self::CaseInsensitive => a.to_ascii_lowercase().starts_with(&b.to_ascii_lowercase()),
121+
}
122+
}
123+
124+
pub fn ends_with(&self, a: &str, b: &str) -> bool {
125+
// FIXME: reduce memory allocation
126+
match self {
127+
Self::CaseSensitive => a.ends_with(b),
128+
Self::CaseInsensitive => a.to_ascii_lowercase().ends_with(&b.to_ascii_lowercase()),
129+
}
130+
}
131+
132+
pub fn contains(&self, a: &str, b: &str) -> bool {
133+
// FIXME: reduce memory allocation
134+
match self {
135+
Self::CaseSensitive => a.contains(b),
136+
Self::CaseInsensitive => a.to_ascii_lowercase().contains(&b.to_ascii_lowercase()),
137+
}
138+
}
139+
}
140+
103141
pub trait StyleNode {
104142
type Class: StyleNodeClass;
105143
type ClassIter<'a>: Iterator<Item = &'a Self::Class>
@@ -112,7 +150,7 @@ pub trait StyleNode {
112150
fn tag_name(&self) -> &str;
113151
fn id(&self) -> Option<&str>;
114152
fn classes(&self) -> Self::ClassIter<'_>;
115-
fn attribute(&self, name: &str) -> Option<&str>;
153+
fn attribute(&self, name: &str) -> Option<(&str, StyleNodeAttributeCaseSensitivity)>;
116154

117155
fn contain_scope(&self, scope: Option<NonZeroUsize>) -> bool {
118156
scope.is_none()
@@ -198,7 +236,7 @@ impl<'a> StyleNode for StyleQuery<'a> {
198236
self.classes.iter()
199237
}
200238

201-
fn attribute(&self, name: &str) -> Option<&str> {
239+
fn attribute(&self, name: &str) -> Option<(&str, StyleNodeAttributeCaseSensitivity)> {
202240
None
203241
}
204242
}
@@ -234,7 +272,7 @@ impl<'b, 'a: 'b> StyleNode for &'b StyleQuery<'a> {
234272
self.classes.iter()
235273
}
236274

237-
fn attribute(&self, name: &str) -> Option<&str> {
275+
fn attribute(&self, name: &str) -> Option<(&str, StyleNodeAttributeCaseSensitivity)> {
238276
None
239277
}
240278
}

float-pigment-css/src/sheet/selector.rs

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use cssparser::{Parser, ParserInput};
1010
use float_pigment_css_macro::{compatibility_enum_check, compatibility_struct_check};
1111

1212
use crate::parser::{parse_selector, ParseState};
13-
use crate::query::{StyleNode, StyleNodeClass};
13+
use crate::query::{StyleNode, StyleNodeAttributeCaseSensitivity, StyleNodeClass};
1414

1515
#[cfg_attr(debug_assertions, compatibility_enum_check(selector))]
1616
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
@@ -421,21 +421,31 @@ impl Selector {
421421
for attribute in selector_attributes.iter() {
422422
let selector_attr_value =
423423
attribute.value.as_deref().unwrap_or_default();
424-
if let Some(element_attr_value) =
424+
if let Some((element_attr_value, sensitivity)) =
425425
cur_query.attribute(&attribute.name)
426426
{
427+
let sensitivity = match (&attribute.case_insensitive, sensitivity) {
428+
(AttributeFlags::CaseInsensitive, _) | (AttributeFlags::CaseSensitivityDependsOnName, StyleNodeAttributeCaseSensitivity::CaseInsensitive) => {
429+
StyleNodeAttributeCaseSensitivity::CaseInsensitive
430+
}
431+
(AttributeFlags::CaseSensitive, _) | (AttributeFlags::CaseSensitivityDependsOnName, StyleNodeAttributeCaseSensitivity::CaseSensitive) => {
432+
StyleNodeAttributeCaseSensitivity::CaseSensitive
433+
}
434+
};
427435
if !match attribute.operator {
428436
AttributeOperator::Set => true,
429-
AttributeOperator::Exact => {
430-
element_attr_value == selector_attr_value
431-
}
437+
AttributeOperator::Exact => sensitivity
438+
.eq(element_attr_value, selector_attr_value),
432439
AttributeOperator::List => {
433440
if selector_attr_value.is_empty() {
434441
false
435442
} else {
436443
element_attr_value
437444
.split(SELECTOR_WHITESPACE)
438-
.any(|x| x == selector_attr_value)
445+
.any(|x| {
446+
sensitivity
447+
.eq(x, selector_attr_value)
448+
})
439449
}
440450
}
441451
AttributeOperator::Hyphen => {
@@ -448,21 +458,28 @@ impl Selector {
448458
{
449459
element_attr_value == selector_attr_value
450460
} else {
451-
element_attr_value.starts_with(
461+
sensitivity.starts_with(
462+
element_attr_value,
452463
&alloc::format!(
453464
"{}-",
454465
selector_attr_value
455466
),
456467
)
457468
}
458469
}
459-
AttributeOperator::Begin => element_attr_value
460-
.starts_with(selector_attr_value),
461-
AttributeOperator::End => element_attr_value
462-
.ends_with(selector_attr_value),
463-
AttributeOperator::Contain => {
464-
element_attr_value.contains(selector_attr_value)
465-
}
470+
AttributeOperator::Begin => sensitivity
471+
.starts_with(
472+
element_attr_value,
473+
selector_attr_value,
474+
),
475+
AttributeOperator::End => sensitivity.ends_with(
476+
element_attr_value,
477+
selector_attr_value,
478+
),
479+
AttributeOperator::Contain => sensitivity.contains(
480+
element_attr_value,
481+
selector_attr_value,
482+
),
466483
} {
467484
matches = false;
468485
break;

float-pigment-css/tests/selector.rs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,9 +273,33 @@ fn attribute_selector() {
273273
let ss = StyleSheet::from_str(
274274
r#"
275275
a[title] { height: 100px }
276+
a[class~="logo"] { width: 300px; }
277+
a[href="https://example.org"] { min-height: 100px; }
278+
a[href*="example"] { max-height: 101px; }
279+
a[href$=".ORG" i] { min-width: 102px; }
280+
a[href*="https://example.org"] { max-width: 103px; }
281+
a[href*="https://example.ORG" s] { max-width: 104px; }
276282
.b { height: 200px }
277-
a[class~="logo"] { width: 300px }
278283
.c { height: 400px !important; }
284+
285+
button {
286+
display: flex;
287+
}
288+
button[type="primary"] {
289+
color: red;
290+
}
291+
button[size="mini"] {
292+
height: 10px;
293+
}
294+
button[type="primary"][size="mini"] {
295+
height: 11px;
296+
}
297+
#a {
298+
height: 12px;
299+
}
300+
button[type="primary"]#a {
301+
height: 13px;
302+
}
279303
"#,
280304
);
281305
ssg.append(ss);
@@ -292,6 +316,23 @@ fn attribute_selector() {
292316
assert_eq!(node_properties.height(), Length::Px(100.));
293317
let node_properties = query(&ssg, "", "", [], [("title".into(), "".into())]);
294318
assert_eq!(node_properties.height(), Length::Undefined);
319+
320+
let node_properties = query(&ssg, "a", "", [], [("href".into(), "https://example.org".into())]);
321+
assert_eq!(node_properties.height(), Length::Undefined);
322+
assert_eq!(node_properties.width(), Length::Undefined);
323+
assert_eq!(node_properties.min_height(), Length::Px(100.));
324+
assert_eq!(node_properties.max_height(), Length::Px(101.));
325+
assert_eq!(node_properties.min_width(), Length::Px(102.));
326+
assert_eq!(node_properties.max_width(), Length::Px(103.));
327+
328+
let node_properties = query(&ssg, "button", "a", [], [("type".into(), "primary".into())]);
329+
assert_eq!(node_properties.height(), Length::Px(13.));
330+
let node_properties = query(&ssg, "button", "a", [], [("type".into(), "warn".into())]);
331+
assert_eq!(node_properties.height(), Length::Px(12.));
332+
let node_properties = query(&ssg, "button", "", [], [("type".into(), "primary".into()), ("size".into(), "mini".into())]);
333+
assert_eq!(node_properties.height(), Length::Px(11.));
334+
let node_properties = query(&ssg, "button", "", [], [("type".into(), "warm".into()), ("size".into(), "mini".into())]);
335+
assert_eq!(node_properties.height(), Length::Px(10.));
295336
}
296337

297338
#[test]

float-pigment-css/tests/utils/mod.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::num::NonZeroUsize;
33
use float_pigment_css::{
44
length_num::LengthNum, property::*, MediaQueryStatus, StyleQuery, StyleSheet, StyleSheetGroup,
55
};
6-
use float_pigment_css::query::{StyleNode};
6+
use float_pigment_css::query::{StyleNode, StyleNodeAttributeCaseSensitivity};
77

88
pub struct StyleQueryTest<'a> {
99
pub style_scope: Option<NonZeroUsize>,
@@ -46,11 +46,15 @@ impl<'a> StyleNode for StyleQueryTest<'a> {
4646
self.classes.iter()
4747
}
4848

49-
fn attribute(&self, name: &str) -> Option<&str> {
49+
fn attribute(&self, name: &str) -> Option<(&str, StyleNodeAttributeCaseSensitivity)> {
5050
self.attributes
5151
.iter()
5252
.find(|(n, _)| n == name)
53-
.map(|(_, v)| v.as_str())
53+
.map(|(_, v)| (v.as_str(), match name {
54+
"id" | "class" => StyleNodeAttributeCaseSensitivity::CaseSensitive,
55+
"type" | "size" => StyleNodeAttributeCaseSensitivity::CaseInsensitive,
56+
_ => StyleNodeAttributeCaseSensitivity::CaseSensitive,
57+
}))
5458
}
5559
}
5660

0 commit comments

Comments
 (0)