diff options
Diffstat (limited to 'components/layout_2020/generated_content.rs')
-rw-r--r-- | components/layout_2020/generated_content.rs | 651 |
1 files changed, 651 insertions, 0 deletions
diff --git a/components/layout_2020/generated_content.rs b/components/layout_2020/generated_content.rs new file mode 100644 index 00000000000..e1ebc570a62 --- /dev/null +++ b/components/layout_2020/generated_content.rs @@ -0,0 +1,651 @@ +/* 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/. */ + +//! The generated content assignment phase. +//! +//! This phase handles CSS counters, quotes, and ordered lists per CSS § 12.3-12.5. It cannot be +//! done in parallel and is therefore a sequential pass that runs on as little of the flow tree +//! as possible. + +use crate::context::{with_thread_local_font_context, LayoutContext}; +use crate::display_list::items::OpaqueNode; +use crate::flow::{Flow, FlowFlags, GetBaseFlow, ImmutableFlowUtils}; +use crate::fragment::{ + Fragment, GeneratedContentInfo, SpecificFragmentInfo, UnscannedTextFragmentInfo, +}; +use crate::text::TextRunScanner; +use crate::traversal::InorderFlowTraversal; +use script_layout_interface::wrapper_traits::PseudoElementType; +use smallvec::SmallVec; +use std::collections::{HashMap, LinkedList}; +use style::computed_values::display::T as Display; +use style::computed_values::list_style_type::T as ListStyleType; +use style::properties::ComputedValues; +use style::selector_parser::RestyleDamage; +use style::servo::restyle_damage::ServoRestyleDamage; +use style::values::generics::counters::ContentItem; + +// Decimal styles per CSS-COUNTER-STYLES § 6.1: +static DECIMAL: [char; 10] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; +// TODO(pcwalton): `decimal-leading-zero` +static ARABIC_INDIC: [char; 10] = ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩']; +// TODO(pcwalton): `armenian`, `upper-armenian`, `lower-armenian` +static BENGALI: [char; 10] = [ + '০', '১', '২', '৩', '৪', '৫', '৬', '৭', '৮', '৯', +]; +static CAMBODIAN: [char; 10] = [ + '០', '១', '២', '៣', '៤', '៥', '៦', '៧', '៨', '៩', +]; +// TODO(pcwalton): Suffix for CJK decimal. +static CJK_DECIMAL: [char; 10] = [ + '〇', '一', '二', '三', '四', '五', '六', '七', '八', '九', +]; +static DEVANAGARI: [char; 10] = [ + '०', '१', '२', '३', '४', '५', '६', '७', '८', '९', +]; +// TODO(pcwalton): `georgian` +static GUJARATI: [char; 10] = [ + '૦', '૧', '૨', '૩', '૪', '૫', '૬', '૭', '૮', '૯', +]; +static GURMUKHI: [char; 10] = [ + '੦', '੧', '੨', '੩', '੪', '੫', '੬', '੭', '੮', '੯', +]; +// TODO(pcwalton): `hebrew` +static KANNADA: [char; 10] = [ + '೦', '೧', '೨', '೩', '೪', '೫', '೬', '೭', '೮', '೯', +]; +static LAO: [char; 10] = [ + '໐', '໑', '໒', '໓', '໔', '໕', '໖', '໗', '໘', '໙', +]; +static MALAYALAM: [char; 10] = [ + '൦', '൧', '൨', '൩', '൪', '൫', '൬', '൭', '൮', '൯', +]; +static MONGOLIAN: [char; 10] = [ + '᠐', '᠑', '᠒', '᠓', '᠔', '᠕', '᠖', '᠗', '᠘', '᠙', +]; +static MYANMAR: [char; 10] = [ + '၀', '၁', '၂', '၃', '၄', '၅', '၆', '၇', '၈', '၉', +]; +static ORIYA: [char; 10] = [ + '୦', '୧', '୨', '୩', '୪', '୫', '୬', '୭', '୮', '୯', +]; +static PERSIAN: [char; 10] = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹']; +// TODO(pcwalton): `lower-roman`, `upper-roman` +static TELUGU: [char; 10] = [ + '౦', '౧', '౨', '౩', '౪', '౫', '౬', '౭', '౮', '౯', +]; +static THAI: [char; 10] = [ + '๐', '๑', '๒', '๓', '๔', '๕', '๖', '๗', '๘', '๙', +]; +static TIBETAN: [char; 10] = [ + '༠', '༡', '༢', '༣', '༤', '༥', '༦', '༧', '༨', '༩', +]; + +// Alphabetic styles per CSS-COUNTER-STYLES § 6.2: +static LOWER_ALPHA: [char; 26] = [ + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', + 't', 'u', 'v', 'w', 'x', 'y', 'z', +]; +static UPPER_ALPHA: [char; 26] = [ + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', + 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', +]; +static CJK_EARTHLY_BRANCH: [char; 12] = [ + '子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥', +]; +static CJK_HEAVENLY_STEM: [char; 10] = [ + '甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸', +]; +static LOWER_GREEK: [char; 24] = [ + 'α', 'β', 'γ', 'δ', 'ε', 'ζ', 'η', 'θ', 'ι', 'κ', 'λ', 'μ', 'ν', 'ξ', 'ο', 'π', + 'ρ', 'σ', 'τ', 'υ', 'φ', 'χ', 'ψ', 'ω', +]; +static HIRAGANA: [char; 48] = [ + 'あ', 'い', 'う', 'え', 'お', 'か', 'き', 'く', 'け', 'こ', 'さ', 'し', 'す', + 'せ', 'そ', 'た', 'ち', 'つ', 'て', 'と', 'な', 'に', 'ぬ', 'ね', 'の', 'は', + 'ひ', 'ふ', 'へ', 'ほ', 'ま', 'み', 'む', 'め', 'も', 'や', 'ゆ', 'よ', 'ら', + 'り', 'る', 'れ', 'ろ', 'わ', 'ゐ', 'ゑ', 'を', 'ん', +]; +static HIRAGANA_IROHA: [char; 47] = [ + 'い', 'ろ', 'は', 'に', 'ほ', 'へ', 'と', 'ち', 'り', 'ぬ', 'る', 'を', 'わ', + 'か', 'よ', 'た', 'れ', 'そ', 'つ', 'ね', 'な', 'ら', 'む', 'う', 'ゐ', 'の', + 'お', 'く', 'や', 'ま', 'け', 'ふ', 'こ', 'え', 'て', 'あ', 'さ', 'き', 'ゆ', + 'め', 'み', 'し', 'ゑ', 'ひ', 'も', 'せ', 'す', +]; +static KATAKANA: [char; 48] = [ + 'ア', 'イ', 'ウ', 'エ', 'オ', 'カ', 'キ', 'ク', 'ケ', 'コ', 'サ', 'シ', 'ス', + 'セ', 'ソ', 'タ', 'チ', 'ツ', 'テ', 'ト', 'ナ', 'ニ', 'ヌ', 'ネ', 'ノ', 'ハ', + 'ヒ', 'フ', 'ヘ', 'ホ', 'マ', 'ミ', 'ム', 'メ', 'モ', 'ヤ', 'ユ', 'ヨ', 'ラ', + 'リ', 'ル', 'レ', 'ロ', 'ワ', 'ヰ', 'ヱ', 'ヲ', 'ン', +]; +static KATAKANA_IROHA: [char; 47] = [ + 'イ', 'ロ', 'ハ', 'ニ', 'ホ', 'ヘ', 'ト', 'チ', 'リ', 'ヌ', 'ル', 'ヲ', 'ワ', + 'カ', 'ヨ', 'タ', 'レ', 'ソ', 'ツ', 'ネ', 'ナ', 'ラ', 'ム', 'ウ', 'ヰ', 'ノ', + 'オ', 'ク', 'ヤ', 'マ', 'ケ', 'フ', 'コ', 'エ', 'テ', 'ア', 'サ', 'キ', 'ユ', + 'メ', 'ミ', 'シ', 'ヱ', 'ヒ', 'モ', 'セ', 'ス', +]; + +/// The generated content resolution traversal. +pub struct ResolveGeneratedContent<'a> { + /// The layout context. + layout_context: &'a LayoutContext<'a>, + /// The counter representing an ordered list item. + list_item: Counter, + /// Named CSS counters. + counters: HashMap<String, Counter>, + /// The level of quote nesting. + quote: u32, +} + +impl<'a> ResolveGeneratedContent<'a> { + /// Creates a new generated content resolution traversal. + pub fn new(layout_context: &'a LayoutContext) -> ResolveGeneratedContent<'a> { + ResolveGeneratedContent { + layout_context: layout_context, + list_item: Counter::new(), + counters: HashMap::new(), + quote: 0, + } + } +} + +impl<'a> InorderFlowTraversal for ResolveGeneratedContent<'a> { + #[inline] + fn process(&mut self, flow: &mut dyn Flow, level: u32) { + let mut mutator = ResolveGeneratedContentFragmentMutator { + traversal: self, + level: level, + is_block: flow.is_block_like(), + incremented: false, + }; + flow.mutate_fragments(&mut |fragment| mutator.mutate_fragment(fragment)) + } + + #[inline] + fn should_process_subtree(&mut self, flow: &mut dyn Flow) -> bool { + flow.base() + .restyle_damage + .intersects(ServoRestyleDamage::RESOLVE_GENERATED_CONTENT) || + flow.base().flags.intersects( + FlowFlags::AFFECTS_COUNTERS | FlowFlags::HAS_COUNTER_AFFECTING_CHILDREN, + ) + } +} + +/// The object that mutates the generated content fragments. +struct ResolveGeneratedContentFragmentMutator<'a, 'b: 'a> { + /// The traversal. + traversal: &'a mut ResolveGeneratedContent<'b>, + /// The level we're at in the flow tree. + level: u32, + /// Whether this flow is a block flow. + is_block: bool, + /// Whether we've incremented the counter yet. + incremented: bool, +} + +impl<'a, 'b> ResolveGeneratedContentFragmentMutator<'a, 'b> { + fn mutate_fragment(&mut self, fragment: &mut Fragment) { + // We only reset and/or increment counters once per flow. This avoids double-incrementing + // counters on list items (once for the main fragment and once for the marker). + if !self.incremented { + self.reset_and_increment_counters_as_necessary(fragment); + } + + let mut list_style_type = fragment.style().get_list().list_style_type; + if fragment.style().get_box().display != Display::ListItem { + list_style_type = ListStyleType::None + } + + let mut new_info = None; + { + let info = + if let SpecificFragmentInfo::GeneratedContent(ref mut info) = fragment.specific { + info + } else { + return; + }; + + match **info { + GeneratedContentInfo::ListItem => { + new_info = self.traversal.list_item.render( + self.traversal.layout_context, + fragment.node, + fragment.pseudo.clone(), + fragment.style.clone(), + list_style_type, + RenderingMode::Suffix(".\u{00a0}"), + ) + }, + GeneratedContentInfo::Empty | + GeneratedContentInfo::ContentItem(ContentItem::String(_)) => { + // Nothing to do here. + }, + GeneratedContentInfo::ContentItem(ContentItem::Counter( + ref counter_name, + counter_style, + )) => { + let temporary_counter = Counter::new(); + let counter = self + .traversal + .counters + .get(&*counter_name.0) + .unwrap_or(&temporary_counter); + new_info = counter.render( + self.traversal.layout_context, + fragment.node, + fragment.pseudo.clone(), + fragment.style.clone(), + counter_style, + RenderingMode::Plain, + ) + }, + GeneratedContentInfo::ContentItem(ContentItem::Counters( + ref counter_name, + ref separator, + counter_style, + )) => { + let temporary_counter = Counter::new(); + let counter = self + .traversal + .counters + .get(&*counter_name.0) + .unwrap_or(&temporary_counter); + new_info = counter.render( + self.traversal.layout_context, + fragment.node, + fragment.pseudo, + fragment.style.clone(), + counter_style, + RenderingMode::All(&separator), + ); + }, + GeneratedContentInfo::ContentItem(ContentItem::OpenQuote) => { + new_info = render_text( + self.traversal.layout_context, + fragment.node, + fragment.pseudo, + fragment.style.clone(), + self.quote(&*fragment.style, false), + ); + self.traversal.quote += 1 + }, + GeneratedContentInfo::ContentItem(ContentItem::CloseQuote) => { + if self.traversal.quote >= 1 { + self.traversal.quote -= 1 + } + + new_info = render_text( + self.traversal.layout_context, + fragment.node, + fragment.pseudo, + fragment.style.clone(), + self.quote(&*fragment.style, true), + ); + }, + GeneratedContentInfo::ContentItem(ContentItem::NoOpenQuote) => { + self.traversal.quote += 1 + }, + GeneratedContentInfo::ContentItem(ContentItem::NoCloseQuote) => { + if self.traversal.quote >= 1 { + self.traversal.quote -= 1 + } + }, + GeneratedContentInfo::ContentItem(ContentItem::Url(..)) => { + unreachable!("Servo doesn't parse content: url(..) yet") + }, + } + }; + + fragment.specific = match new_info { + Some(new_info) => new_info, + // If the fragment did not generate any content, replace it with a no-op placeholder + // so that it isn't processed again on the next layout. FIXME (mbrubeck): When + // processing an inline flow, this traversal should be allowed to insert or remove + // fragments. Then we can just remove these fragments rather than adding placeholders. + None => SpecificFragmentInfo::GeneratedContent(Box::new(GeneratedContentInfo::Empty)), + }; + } + + fn reset_and_increment_counters_as_necessary(&mut self, fragment: &mut Fragment) { + let mut list_style_type = fragment.style().get_list().list_style_type; + if !self.is_block || fragment.style().get_box().display != Display::ListItem { + list_style_type = ListStyleType::None + } + + match list_style_type { + ListStyleType::Disc | + ListStyleType::None | + ListStyleType::Circle | + ListStyleType::Square | + ListStyleType::DisclosureOpen | + ListStyleType::DisclosureClosed => {}, + _ => self.traversal.list_item.increment(self.level, 1), + } + + // Truncate down counters. + for (_, counter) in &mut self.traversal.counters { + counter.truncate_to_level(self.level); + } + self.traversal.list_item.truncate_to_level(self.level); + + for pair in &*fragment.style().get_counters().counter_reset { + let counter_name = &*pair.name.0; + if let Some(ref mut counter) = self.traversal.counters.get_mut(counter_name) { + counter.reset(self.level, pair.value); + continue; + } + + let mut counter = Counter::new(); + counter.reset(self.level, pair.value); + self.traversal + .counters + .insert(counter_name.to_owned(), counter); + } + + for pair in &*fragment.style().get_counters().counter_increment { + let counter_name = &*pair.name.0; + if let Some(ref mut counter) = self.traversal.counters.get_mut(counter_name) { + counter.increment(self.level, pair.value); + continue; + } + + let mut counter = Counter::new(); + counter.increment(self.level, pair.value); + self.traversal + .counters + .insert(counter_name.to_owned(), counter); + } + + self.incremented = true + } + + fn quote(&self, style: &ComputedValues, close: bool) -> String { + let quotes = &style.get_list().quotes; + if quotes.0.is_empty() { + return String::new(); + } + let pair = if self.traversal.quote as usize >= quotes.0.len() { + quotes.0.last().unwrap() + } else { + "es.0[self.traversal.quote as usize] + }; + if close { + pair.closing.to_string() + } else { + pair.opening.to_string() + } + } +} + +/// A counter per CSS 2.1 § 12.4. +struct Counter { + /// The values at each level. + values: Vec<CounterValue>, +} + +impl Counter { + fn new() -> Counter { + Counter { values: Vec::new() } + } + + fn reset(&mut self, level: u32, value: i32) { + // Do we have an instance of the counter at this level? If so, just mutate it. + if let Some(ref mut existing_value) = self.values.last_mut() { + if level == existing_value.level { + existing_value.value = value; + return; + } + } + + // Otherwise, push a new instance of the counter. + self.values.push(CounterValue { + level: level, + value: value, + }) + } + + fn truncate_to_level(&mut self, level: u32) { + if let Some(position) = self.values.iter().position(|value| value.level > level) { + self.values.truncate(position) + } + } + + fn increment(&mut self, level: u32, amount: i32) { + if let Some(ref mut value) = self.values.last_mut() { + value.value += amount; + return; + } + + self.values.push(CounterValue { + level: level, + value: amount, + }) + } + + fn render( + &self, + layout_context: &LayoutContext, + node: OpaqueNode, + pseudo: PseudoElementType, + style: crate::ServoArc<ComputedValues>, + list_style_type: ListStyleType, + mode: RenderingMode, + ) -> Option<SpecificFragmentInfo> { + let mut string = String::new(); + match mode { + RenderingMode::Plain => { + let value = match self.values.last() { + Some(ref value) => value.value, + None => 0, + }; + push_representation(value, list_style_type, &mut string) + }, + RenderingMode::Suffix(suffix) => { + let value = match self.values.last() { + Some(ref value) => value.value, + None => 0, + }; + push_representation(value, list_style_type, &mut string); + string.push_str(suffix) + }, + RenderingMode::All(separator) => { + let mut first = true; + for value in &self.values { + if !first { + string.push_str(separator) + } + first = false; + push_representation(value.value, list_style_type, &mut string) + } + }, + } + + if string.is_empty() { + None + } else { + render_text(layout_context, node, pseudo, style, string) + } + } +} + +/// How a counter value is to be rendered. +enum RenderingMode<'a> { + /// The innermost counter value is rendered with no extra decoration. + Plain, + /// The innermost counter value is rendered with the given string suffix. + Suffix(&'a str), + /// All values of the counter are rendered with the given separator string between them. + All(&'a str), +} + +/// The value of a counter at a given level. +struct CounterValue { + /// The level of the flow tree that this corresponds to. + level: u32, + /// The value of the counter at this level. + value: i32, +} + +/// Creates fragment info for a literal string. +fn render_text( + layout_context: &LayoutContext, + node: OpaqueNode, + pseudo: PseudoElementType, + style: crate::ServoArc<ComputedValues>, + string: String, +) -> Option<SpecificFragmentInfo> { + let mut fragments = LinkedList::new(); + let info = SpecificFragmentInfo::UnscannedText(Box::new(UnscannedTextFragmentInfo::new( + string.into_boxed_str(), + None, + ))); + fragments.push_back(Fragment::from_opaque_node_and_style( + node, + pseudo, + style.clone(), + style, + RestyleDamage::rebuild_and_reflow(), + info, + )); + // FIXME(pcwalton): This should properly handle multiple marker fragments. This could happen + // due to text run splitting. + let fragments = with_thread_local_font_context(layout_context, |font_context| { + TextRunScanner::new().scan_for_runs(font_context, fragments) + }); + if fragments.is_empty() { + None + } else { + Some(fragments.fragments.into_iter().next().unwrap().specific) + } +} + +/// Appends string that represents the value rendered using the system appropriate for the given +/// `list-style-type` onto the given string. +fn push_representation(value: i32, list_style_type: ListStyleType, accumulator: &mut String) { + match list_style_type { + ListStyleType::None => {}, + ListStyleType::Disc | + ListStyleType::Circle | + ListStyleType::Square | + ListStyleType::DisclosureOpen | + ListStyleType::DisclosureClosed => accumulator.push(static_representation(list_style_type)), + ListStyleType::Decimal => push_numeric_representation(value, &DECIMAL, accumulator), + ListStyleType::ArabicIndic => { + push_numeric_representation(value, &ARABIC_INDIC, accumulator) + }, + ListStyleType::Bengali => push_numeric_representation(value, &BENGALI, accumulator), + ListStyleType::Cambodian | ListStyleType::Khmer => { + push_numeric_representation(value, &CAMBODIAN, accumulator) + }, + ListStyleType::CjkDecimal => push_numeric_representation(value, &CJK_DECIMAL, accumulator), + ListStyleType::Devanagari => push_numeric_representation(value, &DEVANAGARI, accumulator), + ListStyleType::Gujarati => push_numeric_representation(value, &GUJARATI, accumulator), + ListStyleType::Gurmukhi => push_numeric_representation(value, &GURMUKHI, accumulator), + ListStyleType::Kannada => push_numeric_representation(value, &KANNADA, accumulator), + ListStyleType::Lao => push_numeric_representation(value, &LAO, accumulator), + ListStyleType::Malayalam => push_numeric_representation(value, &MALAYALAM, accumulator), + ListStyleType::Mongolian => push_numeric_representation(value, &MONGOLIAN, accumulator), + ListStyleType::Myanmar => push_numeric_representation(value, &MYANMAR, accumulator), + ListStyleType::Oriya => push_numeric_representation(value, &ORIYA, accumulator), + ListStyleType::Persian => push_numeric_representation(value, &PERSIAN, accumulator), + ListStyleType::Telugu => push_numeric_representation(value, &TELUGU, accumulator), + ListStyleType::Thai => push_numeric_representation(value, &THAI, accumulator), + ListStyleType::Tibetan => push_numeric_representation(value, &TIBETAN, accumulator), + ListStyleType::LowerAlpha => { + push_alphabetic_representation(value, &LOWER_ALPHA, accumulator) + }, + ListStyleType::UpperAlpha => { + push_alphabetic_representation(value, &UPPER_ALPHA, accumulator) + }, + ListStyleType::CjkEarthlyBranch => { + push_alphabetic_representation(value, &CJK_EARTHLY_BRANCH, accumulator) + }, + ListStyleType::CjkHeavenlyStem => { + push_alphabetic_representation(value, &CJK_HEAVENLY_STEM, accumulator) + }, + ListStyleType::LowerGreek => { + push_alphabetic_representation(value, &LOWER_GREEK, accumulator) + }, + ListStyleType::Hiragana => push_alphabetic_representation(value, &HIRAGANA, accumulator), + ListStyleType::HiraganaIroha => { + push_alphabetic_representation(value, &HIRAGANA_IROHA, accumulator) + }, + ListStyleType::Katakana => push_alphabetic_representation(value, &KATAKANA, accumulator), + ListStyleType::KatakanaIroha => { + push_alphabetic_representation(value, &KATAKANA_IROHA, accumulator) + }, + } +} + +/// Returns the static character that represents the value rendered using the given list-style, if +/// possible. +pub fn static_representation(list_style_type: ListStyleType) -> char { + match list_style_type { + ListStyleType::Disc => '•', + ListStyleType::Circle => '◦', + ListStyleType::Square => '▪', + ListStyleType::DisclosureOpen => '▾', + ListStyleType::DisclosureClosed => '‣', + _ => panic!("No static representation for this list-style-type!"), + } +} + +/// Pushes the string that represents the value rendered using the given *alphabetic system* onto +/// the accumulator per CSS-COUNTER-STYLES § 3.1.4. +fn push_alphabetic_representation(value: i32, system: &[char], accumulator: &mut String) { + let mut abs_value = handle_negative_value(value, accumulator); + + let mut string: SmallVec<[char; 8]> = SmallVec::new(); + while abs_value != 0 { + // Step 1. + abs_value = abs_value - 1; + // Step 2. + string.push(system[abs_value % system.len()]); + // Step 3. + abs_value = abs_value / system.len(); + } + + accumulator.extend(string.iter().cloned().rev()) +} + +/// Pushes the string that represents the value rendered using the given *numeric system* onto the +/// accumulator per CSS-COUNTER-STYLES § 3.1.5. +fn push_numeric_representation(value: i32, system: &[char], accumulator: &mut String) { + let mut abs_value = handle_negative_value(value, accumulator); + + // Step 1. + if abs_value == 0 { + accumulator.push(system[0]); + return; + } + + // Step 2. + let mut string: SmallVec<[char; 8]> = SmallVec::new(); + while abs_value != 0 { + // Step 2.1. + string.push(system[abs_value % system.len()]); + // Step 2.2. + abs_value = abs_value / system.len(); + } + + // Step 3. + accumulator.extend(string.iter().cloned().rev()) +} + +/// If the system uses a negative sign, handle negative values per CSS-COUNTER-STYLES § 2. +/// +/// Returns the absolute value of the counter. +fn handle_negative_value(value: i32, accumulator: &mut String) -> usize { + // 3. If the counter value is negative and the counter style uses a negative sign, instead + // generate an initial representation using the absolute value of the counter value. + if value < 0 { + // TODO: Support different negative signs using the 'negative' descriptor. + // https://drafts.csswg.org/date/2015-07-16/css-counter-styles/#counter-style-negative + accumulator.push('-'); + value.abs() as usize + } else { + value as usize + } +} |