text.rs (37500B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ 4 5 //! Specified types for text properties. 6 7 use crate::derives::*; 8 use crate::parser::{Parse, ParserContext}; 9 use crate::properties::longhands::writing_mode::computed_value::T as SpecifiedWritingMode; 10 use crate::values::computed; 11 use crate::values::computed::text::TextEmphasisStyle as ComputedTextEmphasisStyle; 12 use crate::values::computed::{Context, ToComputedValue}; 13 use crate::values::generics::text::{ 14 GenericHyphenateLimitChars, GenericInitialLetter, GenericTextDecorationInset, 15 GenericTextDecorationLength, GenericTextIndent, 16 }; 17 use crate::values::generics::NumberOrAuto; 18 use crate::values::specified::length::{Length, LengthPercentage}; 19 use crate::values::specified::{AllowQuirks, Integer, Number}; 20 use crate::Zero; 21 use cssparser::Parser; 22 use icu_segmenter::GraphemeClusterSegmenter; 23 use std::fmt::{self, Write}; 24 use style_traits::values::SequenceWriter; 25 use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; 26 use style_traits::{KeywordsCollectFn, SpecifiedValueInfo}; 27 28 /// A specified type for the `initial-letter` property. 29 pub type InitialLetter = GenericInitialLetter<Number, Integer>; 30 31 /// A spacing value used by either the `letter-spacing` or `word-spacing` properties. 32 #[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, ToTyped)] 33 pub enum Spacing { 34 /// `normal` 35 Normal, 36 /// `<value>` 37 Value(LengthPercentage), 38 } 39 40 impl Parse for Spacing { 41 fn parse<'i, 't>( 42 context: &ParserContext, 43 input: &mut Parser<'i, 't>, 44 ) -> Result<Self, ParseError<'i>> { 45 if input 46 .try_parse(|i| i.expect_ident_matching("normal")) 47 .is_ok() 48 { 49 return Ok(Spacing::Normal); 50 } 51 LengthPercentage::parse_quirky(context, input, AllowQuirks::Yes).map(Spacing::Value) 52 } 53 } 54 55 /// A specified value for the `letter-spacing` property. 56 #[derive( 57 Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, ToTyped, 58 )] 59 #[typed_value(derive_fields)] 60 pub struct LetterSpacing(pub Spacing); 61 62 impl ToComputedValue for LetterSpacing { 63 type ComputedValue = computed::LetterSpacing; 64 65 fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { 66 use computed::text::GenericLetterSpacing; 67 match self.0 { 68 Spacing::Normal => GenericLetterSpacing(computed::LengthPercentage::zero()), 69 Spacing::Value(ref v) => GenericLetterSpacing(v.to_computed_value(context)), 70 } 71 } 72 73 fn from_computed_value(computed: &Self::ComputedValue) -> Self { 74 if computed.0.is_zero() { 75 return LetterSpacing(Spacing::Normal); 76 } 77 LetterSpacing(Spacing::Value(ToComputedValue::from_computed_value( 78 &computed.0, 79 ))) 80 } 81 } 82 83 /// A specified value for the `word-spacing` property. 84 #[derive( 85 Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, ToTyped, 86 )] 87 pub struct WordSpacing(pub Spacing); 88 89 impl ToComputedValue for WordSpacing { 90 type ComputedValue = computed::WordSpacing; 91 92 fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { 93 match self.0 { 94 Spacing::Normal => computed::LengthPercentage::zero(), 95 Spacing::Value(ref v) => v.to_computed_value(context), 96 } 97 } 98 99 fn from_computed_value(computed: &Self::ComputedValue) -> Self { 100 WordSpacing(Spacing::Value(ToComputedValue::from_computed_value( 101 computed, 102 ))) 103 } 104 } 105 106 /// A value for the `hyphenate-character` property. 107 #[derive( 108 Clone, 109 Debug, 110 MallocSizeOf, 111 Parse, 112 PartialEq, 113 SpecifiedValueInfo, 114 ToComputedValue, 115 ToCss, 116 ToResolvedValue, 117 ToShmem, 118 ToTyped, 119 )] 120 #[repr(C, u8)] 121 pub enum HyphenateCharacter { 122 /// `auto` 123 Auto, 124 /// `<string>` 125 String(crate::OwnedStr), 126 } 127 128 /// A value for the `hyphenate-limit-chars` property. 129 pub type HyphenateLimitChars = GenericHyphenateLimitChars<Integer>; 130 131 impl Parse for HyphenateLimitChars { 132 fn parse<'i, 't>( 133 context: &ParserContext, 134 input: &mut Parser<'i, 't>, 135 ) -> Result<Self, ParseError<'i>> { 136 type IntegerOrAuto = NumberOrAuto<Integer>; 137 138 let total_word_length = IntegerOrAuto::parse(context, input)?; 139 let pre_hyphen_length = input 140 .try_parse(|i| IntegerOrAuto::parse(context, i)) 141 .unwrap_or(IntegerOrAuto::Auto); 142 let post_hyphen_length = input 143 .try_parse(|i| IntegerOrAuto::parse(context, i)) 144 .unwrap_or(pre_hyphen_length); 145 Ok(Self { 146 total_word_length, 147 pre_hyphen_length, 148 post_hyphen_length, 149 }) 150 } 151 } 152 153 impl Parse for InitialLetter { 154 fn parse<'i, 't>( 155 context: &ParserContext, 156 input: &mut Parser<'i, 't>, 157 ) -> Result<Self, ParseError<'i>> { 158 if input 159 .try_parse(|i| i.expect_ident_matching("normal")) 160 .is_ok() 161 { 162 return Ok(Self::normal()); 163 } 164 let size = Number::parse_at_least_one(context, input)?; 165 let sink = input 166 .try_parse(|i| Integer::parse_positive(context, i)) 167 .unwrap_or_else(|_| crate::Zero::zero()); 168 Ok(Self { size, sink }) 169 } 170 } 171 172 /// A generic value for the `text-overflow` property. 173 #[derive( 174 Clone, 175 Debug, 176 Eq, 177 MallocSizeOf, 178 PartialEq, 179 Parse, 180 SpecifiedValueInfo, 181 ToComputedValue, 182 ToCss, 183 ToResolvedValue, 184 ToShmem, 185 )] 186 #[repr(C, u8)] 187 pub enum TextOverflowSide { 188 /// Clip inline content. 189 Clip, 190 /// Render ellipsis to represent clipped inline content. 191 Ellipsis, 192 /// Render a given string to represent clipped inline content. 193 String(crate::values::AtomString), 194 } 195 196 #[derive( 197 Clone, 198 Debug, 199 Eq, 200 MallocSizeOf, 201 PartialEq, 202 SpecifiedValueInfo, 203 ToComputedValue, 204 ToResolvedValue, 205 ToShmem, 206 ToTyped, 207 )] 208 #[repr(C)] 209 /// text-overflow. 210 /// When the specified value only has one side, that's the "second" 211 /// side, and the sides are logical, so "second" means "end". The 212 /// start side is Clip in that case. 213 /// 214 /// When the specified value has two sides, those are our "first" 215 /// and "second" sides, and they are physical sides ("left" and 216 /// "right"). 217 pub struct TextOverflow { 218 /// First side 219 pub first: TextOverflowSide, 220 /// Second side 221 pub second: TextOverflowSide, 222 /// True if the specified value only has one side. 223 pub sides_are_logical: bool, 224 } 225 226 impl Parse for TextOverflow { 227 fn parse<'i, 't>( 228 context: &ParserContext, 229 input: &mut Parser<'i, 't>, 230 ) -> Result<TextOverflow, ParseError<'i>> { 231 let first = TextOverflowSide::parse(context, input)?; 232 Ok( 233 if let Ok(second) = input.try_parse(|input| TextOverflowSide::parse(context, input)) { 234 Self { 235 first, 236 second, 237 sides_are_logical: false, 238 } 239 } else { 240 Self { 241 first: TextOverflowSide::Clip, 242 second: first, 243 sides_are_logical: true, 244 } 245 }, 246 ) 247 } 248 } 249 250 impl TextOverflow { 251 /// Returns the initial `text-overflow` value 252 pub fn get_initial_value() -> TextOverflow { 253 TextOverflow { 254 first: TextOverflowSide::Clip, 255 second: TextOverflowSide::Clip, 256 sides_are_logical: true, 257 } 258 } 259 } 260 261 impl ToCss for TextOverflow { 262 fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result 263 where 264 W: Write, 265 { 266 if self.sides_are_logical { 267 debug_assert_eq!(self.first, TextOverflowSide::Clip); 268 self.second.to_css(dest)?; 269 } else { 270 self.first.to_css(dest)?; 271 dest.write_char(' ')?; 272 self.second.to_css(dest)?; 273 } 274 Ok(()) 275 } 276 } 277 278 #[derive( 279 Clone, 280 Copy, 281 Debug, 282 Eq, 283 MallocSizeOf, 284 PartialEq, 285 Parse, 286 Serialize, 287 SpecifiedValueInfo, 288 ToCss, 289 ToComputedValue, 290 ToResolvedValue, 291 ToShmem, 292 ToTyped, 293 )] 294 #[cfg_attr( 295 feature = "gecko", 296 css(bitflags( 297 single = "none,spelling-error,grammar-error", 298 mixed = "underline,overline,line-through,blink", 299 )) 300 )] 301 #[cfg_attr( 302 not(feature = "gecko"), 303 css(bitflags(single = "none", mixed = "underline,overline,line-through,blink",)) 304 )] 305 #[repr(C)] 306 /// Specified keyword values for the text-decoration-line property. 307 pub struct TextDecorationLine(u8); 308 bitflags! { 309 impl TextDecorationLine: u8 { 310 /// No text decoration line is specified. 311 const NONE = 0; 312 /// underline 313 const UNDERLINE = 1 << 0; 314 /// overline 315 const OVERLINE = 1 << 1; 316 /// line-through 317 const LINE_THROUGH = 1 << 2; 318 /// blink 319 const BLINK = 1 << 3; 320 /// spelling-error 321 const SPELLING_ERROR = 1 << 4; 322 /// grammar-error 323 const GRAMMAR_ERROR = 1 << 5; 324 /// Only set by presentation attributes 325 /// 326 /// Setting this will mean that text-decorations use the color 327 /// specified by `color` in quirks mode. 328 /// 329 /// For example, this gives <a href=foo><font color="red">text</font></a> 330 /// a red text decoration 331 #[cfg(feature = "gecko")] 332 const COLOR_OVERRIDE = 1 << 7; 333 } 334 } 335 336 impl Default for TextDecorationLine { 337 fn default() -> Self { 338 TextDecorationLine::NONE 339 } 340 } 341 342 impl TextDecorationLine { 343 #[inline] 344 /// Returns the initial value of text-decoration-line 345 pub fn none() -> Self { 346 TextDecorationLine::NONE 347 } 348 } 349 350 #[derive( 351 Clone, 352 Copy, 353 Debug, 354 Eq, 355 MallocSizeOf, 356 PartialEq, 357 SpecifiedValueInfo, 358 ToComputedValue, 359 ToCss, 360 ToResolvedValue, 361 ToShmem, 362 )] 363 #[repr(C)] 364 /// Specified keyword values for case transforms in the text-transform property. (These are exclusive.) 365 pub enum TextTransformCase { 366 /// No case transform. 367 None, 368 /// All uppercase. 369 Uppercase, 370 /// All lowercase. 371 Lowercase, 372 /// Capitalize each word. 373 Capitalize, 374 /// Automatic italicization of math variables. 375 #[cfg(feature = "gecko")] 376 MathAuto, 377 } 378 379 #[derive( 380 Clone, 381 Copy, 382 Debug, 383 Eq, 384 MallocSizeOf, 385 PartialEq, 386 Parse, 387 Serialize, 388 SpecifiedValueInfo, 389 ToCss, 390 ToComputedValue, 391 ToResolvedValue, 392 ToShmem, 393 ToTyped, 394 )] 395 #[cfg_attr( 396 feature = "gecko", 397 css(bitflags( 398 single = "none,math-auto", 399 mixed = "uppercase,lowercase,capitalize,full-width,full-size-kana", 400 validate_mixed = "Self::validate_mixed_flags", 401 )) 402 )] 403 #[cfg_attr( 404 not(feature = "gecko"), 405 css(bitflags( 406 single = "none", 407 mixed = "uppercase,lowercase,capitalize,full-width,full-size-kana", 408 validate_mixed = "Self::validate_mixed_flags", 409 )) 410 )] 411 #[repr(C)] 412 /// Specified value for the text-transform property. 413 /// (The spec grammar gives 414 /// `none | math-auto | [capitalize | uppercase | lowercase] || full-width || full-size-kana`.) 415 /// https://drafts.csswg.org/css-text-4/#text-transform-property 416 pub struct TextTransform(u8); 417 bitflags! { 418 impl TextTransform: u8 { 419 /// none 420 const NONE = 0; 421 /// All uppercase. 422 const UPPERCASE = 1 << 0; 423 /// All lowercase. 424 const LOWERCASE = 1 << 1; 425 /// Capitalize each word. 426 const CAPITALIZE = 1 << 2; 427 /// Automatic italicization of math variables. 428 const MATH_AUTO = 1 << 3; 429 430 /// All the case transforms, which are exclusive with each other. 431 const CASE_TRANSFORMS = Self::UPPERCASE.0 | Self::LOWERCASE.0 | Self::CAPITALIZE.0 | Self::MATH_AUTO.0; 432 433 /// full-width 434 const FULL_WIDTH = 1 << 4; 435 /// full-size-kana 436 const FULL_SIZE_KANA = 1 << 5; 437 } 438 } 439 440 impl TextTransform { 441 /// Returns the initial value of text-transform 442 #[inline] 443 pub fn none() -> Self { 444 Self::NONE 445 } 446 447 /// Returns whether the value is 'none' 448 #[inline] 449 pub fn is_none(self) -> bool { 450 self == Self::NONE 451 } 452 453 fn validate_mixed_flags(&self) -> bool { 454 let case = self.intersection(Self::CASE_TRANSFORMS); 455 // Case bits are exclusive with each other. 456 case.is_empty() || case.bits().is_power_of_two() 457 } 458 459 /// Returns the corresponding TextTransformCase. 460 pub fn case(&self) -> TextTransformCase { 461 match *self & Self::CASE_TRANSFORMS { 462 Self::NONE => TextTransformCase::None, 463 Self::UPPERCASE => TextTransformCase::Uppercase, 464 Self::LOWERCASE => TextTransformCase::Lowercase, 465 Self::CAPITALIZE => TextTransformCase::Capitalize, 466 Self::MATH_AUTO => TextTransformCase::MathAuto, 467 _ => unreachable!("Case bits are exclusive with each other"), 468 } 469 } 470 } 471 472 /// Specified and computed value of text-align-last. 473 #[derive( 474 Clone, 475 Copy, 476 Debug, 477 Eq, 478 FromPrimitive, 479 Hash, 480 MallocSizeOf, 481 Parse, 482 PartialEq, 483 SpecifiedValueInfo, 484 ToComputedValue, 485 ToCss, 486 ToResolvedValue, 487 ToShmem, 488 ToTyped, 489 )] 490 #[allow(missing_docs)] 491 #[repr(u8)] 492 pub enum TextAlignLast { 493 Auto, 494 Start, 495 End, 496 Left, 497 Right, 498 Center, 499 Justify, 500 } 501 502 /// Specified value of text-align keyword value. 503 #[derive( 504 Clone, 505 Copy, 506 Debug, 507 Eq, 508 FromPrimitive, 509 Hash, 510 MallocSizeOf, 511 Parse, 512 PartialEq, 513 SpecifiedValueInfo, 514 ToComputedValue, 515 ToCss, 516 ToResolvedValue, 517 ToShmem, 518 ToTyped, 519 )] 520 #[allow(missing_docs)] 521 #[repr(u8)] 522 pub enum TextAlignKeyword { 523 Start, 524 Left, 525 Right, 526 Center, 527 Justify, 528 End, 529 #[parse(aliases = "-webkit-center")] 530 MozCenter, 531 #[parse(aliases = "-webkit-left")] 532 MozLeft, 533 #[parse(aliases = "-webkit-right")] 534 MozRight, 535 } 536 537 /// Specified value of text-align property. 538 #[derive( 539 Clone, 540 Copy, 541 Debug, 542 Eq, 543 Hash, 544 MallocSizeOf, 545 Parse, 546 PartialEq, 547 SpecifiedValueInfo, 548 ToCss, 549 ToShmem, 550 ToTyped, 551 )] 552 pub enum TextAlign { 553 /// Keyword value of text-align property. 554 Keyword(TextAlignKeyword), 555 /// `match-parent` value of text-align property. It has a different handling 556 /// unlike other keywords. 557 #[cfg(feature = "gecko")] 558 MatchParent, 559 /// This is how we implement the following HTML behavior from 560 /// https://html.spec.whatwg.org/#tables-2: 561 /// 562 /// User agents are expected to have a rule in their user agent style sheet 563 /// that matches th elements that have a parent node whose computed value 564 /// for the 'text-align' property is its initial value, whose declaration 565 /// block consists of just a single declaration that sets the 'text-align' 566 /// property to the value 'center'. 567 /// 568 /// Since selectors can't depend on the ancestor styles, we implement it with a 569 /// magic value that computes to the right thing. Since this is an 570 /// implementation detail, it shouldn't be exposed to web content. 571 #[parse(condition = "ParserContext::chrome_rules_enabled")] 572 MozCenterOrInherit, 573 } 574 575 impl ToComputedValue for TextAlign { 576 type ComputedValue = TextAlignKeyword; 577 578 #[inline] 579 fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue { 580 match *self { 581 TextAlign::Keyword(key) => key, 582 #[cfg(feature = "gecko")] 583 TextAlign::MatchParent => { 584 // on the root <html> element we should still respect the dir 585 // but the parent dir of that element is LTR even if it's <html dir=rtl> 586 // and will only be RTL if certain prefs have been set. 587 // In that case, the default behavior here will set it to left, 588 // but we want to set it to right -- instead set it to the default (`start`), 589 // which will do the right thing in this case (but not the general case) 590 if _context.builder.is_root_element { 591 return TextAlignKeyword::Start; 592 } 593 let parent = _context 594 .builder 595 .get_parent_inherited_text() 596 .clone_text_align(); 597 let ltr = _context.builder.inherited_writing_mode().is_bidi_ltr(); 598 match (parent, ltr) { 599 (TextAlignKeyword::Start, true) => TextAlignKeyword::Left, 600 (TextAlignKeyword::Start, false) => TextAlignKeyword::Right, 601 (TextAlignKeyword::End, true) => TextAlignKeyword::Right, 602 (TextAlignKeyword::End, false) => TextAlignKeyword::Left, 603 _ => parent, 604 } 605 }, 606 TextAlign::MozCenterOrInherit => { 607 let parent = _context 608 .builder 609 .get_parent_inherited_text() 610 .clone_text_align(); 611 if parent == TextAlignKeyword::Start { 612 TextAlignKeyword::Center 613 } else { 614 parent 615 } 616 }, 617 } 618 } 619 620 #[inline] 621 fn from_computed_value(computed: &Self::ComputedValue) -> Self { 622 TextAlign::Keyword(*computed) 623 } 624 } 625 626 fn fill_mode_is_default_and_shape_exists( 627 fill: &TextEmphasisFillMode, 628 shape: &Option<TextEmphasisShapeKeyword>, 629 ) -> bool { 630 shape.is_some() && fill.is_filled() 631 } 632 633 /// Specified value of text-emphasis-style property. 634 /// 635 /// https://drafts.csswg.org/css-text-decor/#propdef-text-emphasis-style 636 #[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, ToTyped)] 637 #[allow(missing_docs)] 638 pub enum TextEmphasisStyle { 639 /// [ <fill> || <shape> ] 640 Keyword { 641 #[css(contextual_skip_if = "fill_mode_is_default_and_shape_exists")] 642 fill: TextEmphasisFillMode, 643 shape: Option<TextEmphasisShapeKeyword>, 644 }, 645 /// `none` 646 None, 647 /// `<string>` (of which only the first grapheme cluster will be used). 648 String(crate::OwnedStr), 649 } 650 651 /// Fill mode for the text-emphasis-style property 652 #[derive( 653 Clone, 654 Copy, 655 Debug, 656 MallocSizeOf, 657 Parse, 658 PartialEq, 659 SpecifiedValueInfo, 660 ToCss, 661 ToComputedValue, 662 ToResolvedValue, 663 ToShmem, 664 )] 665 #[repr(u8)] 666 pub enum TextEmphasisFillMode { 667 /// `filled` 668 Filled, 669 /// `open` 670 Open, 671 } 672 673 impl TextEmphasisFillMode { 674 /// Whether the value is `filled`. 675 #[inline] 676 pub fn is_filled(&self) -> bool { 677 matches!(*self, TextEmphasisFillMode::Filled) 678 } 679 } 680 681 /// Shape keyword for the text-emphasis-style property 682 #[derive( 683 Clone, 684 Copy, 685 Debug, 686 Eq, 687 MallocSizeOf, 688 Parse, 689 PartialEq, 690 SpecifiedValueInfo, 691 ToCss, 692 ToComputedValue, 693 ToResolvedValue, 694 ToShmem, 695 )] 696 #[repr(u8)] 697 pub enum TextEmphasisShapeKeyword { 698 /// `dot` 699 Dot, 700 /// `circle` 701 Circle, 702 /// `double-circle` 703 DoubleCircle, 704 /// `triangle` 705 Triangle, 706 /// `sesame` 707 Sesame, 708 } 709 710 impl ToComputedValue for TextEmphasisStyle { 711 type ComputedValue = ComputedTextEmphasisStyle; 712 713 #[inline] 714 fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { 715 match *self { 716 TextEmphasisStyle::Keyword { fill, shape } => { 717 let shape = shape.unwrap_or_else(|| { 718 // FIXME(emilio, bug 1572958): This should set the 719 // rule_cache_conditions properly. 720 // 721 // Also should probably use WritingMode::is_vertical rather 722 // than the computed value of the `writing-mode` property. 723 if context.style().get_inherited_box().clone_writing_mode() 724 == SpecifiedWritingMode::HorizontalTb 725 { 726 TextEmphasisShapeKeyword::Circle 727 } else { 728 TextEmphasisShapeKeyword::Sesame 729 } 730 }); 731 ComputedTextEmphasisStyle::Keyword { fill, shape } 732 }, 733 TextEmphasisStyle::None => ComputedTextEmphasisStyle::None, 734 TextEmphasisStyle::String(ref s) => { 735 // FIXME(emilio): Doing this at computed value time seems wrong. 736 // The spec doesn't say that this should be a computed-value 737 // time operation. This is observable from getComputedStyle(). 738 // 739 // Note that the first grapheme cluster boundary should always be the start of the string. 740 let first_grapheme_end = GraphemeClusterSegmenter::new() 741 .segment_str(s) 742 .nth(1) 743 .unwrap_or(0); 744 ComputedTextEmphasisStyle::String(s[0..first_grapheme_end].to_string().into()) 745 }, 746 } 747 } 748 749 #[inline] 750 fn from_computed_value(computed: &Self::ComputedValue) -> Self { 751 match *computed { 752 ComputedTextEmphasisStyle::Keyword { fill, shape } => TextEmphasisStyle::Keyword { 753 fill, 754 shape: Some(shape), 755 }, 756 ComputedTextEmphasisStyle::None => TextEmphasisStyle::None, 757 ComputedTextEmphasisStyle::String(ref string) => { 758 TextEmphasisStyle::String(string.clone()) 759 }, 760 } 761 } 762 } 763 764 impl Parse for TextEmphasisStyle { 765 fn parse<'i, 't>( 766 _context: &ParserContext, 767 input: &mut Parser<'i, 't>, 768 ) -> Result<Self, ParseError<'i>> { 769 if input 770 .try_parse(|input| input.expect_ident_matching("none")) 771 .is_ok() 772 { 773 return Ok(TextEmphasisStyle::None); 774 } 775 776 if let Ok(s) = input.try_parse(|i| i.expect_string().map(|s| s.as_ref().to_owned())) { 777 // Handle <string> 778 return Ok(TextEmphasisStyle::String(s.into())); 779 } 780 781 // Handle a pair of keywords 782 let mut shape = input.try_parse(TextEmphasisShapeKeyword::parse).ok(); 783 let fill = input.try_parse(TextEmphasisFillMode::parse).ok(); 784 if shape.is_none() { 785 shape = input.try_parse(TextEmphasisShapeKeyword::parse).ok(); 786 } 787 788 if shape.is_none() && fill.is_none() { 789 return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); 790 } 791 792 // If a shape keyword is specified but neither filled nor open is 793 // specified, filled is assumed. 794 let fill = fill.unwrap_or(TextEmphasisFillMode::Filled); 795 796 // We cannot do the same because the default `<shape>` depends on the 797 // computed writing-mode. 798 Ok(TextEmphasisStyle::Keyword { fill, shape }) 799 } 800 } 801 802 #[derive( 803 Clone, 804 Copy, 805 Debug, 806 Eq, 807 MallocSizeOf, 808 PartialEq, 809 Parse, 810 Serialize, 811 SpecifiedValueInfo, 812 ToCss, 813 ToComputedValue, 814 ToResolvedValue, 815 ToShmem, 816 ToTyped, 817 )] 818 #[repr(C)] 819 #[css(bitflags( 820 single = "auto", 821 mixed = "over,under,left,right", 822 validate_mixed = "Self::validate_and_simplify" 823 ))] 824 /// Values for text-emphasis-position: 825 /// <https://drafts.csswg.org/css-text-decor/#text-emphasis-position-property> 826 pub struct TextEmphasisPosition(u8); 827 bitflags! { 828 impl TextEmphasisPosition: u8 { 829 /// Automatically choose mark position based on language. 830 const AUTO = 1 << 0; 831 /// Draw marks over the text in horizontal writing mode. 832 const OVER = 1 << 1; 833 /// Draw marks under the text in horizontal writing mode. 834 const UNDER = 1 << 2; 835 /// Draw marks to the left of the text in vertical writing mode. 836 const LEFT = 1 << 3; 837 /// Draw marks to the right of the text in vertical writing mode. 838 const RIGHT = 1 << 4; 839 } 840 } 841 842 impl TextEmphasisPosition { 843 fn validate_and_simplify(&mut self) -> bool { 844 // Require one but not both of 'over' and 'under'. 845 if self.intersects(Self::OVER) == self.intersects(Self::UNDER) { 846 return false; 847 } 848 849 // If 'left' is present, 'right' must be absent. 850 if self.intersects(Self::LEFT) { 851 return !self.intersects(Self::RIGHT); 852 } 853 854 self.remove(Self::RIGHT); // Right is the default 855 true 856 } 857 } 858 859 /// Values for the `word-break` property. 860 #[repr(u8)] 861 #[derive( 862 Clone, 863 Copy, 864 Debug, 865 Eq, 866 MallocSizeOf, 867 Parse, 868 PartialEq, 869 SpecifiedValueInfo, 870 ToComputedValue, 871 ToCss, 872 ToResolvedValue, 873 ToShmem, 874 ToTyped, 875 )] 876 #[allow(missing_docs)] 877 pub enum WordBreak { 878 Normal, 879 BreakAll, 880 KeepAll, 881 /// The break-word value, needed for compat. 882 /// 883 /// Specifying `word-break: break-word` makes `overflow-wrap` behave as 884 /// `anywhere`, and `word-break` behave like `normal`. 885 #[cfg(feature = "gecko")] 886 BreakWord, 887 } 888 889 /// Values for the `text-justify` CSS property. 890 #[repr(u8)] 891 #[derive( 892 Clone, 893 Copy, 894 Debug, 895 Eq, 896 MallocSizeOf, 897 Parse, 898 PartialEq, 899 SpecifiedValueInfo, 900 ToComputedValue, 901 ToCss, 902 ToResolvedValue, 903 ToShmem, 904 ToTyped, 905 )] 906 #[allow(missing_docs)] 907 pub enum TextJustify { 908 Auto, 909 None, 910 InterWord, 911 // See https://drafts.csswg.org/css-text-3/#valdef-text-justify-distribute 912 // and https://github.com/w3c/csswg-drafts/issues/6156 for the alias. 913 #[parse(aliases = "distribute")] 914 InterCharacter, 915 } 916 917 /// Values for the `-moz-control-character-visibility` CSS property. 918 #[repr(u8)] 919 #[derive( 920 Clone, 921 Copy, 922 Debug, 923 Eq, 924 MallocSizeOf, 925 Parse, 926 PartialEq, 927 SpecifiedValueInfo, 928 ToComputedValue, 929 ToCss, 930 ToResolvedValue, 931 ToShmem, 932 ToTyped, 933 )] 934 #[allow(missing_docs)] 935 pub enum MozControlCharacterVisibility { 936 Hidden, 937 Visible, 938 } 939 940 #[cfg(feature = "gecko")] 941 impl Default for MozControlCharacterVisibility { 942 fn default() -> Self { 943 if static_prefs::pref!("layout.css.control-characters.visible") { 944 Self::Visible 945 } else { 946 Self::Hidden 947 } 948 } 949 } 950 951 /// Values for the `line-break` property. 952 #[repr(u8)] 953 #[derive( 954 Clone, 955 Copy, 956 Debug, 957 Eq, 958 MallocSizeOf, 959 Parse, 960 PartialEq, 961 SpecifiedValueInfo, 962 ToComputedValue, 963 ToCss, 964 ToResolvedValue, 965 ToShmem, 966 ToTyped, 967 )] 968 #[allow(missing_docs)] 969 pub enum LineBreak { 970 Auto, 971 Loose, 972 Normal, 973 Strict, 974 Anywhere, 975 } 976 977 /// Values for the `overflow-wrap` property. 978 #[repr(u8)] 979 #[derive( 980 Clone, 981 Copy, 982 Debug, 983 Eq, 984 MallocSizeOf, 985 Parse, 986 PartialEq, 987 SpecifiedValueInfo, 988 ToComputedValue, 989 ToCss, 990 ToResolvedValue, 991 ToShmem, 992 ToTyped, 993 )] 994 #[allow(missing_docs)] 995 pub enum OverflowWrap { 996 Normal, 997 BreakWord, 998 Anywhere, 999 } 1000 1001 /// A specified value for the `text-indent` property 1002 /// which takes the grammar of [<length-percentage>] && hanging? && each-line? 1003 /// 1004 /// https://drafts.csswg.org/css-text/#propdef-text-indent 1005 pub type TextIndent = GenericTextIndent<LengthPercentage>; 1006 1007 impl Parse for TextIndent { 1008 fn parse<'i, 't>( 1009 context: &ParserContext, 1010 input: &mut Parser<'i, 't>, 1011 ) -> Result<Self, ParseError<'i>> { 1012 let mut length = None; 1013 let mut hanging = false; 1014 let mut each_line = false; 1015 1016 // The length-percentage and the two possible keywords can occur in any order. 1017 while !input.is_exhausted() { 1018 // If we haven't seen a length yet, try to parse one. 1019 if length.is_none() { 1020 if let Ok(len) = input 1021 .try_parse(|i| LengthPercentage::parse_quirky(context, i, AllowQuirks::Yes)) 1022 { 1023 length = Some(len); 1024 continue; 1025 } 1026 } 1027 1028 // Servo doesn't support the keywords, so just break and let the caller deal with it. 1029 if cfg!(feature = "servo") { 1030 break; 1031 } 1032 1033 // Check for the keywords (boolean flags). 1034 try_match_ident_ignore_ascii_case! { input, 1035 "hanging" if !hanging => hanging = true, 1036 "each-line" if !each_line => each_line = true, 1037 } 1038 } 1039 1040 // The length-percentage value is required for the declaration to be valid. 1041 if let Some(length) = length { 1042 Ok(Self { 1043 length, 1044 hanging, 1045 each_line, 1046 }) 1047 } else { 1048 Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) 1049 } 1050 } 1051 } 1052 1053 /// Implements text-decoration-skip-ink which takes the keywords auto | none | all 1054 /// 1055 /// https://drafts.csswg.org/css-text-decor-4/#text-decoration-skip-ink-property 1056 #[repr(u8)] 1057 #[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] 1058 #[derive( 1059 Clone, 1060 Copy, 1061 Debug, 1062 Eq, 1063 MallocSizeOf, 1064 Parse, 1065 PartialEq, 1066 SpecifiedValueInfo, 1067 ToComputedValue, 1068 ToCss, 1069 ToResolvedValue, 1070 ToShmem, 1071 ToTyped, 1072 )] 1073 #[allow(missing_docs)] 1074 pub enum TextDecorationSkipInk { 1075 Auto, 1076 None, 1077 All, 1078 } 1079 1080 /// Implements type for `text-decoration-thickness` property 1081 pub type TextDecorationLength = GenericTextDecorationLength<LengthPercentage>; 1082 1083 impl TextDecorationLength { 1084 /// `Auto` value. 1085 #[inline] 1086 pub fn auto() -> Self { 1087 GenericTextDecorationLength::Auto 1088 } 1089 1090 /// Whether this is the `Auto` value. 1091 #[inline] 1092 pub fn is_auto(&self) -> bool { 1093 matches!(*self, GenericTextDecorationLength::Auto) 1094 } 1095 } 1096 1097 /// Implements type for `text-decoration-inset` property 1098 pub type TextDecorationInset = GenericTextDecorationInset<Length>; 1099 1100 impl TextDecorationInset { 1101 /// `Auto` value. 1102 #[inline] 1103 pub fn auto() -> Self { 1104 GenericTextDecorationInset::Auto 1105 } 1106 1107 /// Whether this is the `Auto` value. 1108 #[inline] 1109 pub fn is_auto(&self) -> bool { 1110 matches!(*self, GenericTextDecorationInset::Auto) 1111 } 1112 } 1113 1114 impl Parse for TextDecorationInset { 1115 fn parse<'i, 't>( 1116 ctx: &ParserContext, 1117 input: &mut Parser<'i, 't>, 1118 ) -> Result<Self, ParseError<'i>> { 1119 if let Ok(start) = input.try_parse(|i| Length::parse(ctx, i)) { 1120 let end = input.try_parse(|i| Length::parse(ctx, i)); 1121 let end = end.unwrap_or_else(|_| start.clone()); 1122 return Ok(TextDecorationInset::Length { start, end }); 1123 } 1124 input.expect_ident_matching("auto")?; 1125 Ok(TextDecorationInset::Auto) 1126 } 1127 } 1128 1129 #[derive( 1130 Clone, 1131 Copy, 1132 Debug, 1133 Eq, 1134 MallocSizeOf, 1135 Parse, 1136 PartialEq, 1137 SpecifiedValueInfo, 1138 ToComputedValue, 1139 ToResolvedValue, 1140 ToShmem, 1141 ToTyped, 1142 )] 1143 #[css(bitflags( 1144 single = "auto", 1145 mixed = "from-font,under,left,right", 1146 validate_mixed = "Self::validate_mixed_flags", 1147 ))] 1148 #[repr(C)] 1149 /// Specified keyword values for the text-underline-position property. 1150 /// (Non-exclusive, but not all combinations are allowed: the spec grammar gives 1151 /// `auto | [ from-font | under ] || [ left | right ]`.) 1152 /// https://drafts.csswg.org/css-text-decor-4/#text-underline-position-property 1153 pub struct TextUnderlinePosition(u8); 1154 bitflags! { 1155 impl TextUnderlinePosition: u8 { 1156 /// Use automatic positioning below the alphabetic baseline. 1157 const AUTO = 0; 1158 /// Use underline position from the first available font. 1159 const FROM_FONT = 1 << 0; 1160 /// Below the glyph box. 1161 const UNDER = 1 << 1; 1162 /// In vertical mode, place to the left of the text. 1163 const LEFT = 1 << 2; 1164 /// In vertical mode, place to the right of the text. 1165 const RIGHT = 1 << 3; 1166 } 1167 } 1168 1169 impl TextUnderlinePosition { 1170 fn validate_mixed_flags(&self) -> bool { 1171 if self.contains(Self::LEFT | Self::RIGHT) { 1172 // left and right can't be mixed together. 1173 return false; 1174 } 1175 if self.contains(Self::FROM_FONT | Self::UNDER) { 1176 // from-font and under can't be mixed together either. 1177 return false; 1178 } 1179 true 1180 } 1181 } 1182 1183 impl ToCss for TextUnderlinePosition { 1184 fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result 1185 where 1186 W: Write, 1187 { 1188 if self.is_empty() { 1189 return dest.write_str("auto"); 1190 } 1191 1192 let mut writer = SequenceWriter::new(dest, " "); 1193 let mut any = false; 1194 1195 macro_rules! maybe_write { 1196 ($ident:ident => $str:expr) => { 1197 if self.contains(TextUnderlinePosition::$ident) { 1198 any = true; 1199 writer.raw_item($str)?; 1200 } 1201 }; 1202 } 1203 1204 maybe_write!(FROM_FONT => "from-font"); 1205 maybe_write!(UNDER => "under"); 1206 maybe_write!(LEFT => "left"); 1207 maybe_write!(RIGHT => "right"); 1208 1209 debug_assert!(any); 1210 1211 Ok(()) 1212 } 1213 } 1214 1215 /// Values for `ruby-position` property 1216 #[repr(u8)] 1217 #[derive( 1218 Clone, 1219 Copy, 1220 Debug, 1221 Eq, 1222 MallocSizeOf, 1223 PartialEq, 1224 ToComputedValue, 1225 ToResolvedValue, 1226 ToShmem, 1227 ToTyped, 1228 )] 1229 #[allow(missing_docs)] 1230 pub enum RubyPosition { 1231 AlternateOver, 1232 AlternateUnder, 1233 Over, 1234 Under, 1235 } 1236 1237 impl Parse for RubyPosition { 1238 fn parse<'i, 't>( 1239 _context: &ParserContext, 1240 input: &mut Parser<'i, 't>, 1241 ) -> Result<RubyPosition, ParseError<'i>> { 1242 // Parse alternate before 1243 let alternate = input 1244 .try_parse(|i| i.expect_ident_matching("alternate")) 1245 .is_ok(); 1246 if alternate && input.is_exhausted() { 1247 return Ok(RubyPosition::AlternateOver); 1248 } 1249 // Parse over / under 1250 let over = try_match_ident_ignore_ascii_case! { input, 1251 "over" => true, 1252 "under" => false, 1253 }; 1254 // Parse alternate after 1255 let alternate = alternate 1256 || input 1257 .try_parse(|i| i.expect_ident_matching("alternate")) 1258 .is_ok(); 1259 1260 Ok(match (over, alternate) { 1261 (true, true) => RubyPosition::AlternateOver, 1262 (false, true) => RubyPosition::AlternateUnder, 1263 (true, false) => RubyPosition::Over, 1264 (false, false) => RubyPosition::Under, 1265 }) 1266 } 1267 } 1268 1269 impl ToCss for RubyPosition { 1270 fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result 1271 where 1272 W: Write, 1273 { 1274 dest.write_str(match self { 1275 RubyPosition::AlternateOver => "alternate", 1276 RubyPosition::AlternateUnder => "alternate under", 1277 RubyPosition::Over => "over", 1278 RubyPosition::Under => "under", 1279 }) 1280 } 1281 } 1282 1283 impl SpecifiedValueInfo for RubyPosition { 1284 fn collect_completion_keywords(f: KeywordsCollectFn) { 1285 f(&["alternate", "over", "under"]) 1286 } 1287 } 1288 1289 /// Specified value for the text-autospace property 1290 /// which takes the grammar: 1291 /// normal | <autospace> | auto 1292 /// where: 1293 /// <autospace> = no-autospace | 1294 /// [ ideograph-alpha || ideograph-numeric || punctuation ] 1295 /// || [ insert | replace ] 1296 /// 1297 /// https://drafts.csswg.org/css-text-4/#text-autospace-property 1298 /// 1299 /// Bug 1980111: 'replace' value is not supported yet. 1300 #[derive( 1301 Clone, 1302 Copy, 1303 Debug, 1304 Eq, 1305 MallocSizeOf, 1306 Parse, 1307 PartialEq, 1308 Serialize, 1309 SpecifiedValueInfo, 1310 ToCss, 1311 ToComputedValue, 1312 ToResolvedValue, 1313 ToShmem, 1314 ToTyped, 1315 )] 1316 #[css(bitflags( 1317 single = "normal,auto,no-autospace", 1318 // Bug 1980111: add 'replace' to 'mixed' in the future so that it parses correctly. 1319 // Bug 1986500: add 'punctuation' to 'mixed' in the future so that it parses correctly. 1320 mixed = "ideograph-alpha,ideograph-numeric,insert", 1321 // Bug 1980111: Uncomment 'validate_mixed' to support 'replace' value. 1322 // validate_mixed = "Self::validate_mixed_flags", 1323 ))] 1324 #[repr(C)] 1325 pub struct TextAutospace(u8); 1326 bitflags! { 1327 impl TextAutospace: u8 { 1328 /// No automatic space is inserted. 1329 const NO_AUTOSPACE = 0; 1330 1331 /// The user agent chooses a set of typographically high quality spacing values. 1332 const AUTO = 1 << 0; 1333 1334 /// Same behavior as ideograph-alpha ideograph-numeric. 1335 const NORMAL = 1 << 1; 1336 1337 /// 1/8ic space between ideographic characters and non-ideographic letters. 1338 const IDEOGRAPH_ALPHA = 1 << 2; 1339 1340 /// 1/8ic space between ideographic characters and non-ideographic decimal numerals. 1341 const IDEOGRAPH_NUMERIC = 1 << 3; 1342 1343 /* Bug 1986500: Uncomment the following to support the 'punctuation' value. 1344 /// Apply special spacing between letters and punctuation (French). 1345 const PUNCTUATION = 1 << 4; 1346 */ 1347 1348 /// Auto-spacing is only inserted if no space character is present in the text. 1349 const INSERT = 1 << 5; 1350 1351 /* Bug 1980111: Uncomment the following to support 'replace' value. 1352 /// Auto-spacing may replace an existing U+0020 space with custom space. 1353 const REPLACE = 1 << 6; 1354 */ 1355 } 1356 } 1357 1358 /* Bug 1980111: Uncomment the following to support 'replace' value. 1359 impl TextAutospace { 1360 fn validate_mixed_flags(&self) -> bool { 1361 // It's not valid to have both INSERT and REPLACE set. 1362 !self.contains(TextAutospace::INSERT | TextAutospace::REPLACE) 1363 } 1364 } 1365 */