1#![deny(missing_docs)]
6
7use std::fmt::Write;
10
11use super::{
12 color_function::ColorFunction,
13 component::{ColorComponent, ColorComponentType},
14 AbsoluteColor,
15};
16use crate::derives::*;
17use crate::{
18 parser::{Parse, ParserContext},
19 values::{
20 computed::Color as ComputedColor,
21 generics::{calc::CalcUnits, Optional},
22 specified::{angle::NoCalcAngle, calc::Leaf, color::Color as SpecifiedColor},
23 },
24};
25use cssparser::{
26 color::{parse_hash_color, PredefinedColorSpace, OPAQUE},
27 match_ignore_ascii_case, CowRcStr, Parser, Token,
28};
29use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
30
31#[inline]
33pub fn rcs_enabled() -> bool {
34 static_prefs::pref!("layout.css.relative-color-syntax.enabled")
35}
36
37#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, ToShmem)]
39#[repr(C)]
40pub struct ChannelKeyword(u16);
41bitflags! {
42 impl ChannelKeyword: u16 {
43 const ALPHA = 1 << 0;
45 const A = 1 << 1;
47 const B = 1 << 2;
49 const C = 1 << 3;
51 const G = 1 << 4;
53 const H = 1 << 5;
55 const L = 1 << 6;
57 const R = 1 << 7;
59 const S = 1 << 8;
61 const W = 1 << 9;
63 const X = 1 << 10;
65 const Y = 1 << 11;
67 const Z = 1 << 12;
69 }
70}
71
72impl ChannelKeyword {
73 pub fn rgb() -> Self {
76 Self::R | Self::G | Self::B | Self::ALPHA
77 }
78
79 pub fn hsl() -> Self {
82 Self::H | Self::S | Self::L | Self::ALPHA
83 }
84
85 pub fn hwb() -> Self {
88 Self::H | Self::W | Self::B | Self::ALPHA
89 }
90
91 pub fn lab() -> Self {
94 Self::L | Self::A | Self::B | Self::ALPHA
95 }
96
97 pub fn lch() -> Self {
100 Self::L | Self::C | Self::H | Self::ALPHA
101 }
102
103 pub fn xyz() -> Self {
106 Self::X | Self::Y | Self::Z | Self::ALPHA
107 }
108
109 pub fn from_ident(ident: &str) -> Result<Self, ()> {
111 Ok(match_ignore_ascii_case! { ident,
112 "alpha" => Self::ALPHA,
113 "a" => Self::A,
114 "b" => Self::B,
115 "c" => Self::C,
116 "g" => Self::G,
117 "h" => Self::H,
118 "l" => Self::L,
119 "r" => Self::R,
120 "s" => Self::S,
121 "w" => Self::W,
122 "x" => Self::X,
123 "y" => Self::Y,
124 "z" => Self::Z,
125 _ => return Err(())
126 })
127 }
128}
129
130impl Parse for ChannelKeyword {
131 fn parse<'i, 't>(
132 _: &ParserContext,
133 input: &mut Parser<'i, 't>,
134 ) -> Result<Self, ParseError<'i>> {
135 let location = input.current_source_location();
136 let ident = input.expect_ident()?;
137 Self::from_ident(ident.as_ref())
138 .map_err(|()| location.new_unexpected_token_error(Token::Ident(ident.clone())))
139 }
140}
141
142impl ToCss for ChannelKeyword {
143 fn to_css<W>(&self, dest: &mut CssWriter<W>) -> std::fmt::Result
144 where
145 W: std::fmt::Write,
146 {
147 dest.write_str(match *self {
148 Self::ALPHA => "alpha",
149 Self::A => "a",
150 Self::B => "b",
151 Self::C => "c",
152 Self::G => "g",
153 Self::H => "h",
154 Self::L => "l",
155 Self::R => "r",
156 Self::S => "s",
157 Self::W => "w",
158 Self::X => "x",
159 Self::Y => "y",
160 Self::Z => "z",
161 _ => {
162 debug_assert!(
163 false,
164 "tried to serialize unexpected multi-value ChannelKeyword"
165 );
166 ""
167 },
168 })
169 }
170}
171
172#[inline]
178pub fn parse_color_keyword(ident: &str) -> Result<SpecifiedColor, ()> {
179 Ok(match_ignore_ascii_case! { ident,
180 "transparent" => {
181 SpecifiedColor::from_absolute_color(AbsoluteColor::srgb_legacy(0u8, 0u8, 0u8, 0.0))
182 },
183 "currentcolor" => SpecifiedColor::CurrentColor,
184 _ => {
185 let (r, g, b) = cssparser::color::parse_named_color(ident)?;
186 SpecifiedColor::from_absolute_color(AbsoluteColor::srgb_legacy(r, g, b, OPAQUE))
187 },
188 })
189}
190
191pub fn parse_color_with<'i, 't>(
194 context: &ParserContext,
195 input: &mut Parser<'i, 't>,
196) -> Result<SpecifiedColor, ParseError<'i>> {
197 let location = input.current_source_location();
198 let token = input.next()?;
199 match *token {
200 Token::Hash(ref value) | Token::IDHash(ref value) => parse_hash_color(value.as_bytes())
201 .map(|(r, g, b, a)| {
202 SpecifiedColor::from_absolute_color(AbsoluteColor::srgb_legacy(r, g, b, a))
203 }),
204 Token::Ident(ref value) => parse_color_keyword(value),
205 Token::Function(ref name) => {
206 let name = name.clone();
207 return input.parse_nested_block(|arguments| {
208 let color_function = parse_color_function(context, name, arguments)?;
209 if !color_function.has_origin_color() {
210 if let Ok(ComputedColor::Absolute(resolved)) =
211 color_function.to_computed_color(None)
212 {
213 return Ok(SpecifiedColor::from_absolute_color(resolved));
214 }
215 }
216 Ok(SpecifiedColor::ColorFunction(Box::new(color_function)))
218 });
219 },
220 _ => Err(()),
221 }
222 .map_err(|()| location.new_unexpected_token_error(token.clone()))
223}
224
225#[inline]
227fn parse_color_function<'i, 't>(
228 context: &ParserContext,
229 name: CowRcStr<'i>,
230 arguments: &mut Parser<'i, 't>,
231) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
232 let origin_color = parse_origin_color(context, arguments)?;
233 let color = match_ignore_ascii_case! { &name,
234 "rgb" | "rgba" => parse_rgb(context, arguments, origin_color),
235 "hsl" | "hsla" => parse_hsl(context, arguments, origin_color),
236 "hwb" => parse_hwb(context, arguments, origin_color),
237 "lab" => parse_lab_like(context, arguments, origin_color, ColorFunction::Lab),
238 "lch" => parse_lch_like(context, arguments, origin_color, ColorFunction::Lch),
239 "oklab" => parse_lab_like(context, arguments, origin_color, ColorFunction::Oklab),
240 "oklch" => parse_lch_like(context, arguments, origin_color, ColorFunction::Oklch),
241 "color" => parse_color_with_color_space(context, arguments, origin_color),
242 "alpha" if static_prefs::pref!("layout.css.alpha-color-function.enabled") => {
243 parse_relative_alpha(
244 context,
245 arguments,
246 origin_color.ok_or_else(|| arguments.new_custom_error(StyleParseErrorKind::UnspecifiedError))?
247 )
248 },
249 _ => return Err(arguments.new_unexpected_token_error(Token::Ident(name))),
250 }?;
251 arguments.expect_exhausted()?;
252 Ok(color)
253}
254
255fn parse_origin_color<'i, 't>(
257 context: &ParserContext,
258 arguments: &mut Parser<'i, 't>,
259) -> Result<Option<SpecifiedColor>, ParseError<'i>> {
260 if !rcs_enabled() {
261 return Ok(None);
262 }
263
264 if arguments
267 .try_parse(|p| p.expect_ident_matching("from"))
268 .is_err()
269 {
270 return Ok(None);
271 }
272
273 SpecifiedColor::parse(context, arguments).map(Option::Some)
274}
275
276#[inline]
277fn parse_rgb<'i, 't>(
278 context: &ParserContext,
279 arguments: &mut Parser<'i, 't>,
280 origin_color: Option<SpecifiedColor>,
281) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
282 let allowed_channel_keywords = if origin_color.is_some() {
283 ChannelKeyword::rgb()
284 } else {
285 ChannelKeyword::empty()
286 };
287 let maybe_red = parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
288
289 let is_legacy_syntax = origin_color.is_none()
293 && !maybe_red.is_none()
294 && arguments.try_parse(|p| p.expect_comma()).is_ok();
295
296 Ok(if is_legacy_syntax {
297 let (green, blue) = if maybe_red.could_be_percentage() {
298 let green = parse_percentage(context, arguments, false, allowed_channel_keywords)?;
299 arguments.expect_comma()?;
300 let blue = parse_percentage(context, arguments, false, allowed_channel_keywords)?;
301 (green, blue)
302 } else {
303 let green = parse_number(context, arguments, false, allowed_channel_keywords)?;
304 arguments.expect_comma()?;
305 let blue = parse_number(context, arguments, false, allowed_channel_keywords)?;
306 (green, blue)
307 };
308
309 let alpha = parse_legacy_alpha(context, arguments)?;
310
311 ColorFunction::Rgb(origin_color.into(), maybe_red, green, blue, alpha)
312 } else {
313 let green = parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
314 let blue = parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
315
316 let alpha = parse_modern_alpha(context, arguments, allowed_channel_keywords)?;
317
318 ColorFunction::Rgb(origin_color.into(), maybe_red, green, blue, alpha)
319 })
320}
321
322#[inline]
326fn parse_hsl<'i, 't>(
327 context: &ParserContext,
328 arguments: &mut Parser<'i, 't>,
329 origin_color: Option<SpecifiedColor>,
330) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
331 let allowed_channel_keywords = if origin_color.is_some() {
332 ChannelKeyword::hsl()
333 } else {
334 ChannelKeyword::empty()
335 };
336 let hue = parse_number_or_angle(context, arguments, true, allowed_channel_keywords)?;
337
338 let is_legacy_syntax = origin_color.is_none()
341 && !hue.is_none()
342 && arguments.try_parse(|p| p.expect_comma()).is_ok();
343
344 let (saturation, lightness, alpha) = if is_legacy_syntax {
345 let saturation = parse_percentage(context, arguments, false, allowed_channel_keywords)?;
346 arguments.expect_comma()?;
347 let lightness = parse_percentage(context, arguments, false, allowed_channel_keywords)?;
348 let alpha = parse_legacy_alpha(context, arguments)?;
349 (saturation, lightness, alpha)
350 } else {
351 let saturation =
352 parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
353 let lightness =
354 parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
355 let alpha = parse_modern_alpha(context, arguments, allowed_channel_keywords)?;
356 (saturation, lightness, alpha)
357 };
358
359 Ok(ColorFunction::Hsl(
360 origin_color.into(),
361 hue,
362 saturation,
363 lightness,
364 alpha,
365 ))
366}
367
368#[inline]
372fn parse_hwb<'i, 't>(
373 context: &ParserContext,
374 arguments: &mut Parser<'i, 't>,
375 origin_color: Option<SpecifiedColor>,
376) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
377 let allowed_channel_keywords = if origin_color.is_some() {
378 ChannelKeyword::hwb()
379 } else {
380 ChannelKeyword::empty()
381 };
382 let hue = parse_number_or_angle(context, arguments, true, allowed_channel_keywords)?;
383 let whiteness = parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
384 let blackness = parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
385
386 let alpha = parse_modern_alpha(context, arguments, allowed_channel_keywords)?;
387
388 Ok(ColorFunction::Hwb(
389 origin_color.into(),
390 hue,
391 whiteness,
392 blackness,
393 alpha,
394 ))
395}
396
397type IntoLabFn<Output> = fn(
398 origin: Optional<SpecifiedColor>,
399 l: ColorComponent<NumberOrPercentageComponent>,
400 a: ColorComponent<NumberOrPercentageComponent>,
401 b: ColorComponent<NumberOrPercentageComponent>,
402 alpha: ColorComponent<NumberOrPercentageComponent>,
403) -> Output;
404
405#[inline]
406fn parse_lab_like<'i, 't>(
407 context: &ParserContext,
408 arguments: &mut Parser<'i, 't>,
409 origin_color: Option<SpecifiedColor>,
410 into_color: IntoLabFn<ColorFunction<SpecifiedColor>>,
411) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
412 let allowed_channel_keywords = if origin_color.is_some() {
413 ChannelKeyword::lab()
414 } else {
415 ChannelKeyword::empty()
416 };
417 let lightness = parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
418 let a = parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
419 let b = parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
420
421 let alpha = parse_modern_alpha(context, arguments, allowed_channel_keywords)?;
422
423 Ok(into_color(origin_color.into(), lightness, a, b, alpha))
424}
425
426type IntoLchFn<Output> = fn(
427 origin: Optional<SpecifiedColor>,
428 l: ColorComponent<NumberOrPercentageComponent>,
429 a: ColorComponent<NumberOrPercentageComponent>,
430 b: ColorComponent<NumberOrAngleComponent>,
431 alpha: ColorComponent<NumberOrPercentageComponent>,
432) -> Output;
433
434#[inline]
435fn parse_lch_like<'i, 't>(
436 context: &ParserContext,
437 arguments: &mut Parser<'i, 't>,
438 origin_color: Option<SpecifiedColor>,
439 into_color: IntoLchFn<ColorFunction<SpecifiedColor>>,
440) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
441 let allowed_channel_keywords = if origin_color.is_some() {
442 ChannelKeyword::lch()
443 } else {
444 ChannelKeyword::empty()
445 };
446 let lightness = parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
447 let chroma = parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
448 let hue = parse_number_or_angle(context, arguments, true, allowed_channel_keywords)?;
449
450 let alpha = parse_modern_alpha(context, arguments, allowed_channel_keywords)?;
451
452 Ok(into_color(
453 origin_color.into(),
454 lightness,
455 chroma,
456 hue,
457 alpha,
458 ))
459}
460
461#[inline]
463fn parse_color_with_color_space<'i, 't>(
464 context: &ParserContext,
465 arguments: &mut Parser<'i, 't>,
466 origin_color: Option<SpecifiedColor>,
467) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
468 let color_space = PredefinedColorSpace::parse(arguments)?;
469 let allowed_channel_keywords = if origin_color.is_some() {
470 match color_space {
471 PredefinedColorSpace::Srgb
472 | PredefinedColorSpace::SrgbLinear
473 | PredefinedColorSpace::DisplayP3
474 | PredefinedColorSpace::DisplayP3Linear
475 | PredefinedColorSpace::A98Rgb
476 | PredefinedColorSpace::ProphotoRgb
477 | PredefinedColorSpace::Rec2020 => ChannelKeyword::rgb(),
478 PredefinedColorSpace::XyzD50 | PredefinedColorSpace::XyzD65 => ChannelKeyword::xyz(),
479 }
480 } else {
481 ChannelKeyword::empty()
482 };
483
484 let c1 = parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
485 let c2 = parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
486 let c3 = parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)?;
487
488 let alpha = parse_modern_alpha(context, arguments, allowed_channel_keywords)?;
489
490 Ok(ColorFunction::Color(
491 origin_color.into(),
492 c1,
493 c2,
494 c3,
495 alpha,
496 color_space.into(),
497 ))
498}
499
500#[inline]
502fn parse_relative_alpha<'i, 't>(
503 context: &ParserContext,
504 arguments: &mut Parser<'i, 't>,
505 origin_color: SpecifiedColor,
506) -> Result<ColorFunction<SpecifiedColor>, ParseError<'i>> {
507 let alpha = parse_modern_alpha(context, arguments, ChannelKeyword::ALPHA)?;
508 Ok(ColorFunction::Alpha(origin_color.into(), alpha))
509}
510
511#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)]
513#[repr(u8)]
514pub enum NumberOrPercentageComponent {
515 Number(f32),
517 Percentage(f32),
520}
521
522impl NumberOrPercentageComponent {
523 pub fn to_number(&self, percentage_basis: f32) -> f32 {
526 match *self {
527 Self::Number(value) => value,
528 Self::Percentage(unit_value) => unit_value * percentage_basis,
529 }
530 }
531}
532
533impl ColorComponentType for NumberOrPercentageComponent {
534 fn from_value(value: f32) -> Self {
535 Self::Number(value)
536 }
537
538 fn units() -> CalcUnits {
539 CalcUnits::PERCENTAGE
540 }
541
542 fn try_from_token(token: &Token) -> Result<Self, ()> {
543 Ok(match *token {
544 Token::Number { value, .. } => Self::Number(value),
545 Token::Percentage { unit_value, .. } => Self::Percentage(unit_value),
546 _ => {
547 return Err(());
548 },
549 })
550 }
551
552 fn try_from_leaf(leaf: &Leaf) -> Result<Self, ()> {
553 Ok(match *leaf {
554 Leaf::Percentage(p) => Self::Percentage(p.get()),
555 Leaf::Number(n) => Self::Number(n.value()),
556 _ => return Err(()),
557 })
558 }
559}
560
561#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)]
563#[repr(u8)]
564pub enum NumberOrAngleComponent {
565 Number(f32),
567 Angle(f32),
570}
571
572impl NumberOrAngleComponent {
573 pub fn degrees(&self) -> f32 {
576 match *self {
577 Self::Number(value) => value,
578 Self::Angle(degrees) => degrees,
579 }
580 }
581}
582
583impl ColorComponentType for NumberOrAngleComponent {
584 fn from_value(value: f32) -> Self {
585 Self::Number(value)
586 }
587
588 fn units() -> CalcUnits {
589 CalcUnits::ANGLE
590 }
591
592 fn try_from_token(token: &Token) -> Result<Self, ()> {
593 Ok(match *token {
594 Token::Number { value, .. } => Self::Number(value),
595 Token::Dimension {
596 value, ref unit, ..
597 } => {
598 let degrees = NoCalcAngle::parse_dimension(value, unit)?.degrees();
599 NumberOrAngleComponent::Angle(degrees)
600 },
601 _ => {
602 return Err(());
603 },
604 })
605 }
606
607 fn try_from_leaf(leaf: &Leaf) -> Result<Self, ()> {
608 Ok(match *leaf {
609 Leaf::Angle(angle) => Self::Angle(angle.degrees()),
610 Leaf::Number(n) => Self::Number(n.value()),
611 _ => return Err(()),
612 })
613 }
614}
615
616impl ColorComponentType for f32 {
618 fn from_value(value: f32) -> Self {
619 value
620 }
621
622 fn units() -> CalcUnits {
623 CalcUnits::empty()
624 }
625
626 fn try_from_token(token: &Token) -> Result<Self, ()> {
627 if let Token::Number { value, .. } = *token {
628 Ok(value)
629 } else {
630 Err(())
631 }
632 }
633
634 fn try_from_leaf(leaf: &Leaf) -> Result<Self, ()> {
635 if let Leaf::Number(n) = *leaf {
636 Ok(n.value())
637 } else {
638 Err(())
639 }
640 }
641}
642
643fn parse_number_or_angle<'i, 't>(
645 context: &ParserContext,
646 input: &mut Parser<'i, 't>,
647 allow_none: bool,
648 allowed_channel_keywords: ChannelKeyword,
649) -> Result<ColorComponent<NumberOrAngleComponent>, ParseError<'i>> {
650 ColorComponent::parse(context, input, allow_none, allowed_channel_keywords)
651}
652
653fn parse_percentage<'i, 't>(
655 context: &ParserContext,
656 input: &mut Parser<'i, 't>,
657 allow_none: bool,
658 allowed_channel_keywords: ChannelKeyword,
659) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
660 let location = input.current_source_location();
661
662 let value = ColorComponent::<NumberOrPercentageComponent>::parse(
663 context,
664 input,
665 allow_none,
666 allowed_channel_keywords,
667 )?;
668 if !value.could_be_percentage() {
669 return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
670 }
671
672 Ok(value)
673}
674
675fn parse_number<'i, 't>(
677 context: &ParserContext,
678 input: &mut Parser<'i, 't>,
679 allow_none: bool,
680 allowed_channel_keywords: ChannelKeyword,
681) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
682 let location = input.current_source_location();
683
684 let value = ColorComponent::<NumberOrPercentageComponent>::parse(
685 context,
686 input,
687 allow_none,
688 allowed_channel_keywords,
689 )?;
690
691 if !value.could_be_number() {
692 return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
693 }
694
695 Ok(value)
696}
697
698fn parse_number_or_percentage<'i, 't>(
700 context: &ParserContext,
701 input: &mut Parser<'i, 't>,
702 allow_none: bool,
703 allowed_channel_keywords: ChannelKeyword,
704) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
705 ColorComponent::parse(context, input, allow_none, allowed_channel_keywords)
706}
707
708fn parse_legacy_alpha<'i, 't>(
709 context: &ParserContext,
710 arguments: &mut Parser<'i, 't>,
711) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
712 if !arguments.is_exhausted() {
713 arguments.expect_comma()?;
714 parse_number_or_percentage(context, arguments, false, ChannelKeyword::empty())
715 } else {
716 Ok(ColorComponent::AlphaOmitted)
717 }
718}
719
720fn parse_modern_alpha<'i, 't>(
721 context: &ParserContext,
722 arguments: &mut Parser<'i, 't>,
723 allowed_channel_keywords: ChannelKeyword,
724) -> Result<ColorComponent<NumberOrPercentageComponent>, ParseError<'i>> {
725 if !arguments.is_exhausted() {
726 arguments.expect_delim('/')?;
727 parse_number_or_percentage(context, arguments, true, allowed_channel_keywords)
728 } else {
729 Ok(ColorComponent::AlphaOmitted)
730 }
731}
732
733impl ColorComponent<NumberOrPercentageComponent> {
734 fn could_be_number(&self) -> bool {
737 match self {
738 Self::None | Self::AlphaOmitted => true,
739 Self::Value(value) => matches!(value, NumberOrPercentageComponent::Number { .. }),
740 Self::ChannelKeyword(_) => {
741 true
743 },
744 Self::Calc(node) => {
745 if let Ok(unit) = node.unit() {
746 unit.is_empty()
747 } else {
748 false
749 }
750 },
751 }
752 }
753
754 fn could_be_percentage(&self) -> bool {
757 match self {
758 Self::None | Self::AlphaOmitted => true,
759 Self::Value(value) => matches!(value, NumberOrPercentageComponent::Percentage { .. }),
760 Self::ChannelKeyword(_) => {
761 false
763 },
764 Self::Calc(node) => {
765 if let Ok(unit) = node.unit() {
766 unit == CalcUnits::PERCENTAGE
767 } else {
768 false
769 }
770 },
771 }
772 }
773}