Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//! Specified types for text properties.
use crate::parser::{Parse, ParserContext};
use crate::properties::longhands::writing_mode::computed_value::T as SpecifiedWritingMode;
use crate::values::computed::text::TextEmphasisStyle as ComputedTextEmphasisStyle;
use crate::values::computed::text::TextOverflow as ComputedTextOverflow;
use crate::values::computed::{Context, ToComputedValue};
use crate::values::generics::text::InitialLetter as GenericInitialLetter;
use crate::values::generics::text::{GenericTextDecorationLength, GenericTextIndent, Spacing};
use crate::values::specified::length::{Length, LengthPercentage};
use crate::values::specified::{AllowQuirks, Integer, Number};
use cssparser::{Parser, Token};
use icu_segmenter::GraphemeClusterSegmenter;
use selectors::parser::SelectorParseErrorKind;
use std::fmt::{self, Write};
use style_traits::values::SequenceWriter;
use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
use style_traits::{KeywordsCollectFn, SpecifiedValueInfo};
/// A specified type for the `initial-letter` property.
pub type InitialLetter = GenericInitialLetter<Number, Integer>;
/// A specified value for the `letter-spacing` property.
pub type LetterSpacing = Spacing<Length>;
/// A specified value for the `word-spacing` property.
pub type WordSpacing = Spacing<LengthPercentage>;
/// A value for the `hyphenate-character` property.
#[derive(
Clone,
Debug,
MallocSizeOf,
Parse,
PartialEq,
SpecifiedValueInfo,
ToComputedValue,
ToCss,
ToResolvedValue,
ToShmem,
)]
#[repr(C, u8)]
pub enum HyphenateCharacter {
/// `auto`
Auto,
/// `<string>`
String(crate::OwnedStr),
}
impl Parse for InitialLetter {
fn parse<'i, 't>(
context: &ParserContext,
input: &mut Parser<'i, 't>,
) -> Result<Self, ParseError<'i>> {
if input
.try_parse(|i| i.expect_ident_matching("normal"))
.is_ok()
{
return Ok(GenericInitialLetter::Normal);
}
let size = Number::parse_at_least_one(context, input)?;
let sink = input
.try_parse(|i| Integer::parse_positive(context, i))
.ok();
Ok(GenericInitialLetter::Specified(size, sink))
}
}
impl Parse for LetterSpacing {
fn parse<'i, 't>(
context: &ParserContext,
input: &mut Parser<'i, 't>,
) -> Result<Self, ParseError<'i>> {
Spacing::parse_with(context, input, |c, i| {
Length::parse_quirky(c, i, AllowQuirks::Yes)
})
}
}
impl Parse for WordSpacing {
fn parse<'i, 't>(
context: &ParserContext,
input: &mut Parser<'i, 't>,
) -> Result<Self, ParseError<'i>> {
Spacing::parse_with(context, input, |c, i| {
LengthPercentage::parse_quirky(c, i, AllowQuirks::Yes)
})
}
}
/// A generic value for the `text-overflow` property.
#[derive(
Clone,
Debug,
Eq,
MallocSizeOf,
PartialEq,
Parse,
SpecifiedValueInfo,
ToComputedValue,
ToCss,
ToResolvedValue,
ToShmem,
)]
#[repr(C, u8)]
pub enum TextOverflowSide {
/// Clip inline content.
Clip,
/// Render ellipsis to represent clipped inline content.
Ellipsis,
/// Render a given string to represent clipped inline content.
String(crate::OwnedStr),
}
#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
/// text-overflow. Specifies rendering when inline content overflows its line box edge.
pub struct TextOverflow {
/// First value. Applies to end line box edge if no second is supplied; line-left edge otherwise.
pub first: TextOverflowSide,
/// Second value. Applies to the line-right edge if supplied.
pub second: Option<TextOverflowSide>,
}
impl Parse for TextOverflow {
fn parse<'i, 't>(
context: &ParserContext,
input: &mut Parser<'i, 't>,
) -> Result<TextOverflow, ParseError<'i>> {
let first = TextOverflowSide::parse(context, input)?;
let second = input
.try_parse(|input| TextOverflowSide::parse(context, input))
.ok();
Ok(TextOverflow { first, second })
}
}
impl ToComputedValue for TextOverflow {
type ComputedValue = ComputedTextOverflow;
#[inline]
fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue {
if let Some(ref second) = self.second {
Self::ComputedValue {
first: self.first.clone(),
second: second.clone(),
sides_are_logical: false,
}
} else {
Self::ComputedValue {
first: TextOverflowSide::Clip,
second: self.first.clone(),
sides_are_logical: true,
}
}
}
#[inline]
fn from_computed_value(computed: &Self::ComputedValue) -> Self {
if computed.sides_are_logical {
assert_eq!(computed.first, TextOverflowSide::Clip);
TextOverflow {
first: computed.second.clone(),
second: None,
}
} else {
TextOverflow {
first: computed.first.clone(),
second: Some(computed.second.clone()),
}
}
}
}
#[derive(
Clone,
Copy,
Debug,
Eq,
MallocSizeOf,
PartialEq,
Parse,
Serialize,
SpecifiedValueInfo,
ToCss,
ToComputedValue,
ToResolvedValue,
ToShmem,
)]
#[css(bitflags(single = "none", mixed = "underline,overline,line-through,blink"))]
#[repr(C)]
/// Specified keyword values for the text-decoration-line property.
pub struct TextDecorationLine(u8);
bitflags! {
impl TextDecorationLine: u8 {
/// No text decoration line is specified.
const NONE = 0;
/// underline
const UNDERLINE = 1 << 0;
/// overline
const OVERLINE = 1 << 1;
/// line-through
const LINE_THROUGH = 1 << 2;
/// blink
const BLINK = 1 << 3;
/// Only set by presentation attributes
///
/// Setting this will mean that text-decorations use the color
/// specified by `color` in quirks mode.
///
/// For example, this gives <a href=foo><font color="red">text</font></a>
/// a red text decoration
#[cfg(feature = "gecko")]
const COLOR_OVERRIDE = 0x10;
}
}
impl Default for TextDecorationLine {
fn default() -> Self {
TextDecorationLine::NONE
}
}
impl TextDecorationLine {
#[inline]
/// Returns the initial value of text-decoration-line
pub fn none() -> Self {
TextDecorationLine::NONE
}
}
#[derive(
Clone,
Copy,
Debug,
Eq,
MallocSizeOf,
PartialEq,
SpecifiedValueInfo,
ToComputedValue,
ToResolvedValue,
ToShmem,
)]
#[repr(C)]
/// Specified value of the text-transform property, stored in two parts:
/// the case-related transforms (mutually exclusive, only one may be in effect), and others (non-exclusive).
pub struct TextTransform {
/// Case transform, if any.
pub case_: TextTransformCase,
/// Non-case transforms.
pub other_: TextTransformOther,
}
impl TextTransform {
#[inline]
/// Returns the initial value of text-transform
pub fn none() -> Self {
TextTransform {
case_: TextTransformCase::None,
other_: TextTransformOther::empty(),
}
}
#[inline]
/// Returns whether the value is 'none'
pub fn is_none(&self) -> bool {
self.case_ == TextTransformCase::None && self.other_.is_empty()
}
}
// TODO: This can be simplified by deriving it.
impl Parse for TextTransform {
fn parse<'i, 't>(
_context: &ParserContext,
input: &mut Parser<'i, 't>,
) -> Result<Self, ParseError<'i>> {
let mut result = TextTransform::none();
// Case keywords are mutually exclusive; other transforms may co-occur.
loop {
let location = input.current_source_location();
let ident = match input.next() {
Ok(&Token::Ident(ref ident)) => ident,
Ok(other) => return Err(location.new_unexpected_token_error(other.clone())),
Err(..) => break,
};
match_ignore_ascii_case! { ident,
"none" if result.is_none() => {
return Ok(result);
},
"uppercase" if result.case_ == TextTransformCase::None => {
result.case_ = TextTransformCase::Uppercase
},
"lowercase" if result.case_ == TextTransformCase::None => {
result.case_ = TextTransformCase::Lowercase
},
"capitalize" if result.case_ == TextTransformCase::None => {
result.case_ = TextTransformCase::Capitalize
},
"math-auto" if result.case_ == TextTransformCase::None &&
result.other_.is_empty() => {
result.case_ = TextTransformCase::MathAuto;
return Ok(result);
},
"full-width" if !result.other_.intersects(TextTransformOther::FULL_WIDTH) => {
result.other_.insert(TextTransformOther::FULL_WIDTH)
},
"full-size-kana" if !result.other_.intersects(TextTransformOther::FULL_SIZE_KANA) => {
result.other_.insert(TextTransformOther::FULL_SIZE_KANA)
},
_ => return Err(location.new_custom_error(
SelectorParseErrorKind::UnexpectedIdent(ident.clone())
)),
}
}
if result.is_none() {
Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
} else {
Ok(result)
}
}
}
impl ToCss for TextTransform {
fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
where
W: Write,
{
if self.is_none() {
return dest.write_str("none");
}
if self.case_ != TextTransformCase::None {
self.case_.to_css(dest)?;
if !self.other_.is_empty() {
dest.write_char(' ')?;
}
}
self.other_.to_css(dest)
}
}
#[derive(
Clone,
Copy,
Debug,
Eq,
MallocSizeOf,
PartialEq,
SpecifiedValueInfo,
ToComputedValue,
ToCss,
ToResolvedValue,
ToShmem,
)]
#[repr(C)]
/// Specified keyword values for case transforms in the text-transform property. (These are exclusive.)
pub enum TextTransformCase {
/// No case transform.
None,
/// All uppercase.
Uppercase,
/// All lowercase.
Lowercase,
/// Capitalize each word.
Capitalize,
/// Automatic italicization of math variables.
MathAuto,
}
#[derive(
Clone,
Copy,
Debug,
Eq,
MallocSizeOf,
PartialEq,
Parse,
Serialize,
SpecifiedValueInfo,
ToCss,
ToComputedValue,
ToResolvedValue,
ToShmem,
)]
#[css(bitflags(mixed = "full-width,full-size-kana"))]
#[repr(C)]
/// Specified keyword values for non-case transforms in the text-transform property. (Non-exclusive.)
pub struct TextTransformOther(u8);
bitflags! {
impl TextTransformOther: u8 {
/// full-width
const FULL_WIDTH = 1 << 0;
/// full-size-kana
const FULL_SIZE_KANA = 1 << 1;
}
}
/// Specified and computed value of text-align-last.
#[derive(
Clone,
Copy,
Debug,
Eq,
FromPrimitive,
Hash,
MallocSizeOf,
Parse,
PartialEq,
SpecifiedValueInfo,
ToComputedValue,
ToCss,
ToResolvedValue,
ToShmem,
)]
#[allow(missing_docs)]
#[repr(u8)]
pub enum TextAlignLast {
Auto,
Start,
End,
Left,
Right,
Center,
Justify,
}
/// Specified value of text-align keyword value.
#[derive(
Clone,
Copy,
Debug,
Eq,
FromPrimitive,
Hash,
MallocSizeOf,
Parse,
PartialEq,
SpecifiedValueInfo,
ToComputedValue,
ToCss,
ToResolvedValue,
ToShmem,
)]
#[allow(missing_docs)]
#[repr(u8)]
pub enum TextAlignKeyword {
Start,
Left,
Right,
Center,
#[cfg(any(feature = "gecko", feature = "servo-layout-2013"))]
Justify,
#[css(skip)]
#[cfg(feature = "gecko")]
Char,
End,
#[cfg(feature = "gecko")]
MozCenter,
#[cfg(feature = "gecko")]
MozLeft,
#[cfg(feature = "gecko")]
MozRight,
#[cfg(feature = "servo-layout-2013")]
ServoCenter,
#[cfg(feature = "servo-layout-2013")]
ServoLeft,
#[cfg(feature = "servo-layout-2013")]
ServoRight,
}
/// Specified value of text-align property.
#[derive(
Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem,
)]
pub enum TextAlign {
/// Keyword value of text-align property.
Keyword(TextAlignKeyword),
/// `match-parent` value of text-align property. It has a different handling
/// unlike other keywords.
#[cfg(feature = "gecko")]
MatchParent,
/// This is how we implement the following HTML behavior from
///
/// User agents are expected to have a rule in their user agent style sheet
/// that matches th elements that have a parent node whose computed value
/// for the 'text-align' property is its initial value, whose declaration
/// block consists of just a single declaration that sets the 'text-align'
/// property to the value 'center'.
///
/// Since selectors can't depend on the ancestor styles, we implement it with a
/// magic value that computes to the right thing. Since this is an
/// implementation detail, it shouldn't be exposed to web content.
#[cfg(feature = "gecko")]
#[parse(condition = "ParserContext::chrome_rules_enabled")]
MozCenterOrInherit,
}
impl ToComputedValue for TextAlign {
type ComputedValue = TextAlignKeyword;
#[inline]
fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue {
match *self {
TextAlign::Keyword(key) => key,
#[cfg(feature = "gecko")]
TextAlign::MatchParent => {
// on the root <html> element we should still respect the dir
// but the parent dir of that element is LTR even if it's <html dir=rtl>
// and will only be RTL if certain prefs have been set.
// In that case, the default behavior here will set it to left,
// but we want to set it to right -- instead set it to the default (`start`),
// which will do the right thing in this case (but not the general case)
if _context.builder.is_root_element {
return TextAlignKeyword::Start;
}
let parent = _context
.builder
.get_parent_inherited_text()
.clone_text_align();
let ltr = _context.builder.inherited_writing_mode().is_bidi_ltr();
match (parent, ltr) {
(TextAlignKeyword::Start, true) => TextAlignKeyword::Left,
(TextAlignKeyword::Start, false) => TextAlignKeyword::Right,
(TextAlignKeyword::End, true) => TextAlignKeyword::Right,
(TextAlignKeyword::End, false) => TextAlignKeyword::Left,
_ => parent,
}
},
#[cfg(feature = "gecko")]
TextAlign::MozCenterOrInherit => {
let parent = _context
.builder
.get_parent_inherited_text()
.clone_text_align();
if parent == TextAlignKeyword::Start {
TextAlignKeyword::Center
} else {
parent
}
},
}
}
#[inline]
fn from_computed_value(computed: &Self::ComputedValue) -> Self {
TextAlign::Keyword(*computed)
}
}
fn fill_mode_is_default_and_shape_exists(
fill: &TextEmphasisFillMode,
shape: &Option<TextEmphasisShapeKeyword>,
) -> bool {
shape.is_some() && fill.is_filled()
}
/// Specified value of text-emphasis-style property.
///
#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
#[allow(missing_docs)]
pub enum TextEmphasisStyle {
/// [ <fill> || <shape> ]
Keyword {
#[css(contextual_skip_if = "fill_mode_is_default_and_shape_exists")]
fill: TextEmphasisFillMode,
shape: Option<TextEmphasisShapeKeyword>,
},
/// `none`
None,
/// `<string>` (of which only the first grapheme cluster will be used).
String(crate::OwnedStr),
}
/// Fill mode for the text-emphasis-style property
#[derive(
Clone,
Copy,
Debug,
MallocSizeOf,
Parse,
PartialEq,
SpecifiedValueInfo,
ToCss,
ToComputedValue,
ToResolvedValue,
ToShmem,
)]
#[repr(u8)]
pub enum TextEmphasisFillMode {
/// `filled`
Filled,
/// `open`
Open,
}
impl TextEmphasisFillMode {
/// Whether the value is `filled`.
#[inline]
pub fn is_filled(&self) -> bool {
matches!(*self, TextEmphasisFillMode::Filled)
}
}
/// Shape keyword for the text-emphasis-style property
#[derive(
Clone,
Copy,
Debug,
Eq,
MallocSizeOf,
Parse,
PartialEq,
SpecifiedValueInfo,
ToCss,
ToComputedValue,
ToResolvedValue,
ToShmem,
)]
#[repr(u8)]
pub enum TextEmphasisShapeKeyword {
/// `dot`
Dot,
/// `circle`
Circle,
/// `double-circle`
DoubleCircle,
/// `triangle`
Triangle,
/// `sesame`
Sesame,
}
impl ToComputedValue for TextEmphasisStyle {
type ComputedValue = ComputedTextEmphasisStyle;
#[inline]
fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
match *self {
TextEmphasisStyle::Keyword { fill, shape } => {
let shape = shape.unwrap_or_else(|| {
// FIXME(emilio, bug 1572958): This should set the
// rule_cache_conditions properly.
//
// Also should probably use WritingMode::is_vertical rather
// than the computed value of the `writing-mode` property.
if context.style().get_inherited_box().clone_writing_mode() ==
SpecifiedWritingMode::HorizontalTb
{
TextEmphasisShapeKeyword::Circle
} else {
TextEmphasisShapeKeyword::Sesame
}
});
ComputedTextEmphasisStyle::Keyword { fill, shape }
},
TextEmphasisStyle::None => ComputedTextEmphasisStyle::None,
TextEmphasisStyle::String(ref s) => {
// FIXME(emilio): Doing this at computed value time seems wrong.
// The spec doesn't say that this should be a computed-value
// time operation. This is observable from getComputedStyle().
//
// Note that the first grapheme cluster boundary should always be the start of the string.
let first_grapheme_end = GraphemeClusterSegmenter::new().segment_str(s).nth(1).unwrap_or(0);
ComputedTextEmphasisStyle::String(s[0..first_grapheme_end].to_string().into())
},
}
}
#[inline]
fn from_computed_value(computed: &Self::ComputedValue) -> Self {
match *computed {
ComputedTextEmphasisStyle::Keyword { fill, shape } => TextEmphasisStyle::Keyword {
fill,
shape: Some(shape),
},
ComputedTextEmphasisStyle::None => TextEmphasisStyle::None,
ComputedTextEmphasisStyle::String(ref string) => {
TextEmphasisStyle::String(string.clone())
},
}
}
}
impl Parse for TextEmphasisStyle {
fn parse<'i, 't>(
_context: &ParserContext,
input: &mut Parser<'i, 't>,
) -> Result<Self, ParseError<'i>> {
if input
.try_parse(|input| input.expect_ident_matching("none"))
.is_ok()
{
return Ok(TextEmphasisStyle::None);
}
if let Ok(s) = input.try_parse(|i| i.expect_string().map(|s| s.as_ref().to_owned())) {
// Handle <string>
return Ok(TextEmphasisStyle::String(s.into()));
}
// Handle a pair of keywords
let mut shape = input.try_parse(TextEmphasisShapeKeyword::parse).ok();
let fill = input.try_parse(TextEmphasisFillMode::parse).ok();
if shape.is_none() {
shape = input.try_parse(TextEmphasisShapeKeyword::parse).ok();
}
if shape.is_none() && fill.is_none() {
return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
}
// If a shape keyword is specified but neither filled nor open is
// specified, filled is assumed.
let fill = fill.unwrap_or(TextEmphasisFillMode::Filled);
// We cannot do the same because the default `<shape>` depends on the
// computed writing-mode.
Ok(TextEmphasisStyle::Keyword { fill, shape })
}
}
#[derive(
Clone,
Copy,
Debug,
Eq,
MallocSizeOf,
PartialEq,
Parse,
Serialize,
SpecifiedValueInfo,
ToCss,
ToComputedValue,
ToResolvedValue,
ToShmem,
)]
#[repr(C)]
#[css(bitflags(
mixed = "over,under,left,right",
validate_mixed = "Self::validate_and_simplify"
))]
/// Values for text-emphasis-position:
pub struct TextEmphasisPosition(u8);
bitflags! {
impl TextEmphasisPosition: u8 {
/// Draws marks to the right of the text in vertical writing mode.
const OVER = 1 << 0;
/// Draw marks under the text in horizontal writing mode.
const UNDER = 1 << 1;
/// Draw marks to the left of the text in vertical writing mode.
const LEFT = 1 << 2;
/// Draws marks to the right of the text in vertical writing mode.
const RIGHT = 1 << 3;
}
}
impl TextEmphasisPosition {
fn validate_and_simplify(&mut self) -> bool {
if self.intersects(Self::OVER) == self.intersects(Self::UNDER) {
return false;
}
if self.intersects(Self::LEFT) {
return !self.intersects(Self::RIGHT);
}
self.remove(Self::RIGHT); // Right is the default
true
}
}
/// Values for the `word-break` property.
#[repr(u8)]
#[derive(
Clone,
Copy,
Debug,
Eq,
MallocSizeOf,
Parse,
PartialEq,
SpecifiedValueInfo,
ToComputedValue,
ToCss,
ToResolvedValue,
ToShmem,
)]
#[allow(missing_docs)]
pub enum WordBreak {
Normal,
BreakAll,
KeepAll,
/// The break-word value, needed for compat.
///
/// Specifying `word-break: break-word` makes `overflow-wrap` behave as
/// `anywhere`, and `word-break` behave like `normal`.
#[cfg(feature = "gecko")]
BreakWord,
}
/// Values for the `text-justify` CSS property.
#[repr(u8)]
#[derive(
Clone,
Copy,
Debug,
Eq,
MallocSizeOf,
Parse,
PartialEq,
SpecifiedValueInfo,
ToComputedValue,
ToCss,
ToResolvedValue,
ToShmem,
)]
#[allow(missing_docs)]
pub enum TextJustify {
Auto,
None,
InterWord,
#[parse(aliases = "distribute")]
InterCharacter,
}
/// Values for the `-moz-control-character-visibility` CSS property.
#[repr(u8)]
#[derive(
Clone,
Copy,
Debug,
Eq,
MallocSizeOf,
Parse,
PartialEq,
SpecifiedValueInfo,
ToComputedValue,
ToCss,
ToResolvedValue,
ToShmem,
)]
#[allow(missing_docs)]
pub enum MozControlCharacterVisibility {
Hidden,
Visible,
}
impl Default for MozControlCharacterVisibility {
fn default() -> Self {
if static_prefs::pref!("layout.css.control-characters.visible") {
Self::Visible
} else {
Self::Hidden
}
}
}
/// Values for the `line-break` property.
#[repr(u8)]
#[derive(
Clone,
Copy,
Debug,
Eq,
MallocSizeOf,
Parse,
PartialEq,
SpecifiedValueInfo,
ToComputedValue,
ToCss,
ToResolvedValue,
ToShmem,
)]
#[allow(missing_docs)]
pub enum LineBreak {
Auto,
Loose,
Normal,
Strict,
Anywhere,
}
/// Values for the `overflow-wrap` property.
#[repr(u8)]
#[derive(
Clone,
Copy,
Debug,
Eq,
MallocSizeOf,
Parse,
PartialEq,
SpecifiedValueInfo,
ToComputedValue,
ToCss,
ToResolvedValue,
ToShmem,
)]
#[allow(missing_docs)]
pub enum OverflowWrap {
Normal,
BreakWord,
Anywhere,
}
/// A specified value for the `text-indent` property
/// which takes the grammar of [<length-percentage>] && hanging? && each-line?
///
pub type TextIndent = GenericTextIndent<LengthPercentage>;
impl Parse for TextIndent {
fn parse<'i, 't>(
context: &ParserContext,
input: &mut Parser<'i, 't>,
) -> Result<Self, ParseError<'i>> {
let mut length = None;
let mut hanging = false;
let mut each_line = false;
// The length-percentage and the two possible keywords can occur in any order.
while !input.is_exhausted() {
// If we haven't seen a length yet, try to parse one.
if length.is_none() {
if let Ok(len) = input
.try_parse(|i| LengthPercentage::parse_quirky(context, i, AllowQuirks::Yes))
{
length = Some(len);
continue;
}
}
if static_prefs::pref!("layout.css.text-indent-keywords.enabled") {
// Check for the keywords (boolean flags).
try_match_ident_ignore_ascii_case! { input,
"hanging" if !hanging => hanging = true,
"each-line" if !each_line => each_line = true,
}
continue;
}
// If we reach here, there must be something that we failed to parse;
// just break and let the caller deal with it.
break;
}
// The length-percentage value is required for the declaration to be valid.
if let Some(length) = length {
Ok(Self {
length,
hanging,
each_line,
})
} else {
Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
}
}
}
/// Implements text-decoration-skip-ink which takes the keywords auto | none | all
///
#[repr(u8)]
#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
#[derive(
Clone,
Copy,
Debug,
Eq,
MallocSizeOf,
Parse,
PartialEq,
SpecifiedValueInfo,
ToComputedValue,
ToCss,
ToResolvedValue,
ToShmem,
)]
#[allow(missing_docs)]
pub enum TextDecorationSkipInk {
Auto,
None,
All,
}
/// Implements type for `text-decoration-thickness` property
pub type TextDecorationLength = GenericTextDecorationLength<LengthPercentage>;
impl TextDecorationLength {
/// `Auto` value.
#[inline]
pub fn auto() -> Self {
GenericTextDecorationLength::Auto
}
/// Whether this is the `Auto` value.
#[inline]
pub fn is_auto(&self) -> bool {
matches!(*self, GenericTextDecorationLength::Auto)
}
}
#[derive(
Clone,
Copy,
Debug,
Eq,
MallocSizeOf,
PartialEq,
SpecifiedValueInfo,
ToComputedValue,
ToResolvedValue,
ToShmem,
)]
#[value_info(other_values = "auto,from-font,under,left,right")]
#[repr(C)]
/// Specified keyword values for the text-underline-position property.
/// (Non-exclusive, but not all combinations are allowed: the spec grammar gives
/// `auto | [ from-font | under ] || [ left | right ]`.)
pub struct TextUnderlinePosition(u8);
bitflags! {
impl TextUnderlinePosition: u8 {
/// Use automatic positioning below the alphabetic baseline.
const AUTO = 0;
/// Use underline position from the first available font.
const FROM_FONT = 1 << 0;
/// Below the glyph box.
const UNDER = 1 << 1;
/// In vertical mode, place to the left of the text.
const LEFT = 1 << 2;
/// In vertical mode, place to the right of the text.
const RIGHT = 1 << 3;
}
}
// TODO: This can be derived with some care.
impl Parse for TextUnderlinePosition {
fn parse<'i, 't>(
_context: &ParserContext,
input: &mut Parser<'i, 't>,
) -> Result<TextUnderlinePosition, ParseError<'i>> {
let mut result = TextUnderlinePosition::empty();
loop {
let location = input.current_source_location();
let ident = match input.next() {
Ok(&Token::Ident(ref ident)) => ident,
Ok(other) => return Err(location.new_unexpected_token_error(other.clone())),
Err(..) => break,
};
match_ignore_ascii_case! { ident,
"auto" if result.is_empty() => {
return Ok(result);
},
"from-font" if !result.intersects(TextUnderlinePosition::FROM_FONT |
TextUnderlinePosition::UNDER) => {
result.insert(TextUnderlinePosition::FROM_FONT);
},
"under" if !result.intersects(TextUnderlinePosition::FROM_FONT |
TextUnderlinePosition::UNDER) => {
result.insert(TextUnderlinePosition::UNDER);
},
"left" if !result.intersects(TextUnderlinePosition::LEFT |
TextUnderlinePosition::RIGHT) => {
result.insert(TextUnderlinePosition::LEFT);
},
"right" if !result.intersects(TextUnderlinePosition::LEFT |
TextUnderlinePosition::RIGHT) => {
result.insert(TextUnderlinePosition::RIGHT);
},
_ => return Err(location.new_custom_error(
SelectorParseErrorKind::UnexpectedIdent(ident.clone())
)),
}
}
if !result.is_empty() {
Ok(result)
} else {
Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
}
}
}
impl ToCss for TextUnderlinePosition {
fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
where
W: Write,
{
if self.is_empty() {
return dest.write_str("auto");
}
let mut writer = SequenceWriter::new(dest, " ");
let mut any = false;
macro_rules! maybe_write {
($ident:ident => $str:expr) => {
if self.contains(TextUnderlinePosition::$ident) {
any = true;
writer.raw_item($str)?;
}
};
}
maybe_write!(FROM_FONT => "from-font");
maybe_write!(UNDER => "under");
maybe_write!(LEFT => "left");
maybe_write!(RIGHT => "right");
debug_assert!(any);
Ok(())
}
}
/// Values for `ruby-position` property
#[repr(u8)]
#[derive(
Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem,
)]
#[allow(missing_docs)]
pub enum RubyPosition {
AlternateOver,
AlternateUnder,
Over,
Under,
}
impl Parse for RubyPosition {
fn parse<'i, 't>(
_context: &ParserContext,
input: &mut Parser<'i, 't>,
) -> Result<RubyPosition, ParseError<'i>> {
// Parse alternate before
let alternate = input
.try_parse(|i| i.expect_ident_matching("alternate"))
.is_ok();
if alternate && input.is_exhausted() {
return Ok(RubyPosition::AlternateOver);
}
// Parse over / under
let over = try_match_ident_ignore_ascii_case! { input,
"over" => true,
"under" => false,
};
// Parse alternate after
let alternate = alternate ||
input
.try_parse(|i| i.expect_ident_matching("alternate"))
.is_ok();
Ok(match (over, alternate) {
(true, true) => RubyPosition::AlternateOver,
(false, true) => RubyPosition::AlternateUnder,
(true, false) => RubyPosition::Over,
(false, false) => RubyPosition::Under,
})
}
}
impl ToCss for RubyPosition {
fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
where
W: Write,
{
dest.write_str(match self {
RubyPosition::AlternateOver => "alternate",
RubyPosition::AlternateUnder => "alternate under",
RubyPosition::Over => "over",
RubyPosition::Under => "under",
})
}
}
impl SpecifiedValueInfo for RubyPosition {
fn collect_completion_keywords(f: KeywordsCollectFn) {
f(&["alternate", "over", "under"])
}
}