1188 lines
32 KiB
Rust
1188 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;
|
|
use crate::values::computed::text::TextEmphasisStyle as ComputedTextEmphasisStyle;
|
|
use crate::values::computed::{Context, ToComputedValue};
|
|
use crate::values::generics::NumberOrAuto;
|
|
use crate::values::generics::text::{
|
|
GenericHyphenateLimitChars, GenericInitialLetter, GenericTextDecorationLength, GenericTextIndent,
|
|
};
|
|
use crate::values::specified::length::LengthPercentage;
|
|
use crate::values::specified::{AllowQuirks, Integer, Number};
|
|
use crate::Zero;
|
|
use cssparser::Parser;
|
|
use icu_segmenter::GraphemeClusterSegmenter;
|
|
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 spacing value used by either the `letter-spacing` or `word-spacing` properties.
|
|
#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
|
|
pub enum Spacing {
|
|
/// `normal`
|
|
Normal,
|
|
/// `<value>`
|
|
Value(LengthPercentage),
|
|
}
|
|
|
|
impl Parse for Spacing {
|
|
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(Spacing::Normal);
|
|
}
|
|
LengthPercentage::parse_quirky(context, input, AllowQuirks::Yes).map(Spacing::Value)
|
|
}
|
|
}
|
|
|
|
/// A specified value for the `letter-spacing` property.
|
|
#[derive(Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
|
|
pub struct LetterSpacing(pub Spacing);
|
|
|
|
impl ToComputedValue for LetterSpacing {
|
|
type ComputedValue = computed::LetterSpacing;
|
|
|
|
fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
|
|
use computed::text::GenericLetterSpacing;
|
|
match self.0 {
|
|
Spacing::Normal => GenericLetterSpacing(computed::LengthPercentage::zero()),
|
|
Spacing::Value(ref v) => GenericLetterSpacing(v.to_computed_value(context)),
|
|
}
|
|
}
|
|
|
|
fn from_computed_value(computed: &Self::ComputedValue) -> Self {
|
|
if computed.0.is_zero() {
|
|
return LetterSpacing(Spacing::Normal);
|
|
}
|
|
LetterSpacing(Spacing::Value(ToComputedValue::from_computed_value(&computed.0)))
|
|
}
|
|
}
|
|
|
|
|
|
/// A specified value for the `word-spacing` property.
|
|
#[derive(Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
|
|
pub struct WordSpacing(pub Spacing);
|
|
|
|
impl ToComputedValue for WordSpacing {
|
|
type ComputedValue = computed::WordSpacing;
|
|
|
|
fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
|
|
match self.0 {
|
|
Spacing::Normal => computed::LengthPercentage::zero(),
|
|
Spacing::Value(ref v) => v.to_computed_value(context),
|
|
}
|
|
}
|
|
|
|
fn from_computed_value(computed: &Self::ComputedValue) -> Self {
|
|
WordSpacing(Spacing::Value(ToComputedValue::from_computed_value(computed)))
|
|
}
|
|
}
|
|
|
|
/// 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),
|
|
}
|
|
|
|
/// A value for the `hyphenate-limit-chars` property.
|
|
pub type HyphenateLimitChars = GenericHyphenateLimitChars<Integer>;
|
|
|
|
impl Parse for HyphenateLimitChars {
|
|
fn parse<'i, 't>(
|
|
context: &ParserContext,
|
|
input: &mut Parser<'i, 't>,
|
|
) -> Result<Self, ParseError<'i>> {
|
|
type IntegerOrAuto = NumberOrAuto<Integer>;
|
|
|
|
let total_word_length = IntegerOrAuto::parse(context, input)?;
|
|
let pre_hyphen_length = input.try_parse(|i| IntegerOrAuto::parse(context, i)).unwrap_or(IntegerOrAuto::Auto);
|
|
let post_hyphen_length = input.try_parse(|i| IntegerOrAuto::parse(context, i)).unwrap_or(pre_hyphen_length);
|
|
Ok(Self {
|
|
total_word_length,
|
|
pre_hyphen_length,
|
|
post_hyphen_length,
|
|
})
|
|
}
|
|
}
|
|
|
|
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(Self::normal());
|
|
}
|
|
let size = Number::parse_at_least_one(context, input)?;
|
|
let sink = input
|
|
.try_parse(|i| Integer::parse_positive(context, i))
|
|
.unwrap_or_else(|_| crate::Zero::zero());
|
|
Ok(Self { size, sink })
|
|
}
|
|
}
|
|
|
|
/// 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::values::AtomString),
|
|
}
|
|
|
|
#[derive(
|
|
Clone,
|
|
Debug,
|
|
Eq,
|
|
MallocSizeOf,
|
|
PartialEq,
|
|
SpecifiedValueInfo,
|
|
ToComputedValue,
|
|
ToResolvedValue,
|
|
ToShmem,
|
|
)]
|
|
#[repr(C)]
|
|
/// text-overflow.
|
|
/// When the specified value only has one side, that's the "second"
|
|
/// side, and the sides are logical, so "second" means "end". The
|
|
/// start side is Clip in that case.
|
|
///
|
|
/// When the specified value has two sides, those are our "first"
|
|
/// and "second" sides, and they are physical sides ("left" and
|
|
/// "right").
|
|
pub struct TextOverflow {
|
|
/// First side
|
|
pub first: TextOverflowSide,
|
|
/// Second side
|
|
pub second: TextOverflowSide,
|
|
/// True if the specified value only has one side.
|
|
pub sides_are_logical: bool,
|
|
}
|
|
|
|
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)?;
|
|
Ok(
|
|
if let Ok(second) = input.try_parse(|input| TextOverflowSide::parse(context, input)) {
|
|
Self {
|
|
first,
|
|
second,
|
|
sides_are_logical: false,
|
|
}
|
|
} else {
|
|
Self {
|
|
first: TextOverflowSide::Clip,
|
|
second: first,
|
|
sides_are_logical: true,
|
|
}
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
impl TextOverflow {
|
|
/// Returns the initial `text-overflow` value
|
|
pub fn get_initial_value() -> TextOverflow {
|
|
TextOverflow {
|
|
first: TextOverflowSide::Clip,
|
|
second: TextOverflowSide::Clip,
|
|
sides_are_logical: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ToCss for TextOverflow {
|
|
fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
|
|
where
|
|
W: Write,
|
|
{
|
|
if self.sides_are_logical {
|
|
debug_assert_eq!(self.first, TextOverflowSide::Clip);
|
|
self.second.to_css(dest)?;
|
|
} else {
|
|
self.first.to_css(dest)?;
|
|
dest.write_char(' ')?;
|
|
self.second.to_css(dest)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(
|
|
Clone,
|
|
Copy,
|
|
Debug,
|
|
Eq,
|
|
MallocSizeOf,
|
|
PartialEq,
|
|
Parse,
|
|
Serialize,
|
|
SpecifiedValueInfo,
|
|
ToCss,
|
|
ToComputedValue,
|
|
ToResolvedValue,
|
|
ToShmem,
|
|
)]
|
|
#[cfg_attr(feature = "gecko", css(bitflags(
|
|
single = "none,spelling-error,grammar-error",
|
|
mixed = "underline,overline,line-through,blink",
|
|
)))]
|
|
#[cfg_attr(not(feature = "gecko"), 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;
|
|
/// spelling-error
|
|
const SPELLING_ERROR = 1 << 4;
|
|
/// grammar-error
|
|
const GRAMMAR_ERROR = 1 << 5;
|
|
/// 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 = 1 << 7;
|
|
}
|
|
}
|
|
|
|
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,
|
|
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.
|
|
#[cfg(feature = "gecko")]
|
|
MathAuto,
|
|
}
|
|
|
|
#[derive(
|
|
Clone,
|
|
Copy,
|
|
Debug,
|
|
Eq,
|
|
MallocSizeOf,
|
|
PartialEq,
|
|
Parse,
|
|
Serialize,
|
|
SpecifiedValueInfo,
|
|
ToCss,
|
|
ToComputedValue,
|
|
ToResolvedValue,
|
|
ToShmem,
|
|
)]
|
|
#[cfg_attr(feature = "gecko", css(bitflags(
|
|
single = "none,math-auto",
|
|
mixed = "uppercase,lowercase,capitalize,full-width,full-size-kana",
|
|
validate_mixed = "Self::validate_mixed_flags",
|
|
)))]
|
|
#[cfg_attr(not(feature = "gecko"), css(bitflags(
|
|
single = "none",
|
|
mixed = "uppercase,lowercase,capitalize,full-width,full-size-kana",
|
|
validate_mixed = "Self::validate_mixed_flags",
|
|
)))]
|
|
#[repr(C)]
|
|
/// Specified value for the text-transform property.
|
|
/// (The spec grammar gives
|
|
/// `none | math-auto | [capitalize | uppercase | lowercase] || full-width || full-size-kana`.)
|
|
/// https://drafts.csswg.org/css-text-4/#text-transform-property
|
|
pub struct TextTransform(u8);
|
|
bitflags! {
|
|
impl TextTransform: u8 {
|
|
/// none
|
|
const NONE = 0;
|
|
/// All uppercase.
|
|
const UPPERCASE = 1 << 0;
|
|
/// All lowercase.
|
|
const LOWERCASE = 1 << 1;
|
|
/// Capitalize each word.
|
|
const CAPITALIZE = 1 << 2;
|
|
/// Automatic italicization of math variables.
|
|
const MATH_AUTO = 1 << 3;
|
|
|
|
/// All the case transforms, which are exclusive with each other.
|
|
const CASE_TRANSFORMS = Self::UPPERCASE.0 | Self::LOWERCASE.0 | Self::CAPITALIZE.0 | Self::MATH_AUTO.0;
|
|
|
|
/// full-width
|
|
const FULL_WIDTH = 1 << 4;
|
|
/// full-size-kana
|
|
const FULL_SIZE_KANA = 1 << 5;
|
|
}
|
|
}
|
|
|
|
impl TextTransform {
|
|
/// Returns the initial value of text-transform
|
|
#[inline]
|
|
pub fn none() -> Self {
|
|
Self::NONE
|
|
}
|
|
|
|
/// Returns whether the value is 'none'
|
|
#[inline]
|
|
pub fn is_none(self) -> bool {
|
|
self == Self::NONE
|
|
}
|
|
|
|
fn validate_mixed_flags(&self) -> bool {
|
|
let case = self.intersection(Self::CASE_TRANSFORMS);
|
|
// Case bits are exclusive with each other.
|
|
case.is_empty() || case.bits().is_power_of_two()
|
|
}
|
|
}
|
|
|
|
/// 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,
|
|
Justify,
|
|
End,
|
|
#[parse(aliases = "-webkit-center")]
|
|
MozCenter,
|
|
#[parse(aliases = "-webkit-left")]
|
|
MozLeft,
|
|
#[parse(aliases = "-webkit-right")]
|
|
MozRight,
|
|
}
|
|
|
|
/// 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
|
|
/// https://html.spec.whatwg.org/#tables-2:
|
|
///
|
|
/// 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.
|
|
#[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,
|
|
}
|
|
},
|
|
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.
|
|
///
|
|
/// https://drafts.csswg.org/css-text-decor/#propdef-text-emphasis-style
|
|
#[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(
|
|
single = "auto",
|
|
mixed = "over,under,left,right",
|
|
validate_mixed = "Self::validate_and_simplify"
|
|
))]
|
|
/// Values for text-emphasis-position:
|
|
/// <https://drafts.csswg.org/css-text-decor/#text-emphasis-position-property>
|
|
pub struct TextEmphasisPosition(u8);
|
|
bitflags! {
|
|
impl TextEmphasisPosition: u8 {
|
|
/// Automatically choose mark position based on language.
|
|
const AUTO = 1 << 0;
|
|
/// Draw marks over the text in horizontal writing mode.
|
|
const OVER = 1 << 1;
|
|
/// Draw marks under the text in horizontal writing mode.
|
|
const UNDER = 1 << 2;
|
|
/// Draw marks to the left of the text in vertical writing mode.
|
|
const LEFT = 1 << 3;
|
|
/// Draw marks to the right of the text in vertical writing mode.
|
|
const RIGHT = 1 << 4;
|
|
}
|
|
}
|
|
|
|
impl TextEmphasisPosition {
|
|
fn validate_and_simplify(&mut self) -> bool {
|
|
// Require one but not both of 'over' and 'under'.
|
|
if self.intersects(Self::OVER) == self.intersects(Self::UNDER) {
|
|
return false;
|
|
}
|
|
|
|
// If 'left' is present, 'right' must be absent.
|
|
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,
|
|
// See https://drafts.csswg.org/css-text-3/#valdef-text-justify-distribute
|
|
// and https://github.com/w3c/csswg-drafts/issues/6156 for the alias.
|
|
#[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,
|
|
}
|
|
|
|
#[cfg(feature = "gecko")]
|
|
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?
|
|
///
|
|
/// https://drafts.csswg.org/css-text/#propdef-text-indent
|
|
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
|
|
///
|
|
/// https://drafts.csswg.org/css-text-decor-4/#text-decoration-skip-ink-property
|
|
#[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,
|
|
Parse,
|
|
PartialEq,
|
|
SpecifiedValueInfo,
|
|
ToComputedValue,
|
|
ToResolvedValue,
|
|
ToShmem,
|
|
)]
|
|
#[css(bitflags(
|
|
single = "auto",
|
|
mixed = "from-font,under,left,right",
|
|
validate_mixed = "Self::validate_mixed_flags",
|
|
))]
|
|
#[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 ]`.)
|
|
/// https://drafts.csswg.org/css-text-decor-4/#text-underline-position-property
|
|
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;
|
|
}
|
|
}
|
|
|
|
impl TextUnderlinePosition {
|
|
fn validate_mixed_flags(&self) -> bool {
|
|
if self.contains(Self::LEFT | Self::RIGHT) {
|
|
// left and right can't be mixed together.
|
|
return false;
|
|
}
|
|
if self.contains(Self::FROM_FONT | Self::UNDER) {
|
|
// from-font and under can't be mixed together either.
|
|
return false;
|
|
}
|
|
true
|
|
}
|
|
}
|
|
|
|
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"])
|
|
}
|
|
}
|