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