mirror of
https://github.com/servo/servo.git
synced 2025-07-10 17:03:40 +01:00
1029 lines
32 KiB
Rust
1029 lines
32 KiB
Rust
/* 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::LineHeight as ComputedLineHeight;
|
|
use crate::values::computed::text::TextEmphasisKeywordValue as ComputedTextEmphasisKeywordValue;
|
|
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::LineHeight as GenericLineHeight;
|
|
use crate::values::generics::text::Spacing;
|
|
use crate::values::specified::length::NonNegativeLengthPercentage;
|
|
use crate::values::specified::length::{FontRelativeLength, Length};
|
|
use crate::values::specified::length::{LengthPercentage, NoCalcLength};
|
|
use crate::values::specified::{AllowQuirks, Integer, NonNegativeNumber, Number};
|
|
use cssparser::{Parser, Token};
|
|
use selectors::parser::SelectorParseErrorKind;
|
|
use std::fmt::{self, Write};
|
|
use style_traits::values::SequenceWriter;
|
|
use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
|
|
use unicode_segmentation::UnicodeSegmentation;
|
|
|
|
/// 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 specified value for the `line-height` property.
|
|
pub type LineHeight = GenericLineHeight<NonNegativeNumber, NonNegativeLengthPercentage>;
|
|
|
|
impl Parse for InitialLetter {
|
|
fn parse<'i, 't>(
|
|
context: &ParserContext,
|
|
input: &mut Parser<'i, 't>,
|
|
) -> Result<Self, ParseError<'i>> {
|
|
if input.try(|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(|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)
|
|
})
|
|
}
|
|
}
|
|
|
|
impl ToComputedValue for LineHeight {
|
|
type ComputedValue = ComputedLineHeight;
|
|
|
|
#[inline]
|
|
fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
|
|
use crate::values::computed::Length as ComputedLength;
|
|
use crate::values::specified::length::FontBaseSize;
|
|
match *self {
|
|
GenericLineHeight::Normal => GenericLineHeight::Normal,
|
|
#[cfg(feature = "gecko")]
|
|
GenericLineHeight::MozBlockHeight => GenericLineHeight::MozBlockHeight,
|
|
GenericLineHeight::Number(number) => {
|
|
GenericLineHeight::Number(number.to_computed_value(context))
|
|
},
|
|
GenericLineHeight::Length(ref non_negative_lp) => {
|
|
let result = match non_negative_lp.0 {
|
|
LengthPercentage::Length(NoCalcLength::Absolute(ref abs)) => {
|
|
context
|
|
.maybe_zoom_text(abs.to_computed_value(context).into())
|
|
.0
|
|
},
|
|
LengthPercentage::Length(ref length) => length.to_computed_value(context),
|
|
LengthPercentage::Percentage(ref p) => FontRelativeLength::Em(p.0)
|
|
.to_computed_value(context, FontBaseSize::CurrentStyle),
|
|
LengthPercentage::Calc(ref calc) => {
|
|
let computed_calc =
|
|
calc.to_computed_value_zoomed(context, FontBaseSize::CurrentStyle);
|
|
let font_relative_length =
|
|
FontRelativeLength::Em(computed_calc.percentage())
|
|
.to_computed_value(context, FontBaseSize::CurrentStyle)
|
|
.px();
|
|
|
|
let absolute_length = computed_calc.unclamped_length().px();
|
|
let pixel = computed_calc
|
|
.clamping_mode
|
|
.clamp(absolute_length + font_relative_length);
|
|
ComputedLength::new(pixel)
|
|
},
|
|
};
|
|
GenericLineHeight::Length(result.into())
|
|
},
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn from_computed_value(computed: &Self::ComputedValue) -> Self {
|
|
match *computed {
|
|
GenericLineHeight::Normal => GenericLineHeight::Normal,
|
|
#[cfg(feature = "gecko")]
|
|
GenericLineHeight::MozBlockHeight => GenericLineHeight::MozBlockHeight,
|
|
GenericLineHeight::Number(ref number) => {
|
|
GenericLineHeight::Number(NonNegativeNumber::from_computed_value(number))
|
|
},
|
|
GenericLineHeight::Length(ref length) => {
|
|
GenericLineHeight::Length(NoCalcLength::from_computed_value(&length.0).into())
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A generic value for the `text-overflow` property.
|
|
#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
|
|
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(Box<str>),
|
|
}
|
|
|
|
impl Parse for TextOverflowSide {
|
|
fn parse<'i, 't>(
|
|
_context: &ParserContext,
|
|
input: &mut Parser<'i, 't>,
|
|
) -> Result<TextOverflowSide, ParseError<'i>> {
|
|
let location = input.current_source_location();
|
|
match *input.next()? {
|
|
Token::Ident(ref ident) => {
|
|
match_ignore_ascii_case! { ident,
|
|
"clip" => Ok(TextOverflowSide::Clip),
|
|
"ellipsis" => Ok(TextOverflowSide::Ellipsis),
|
|
_ => Err(location.new_custom_error(
|
|
SelectorParseErrorKind::UnexpectedIdent(ident.clone())
|
|
))
|
|
}
|
|
},
|
|
Token::QuotedString(ref v) => Ok(TextOverflowSide::String(
|
|
v.as_ref().to_owned().into_boxed_str(),
|
|
)),
|
|
ref t => Err(location.new_unexpected_token_error(t.clone())),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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(|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()),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bitflags! {
|
|
#[derive(MallocSizeOf, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem)]
|
|
#[value_info(other_values = "none,underline,overline,line-through,blink")]
|
|
#[repr(C)]
|
|
/// Specified keyword values for the text-decoration-line property.
|
|
pub struct 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 Parse for TextDecorationLine {
|
|
/// none | [ underline || overline || line-through || blink ]
|
|
fn parse<'i, 't>(
|
|
_context: &ParserContext,
|
|
input: &mut Parser<'i, 't>,
|
|
) -> Result<Self, ParseError<'i>> {
|
|
let mut result = TextDecorationLine::empty();
|
|
|
|
// NOTE(emilio): this loop has this weird structure because we run this
|
|
// code to parse the text-decoration shorthand as well, so we need to
|
|
// ensure we don't return an error if we don't consume the whole thing
|
|
// because we find an invalid identifier or other kind of token.
|
|
loop {
|
|
let flag: Result<_, ParseError<'i>> = input.try(|input| {
|
|
let flag = try_match_ident_ignore_ascii_case! { input,
|
|
"none" if result.is_empty() => TextDecorationLine::NONE,
|
|
"underline" => TextDecorationLine::UNDERLINE,
|
|
"overline" => TextDecorationLine::OVERLINE,
|
|
"line-through" => TextDecorationLine::LINE_THROUGH,
|
|
"blink" => TextDecorationLine::BLINK,
|
|
};
|
|
|
|
Ok(flag)
|
|
});
|
|
|
|
let flag = match flag {
|
|
Ok(flag) => flag,
|
|
Err(..) => break,
|
|
};
|
|
|
|
if flag.is_empty() {
|
|
return Ok(TextDecorationLine::NONE);
|
|
}
|
|
|
|
if result.contains(flag) {
|
|
return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
|
|
}
|
|
|
|
result.insert(flag)
|
|
}
|
|
|
|
if !result.is_empty() {
|
|
Ok(result)
|
|
} else {
|
|
Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ToCss for TextDecorationLine {
|
|
fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
|
|
where
|
|
W: Write,
|
|
{
|
|
if self.is_empty() {
|
|
return dest.write_str("none");
|
|
}
|
|
|
|
#[cfg(feature = "gecko")]
|
|
{
|
|
if *self == TextDecorationLine::COLOR_OVERRIDE {
|
|
return Ok(());
|
|
}
|
|
}
|
|
|
|
let mut writer = SequenceWriter::new(dest, " ");
|
|
let mut any = false;
|
|
|
|
macro_rules! maybe_write {
|
|
($ident:ident => $str:expr) => {
|
|
if self.contains(TextDecorationLine::$ident) {
|
|
any = true;
|
|
writer.raw_item($str)?;
|
|
}
|
|
};
|
|
}
|
|
|
|
maybe_write!(UNDERLINE => "underline");
|
|
maybe_write!(OVERLINE => "overline");
|
|
maybe_write!(LINE_THROUGH => "line-through");
|
|
maybe_write!(BLINK => "blink");
|
|
|
|
debug_assert!(any);
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
|
|
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
|
|
},
|
|
"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_str(" ")?;
|
|
}
|
|
}
|
|
|
|
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,
|
|
}
|
|
|
|
bitflags! {
|
|
#[derive(MallocSizeOf, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem)]
|
|
#[value_info(other_values = "none,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 {
|
|
/// full-width
|
|
const FULL_WIDTH = 1 << 0;
|
|
/// full-size-kana
|
|
const FULL_SIZE_KANA = 1 << 1;
|
|
}
|
|
}
|
|
|
|
impl ToCss for TextTransformOther {
|
|
fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
|
|
where
|
|
W: Write,
|
|
{
|
|
let mut writer = SequenceWriter::new(dest, " ");
|
|
let mut any = false;
|
|
macro_rules! maybe_write {
|
|
($ident:ident => $str:expr) => {
|
|
if self.contains(TextTransformOther::$ident) {
|
|
writer.raw_item($str)?;
|
|
any = true;
|
|
}
|
|
};
|
|
}
|
|
|
|
maybe_write!(FULL_WIDTH => "full-width");
|
|
maybe_write!(FULL_SIZE_KANA => "full-size-kana");
|
|
|
|
debug_assert!(any || self.is_empty());
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// 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)]
|
|
pub enum TextAlignKeyword {
|
|
Start,
|
|
End,
|
|
Left,
|
|
Right,
|
|
Center,
|
|
Justify,
|
|
#[cfg(feature = "gecko")]
|
|
MozCenter,
|
|
#[cfg(feature = "gecko")]
|
|
MozLeft,
|
|
#[cfg(feature = "gecko")]
|
|
MozRight,
|
|
#[cfg(feature = "servo")]
|
|
ServoCenter,
|
|
#[cfg(feature = "servo")]
|
|
ServoLeft,
|
|
#[cfg(feature = "servo")]
|
|
ServoRight,
|
|
#[css(skip)]
|
|
#[cfg(feature = "gecko")]
|
|
Char,
|
|
}
|
|
|
|
/// 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,
|
|
/// `MozCenterOrInherit` value of text-align property. It cannot be parsed,
|
|
/// only set directly on the elements and it has a different handling
|
|
/// unlike other values.
|
|
#[cfg(feature = "gecko")]
|
|
#[css(skip)]
|
|
MozCenterOrInherit,
|
|
}
|
|
|
|
impl TextAlign {
|
|
/// Convert an enumerated value coming from Gecko to a `TextAlign`.
|
|
#[cfg(feature = "gecko")]
|
|
pub fn from_gecko_keyword(kw: u32) -> Self {
|
|
TextAlign::Keyword(TextAlignKeyword::from_gecko_keyword(kw))
|
|
}
|
|
}
|
|
|
|
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.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)
|
|
}
|
|
}
|
|
|
|
/// Specified value of text-emphasis-style property.
|
|
#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
|
|
pub enum TextEmphasisStyle {
|
|
/// <fill> <shape>
|
|
Keyword(TextEmphasisKeywordValue),
|
|
/// `none`
|
|
None,
|
|
/// String (will be used only first grapheme cluster) for the text-emphasis-style property
|
|
String(String),
|
|
}
|
|
|
|
/// Keyword value for the text-emphasis-style property
|
|
#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
|
|
pub enum TextEmphasisKeywordValue {
|
|
/// <fill>
|
|
Fill(TextEmphasisFillMode),
|
|
/// <shape>
|
|
Shape(TextEmphasisShapeKeyword),
|
|
/// <fill> <shape>
|
|
FillAndShape(TextEmphasisFillMode, TextEmphasisShapeKeyword),
|
|
}
|
|
|
|
impl TextEmphasisKeywordValue {
|
|
fn fill(&self) -> Option<TextEmphasisFillMode> {
|
|
match *self {
|
|
TextEmphasisKeywordValue::Fill(fill) |
|
|
TextEmphasisKeywordValue::FillAndShape(fill, _) => Some(fill),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn shape(&self) -> Option<TextEmphasisShapeKeyword> {
|
|
match *self {
|
|
TextEmphasisKeywordValue::Shape(shape) |
|
|
TextEmphasisKeywordValue::FillAndShape(_, shape) => Some(shape),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Fill mode for the text-emphasis-style property
|
|
#[derive(Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
|
|
pub enum TextEmphasisFillMode {
|
|
/// `filled`
|
|
Filled,
|
|
/// `open`
|
|
Open,
|
|
}
|
|
|
|
/// Shape keyword for the text-emphasis-style property
|
|
#[derive(
|
|
Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem,
|
|
)]
|
|
pub enum TextEmphasisShapeKeyword {
|
|
/// `dot`
|
|
Dot,
|
|
/// `circle`
|
|
Circle,
|
|
/// `double-circle`
|
|
DoubleCircle,
|
|
/// `triangle`
|
|
Triangle,
|
|
/// `sesame`
|
|
Sesame,
|
|
}
|
|
|
|
impl TextEmphasisShapeKeyword {
|
|
/// converts fill mode to a unicode char
|
|
pub fn char(&self, fill: TextEmphasisFillMode) -> &str {
|
|
let fill = fill == TextEmphasisFillMode::Filled;
|
|
match *self {
|
|
TextEmphasisShapeKeyword::Dot => {
|
|
if fill {
|
|
"\u{2022}"
|
|
} else {
|
|
"\u{25e6}"
|
|
}
|
|
},
|
|
TextEmphasisShapeKeyword::Circle => {
|
|
if fill {
|
|
"\u{25cf}"
|
|
} else {
|
|
"\u{25cb}"
|
|
}
|
|
},
|
|
TextEmphasisShapeKeyword::DoubleCircle => {
|
|
if fill {
|
|
"\u{25c9}"
|
|
} else {
|
|
"\u{25ce}"
|
|
}
|
|
},
|
|
TextEmphasisShapeKeyword::Triangle => {
|
|
if fill {
|
|
"\u{25b2}"
|
|
} else {
|
|
"\u{25b3}"
|
|
}
|
|
},
|
|
TextEmphasisShapeKeyword::Sesame => {
|
|
if fill {
|
|
"\u{fe45}"
|
|
} else {
|
|
"\u{fe46}"
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ToComputedValue for TextEmphasisStyle {
|
|
type ComputedValue = ComputedTextEmphasisStyle;
|
|
|
|
#[inline]
|
|
fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
|
|
match *self {
|
|
TextEmphasisStyle::Keyword(ref keyword) => {
|
|
// FIXME(emilio): This should set the rule_cache_conditions
|
|
// properly.
|
|
let default_shape = if context.style().get_inherited_box().clone_writing_mode() ==
|
|
SpecifiedWritingMode::HorizontalTb
|
|
{
|
|
TextEmphasisShapeKeyword::Circle
|
|
} else {
|
|
TextEmphasisShapeKeyword::Sesame
|
|
};
|
|
ComputedTextEmphasisStyle::Keyword(ComputedTextEmphasisKeywordValue {
|
|
fill: keyword.fill().unwrap_or(TextEmphasisFillMode::Filled),
|
|
shape: keyword.shape().unwrap_or(default_shape),
|
|
})
|
|
},
|
|
TextEmphasisStyle::None => ComputedTextEmphasisStyle::None,
|
|
TextEmphasisStyle::String(ref s) => {
|
|
// Passing `true` to iterate over extended grapheme clusters, following
|
|
// recommendation at http://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries
|
|
let string = s.graphemes(true).next().unwrap_or("").to_string();
|
|
ComputedTextEmphasisStyle::String(string)
|
|
},
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn from_computed_value(computed: &Self::ComputedValue) -> Self {
|
|
match *computed {
|
|
ComputedTextEmphasisStyle::Keyword(ref keyword) => TextEmphasisStyle::Keyword(
|
|
TextEmphasisKeywordValue::FillAndShape(keyword.fill, keyword.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(|input| input.expect_ident_matching("none"))
|
|
.is_ok()
|
|
{
|
|
return Ok(TextEmphasisStyle::None);
|
|
}
|
|
|
|
if let Ok(s) = input.try(|i| i.expect_string().map(|s| s.as_ref().to_owned())) {
|
|
// Handle <string>
|
|
return Ok(TextEmphasisStyle::String(s));
|
|
}
|
|
|
|
// Handle a pair of keywords
|
|
let mut shape = input.try(TextEmphasisShapeKeyword::parse).ok();
|
|
let fill = input.try(TextEmphasisFillMode::parse).ok();
|
|
if shape.is_none() {
|
|
shape = input.try(TextEmphasisShapeKeyword::parse).ok();
|
|
}
|
|
|
|
// At least one of shape or fill must be handled
|
|
let keyword_value = match (fill, shape) {
|
|
(Some(fill), Some(shape)) => TextEmphasisKeywordValue::FillAndShape(fill, shape),
|
|
(Some(fill), None) => TextEmphasisKeywordValue::Fill(fill),
|
|
(None, Some(shape)) => TextEmphasisKeywordValue::Shape(shape),
|
|
_ => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
|
|
};
|
|
Ok(TextEmphasisStyle::Keyword(keyword_value))
|
|
}
|
|
}
|
|
|
|
/// The allowed horizontal values for the `text-emphasis-position` property.
|
|
#[derive(
|
|
Clone,
|
|
Copy,
|
|
Debug,
|
|
Eq,
|
|
MallocSizeOf,
|
|
Parse,
|
|
PartialEq,
|
|
SpecifiedValueInfo,
|
|
ToComputedValue,
|
|
ToCss,
|
|
ToResolvedValue,
|
|
ToShmem,
|
|
)]
|
|
pub enum TextEmphasisHorizontalWritingModeValue {
|
|
/// Draw marks over the text in horizontal writing mode.
|
|
Over,
|
|
/// Draw marks under the text in horizontal writing mode.
|
|
Under,
|
|
}
|
|
|
|
/// The allowed vertical values for the `text-emphasis-position` property.
|
|
#[derive(
|
|
Clone,
|
|
Copy,
|
|
Debug,
|
|
Eq,
|
|
MallocSizeOf,
|
|
Parse,
|
|
PartialEq,
|
|
SpecifiedValueInfo,
|
|
ToComputedValue,
|
|
ToCss,
|
|
ToResolvedValue,
|
|
ToShmem,
|
|
)]
|
|
pub enum TextEmphasisVerticalWritingModeValue {
|
|
/// Draws marks to the right of the text in vertical writing mode.
|
|
Right,
|
|
/// Draw marks to the left of the text in vertical writing mode.
|
|
Left,
|
|
}
|
|
|
|
/// Specified value of `text-emphasis-position` property.
|
|
#[derive(
|
|
Clone,
|
|
Copy,
|
|
Debug,
|
|
MallocSizeOf,
|
|
PartialEq,
|
|
SpecifiedValueInfo,
|
|
ToComputedValue,
|
|
ToCss,
|
|
ToResolvedValue,
|
|
ToShmem,
|
|
)]
|
|
pub struct TextEmphasisPosition(
|
|
pub TextEmphasisHorizontalWritingModeValue,
|
|
pub TextEmphasisVerticalWritingModeValue,
|
|
);
|
|
|
|
impl TextEmphasisPosition {
|
|
#[inline]
|
|
/// Returns the initial value of `text-emphasis-position`
|
|
pub fn over_right() -> Self {
|
|
TextEmphasisPosition(
|
|
TextEmphasisHorizontalWritingModeValue::Over,
|
|
TextEmphasisVerticalWritingModeValue::Right,
|
|
)
|
|
}
|
|
|
|
#[cfg(feature = "gecko")]
|
|
/// Converts an enumerated value coming from Gecko to a `TextEmphasisPosition`.
|
|
pub fn from_gecko_keyword(kw: u32) -> Self {
|
|
use crate::gecko_bindings::structs;
|
|
|
|
let vert = if kw & structs::NS_STYLE_TEXT_EMPHASIS_POSITION_RIGHT != 0 {
|
|
TextEmphasisVerticalWritingModeValue::Right
|
|
} else {
|
|
debug_assert!(kw & structs::NS_STYLE_TEXT_EMPHASIS_POSITION_LEFT != 0);
|
|
TextEmphasisVerticalWritingModeValue::Left
|
|
};
|
|
let horiz = if kw & structs::NS_STYLE_TEXT_EMPHASIS_POSITION_OVER != 0 {
|
|
TextEmphasisHorizontalWritingModeValue::Over
|
|
} else {
|
|
debug_assert!(kw & structs::NS_STYLE_TEXT_EMPHASIS_POSITION_UNDER != 0);
|
|
TextEmphasisHorizontalWritingModeValue::Under
|
|
};
|
|
TextEmphasisPosition(horiz, vert)
|
|
}
|
|
}
|
|
|
|
impl Parse for TextEmphasisPosition {
|
|
fn parse<'i, 't>(
|
|
_context: &ParserContext,
|
|
input: &mut Parser<'i, 't>,
|
|
) -> Result<Self, ParseError<'i>> {
|
|
if let Ok(horizontal) =
|
|
input.try(|input| TextEmphasisHorizontalWritingModeValue::parse(input))
|
|
{
|
|
let vertical = TextEmphasisVerticalWritingModeValue::parse(input)?;
|
|
Ok(TextEmphasisPosition(horizontal, vertical))
|
|
} else {
|
|
let vertical = TextEmphasisVerticalWritingModeValue::parse(input)?;
|
|
let horizontal = TextEmphasisHorizontalWritingModeValue::parse(input)?;
|
|
Ok(TextEmphasisPosition(horizontal, vertical))
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "gecko")]
|
|
impl From<u8> for TextEmphasisPosition {
|
|
fn from(bits: u8) -> Self {
|
|
TextEmphasisPosition::from_gecko_keyword(bits as u32)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "gecko")]
|
|
impl From<TextEmphasisPosition> for u8 {
|
|
fn from(v: TextEmphasisPosition) -> u8 {
|
|
use crate::gecko_bindings::structs;
|
|
|
|
let mut result = match v.0 {
|
|
TextEmphasisHorizontalWritingModeValue::Over => {
|
|
structs::NS_STYLE_TEXT_EMPHASIS_POSITION_OVER
|
|
},
|
|
TextEmphasisHorizontalWritingModeValue::Under => {
|
|
structs::NS_STYLE_TEXT_EMPHASIS_POSITION_UNDER
|
|
},
|
|
};
|
|
match v.1 {
|
|
TextEmphasisVerticalWritingModeValue::Right => {
|
|
result |= structs::NS_STYLE_TEXT_EMPHASIS_POSITION_RIGHT;
|
|
},
|
|
TextEmphasisVerticalWritingModeValue::Left => {
|
|
result |= structs::NS_STYLE_TEXT_EMPHASIS_POSITION_LEFT;
|
|
},
|
|
};
|
|
result as u8
|
|
}
|
|
}
|
|
|
|
/// 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 `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,
|
|
}
|