script/dom/execcommand/commands/
fontsize.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use app_units::Au;
6use js::context::JSContext;
7use servo_config::pref;
8use style::attr::parse_integer;
9use style::values::computed::CSSPixelLength;
10use style::values::specified::FontSize;
11
12use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
13use crate::dom::bindings::str::DOMString;
14use crate::dom::document::Document;
15use crate::dom::execcommand::basecommand::CommandName;
16use crate::dom::selection::Selection;
17
18/// <https://w3c.github.io/editing/docs/execCommand/#legacy-font-size-for>
19pub(crate) fn legacy_font_size_for(pixel_size: f32, document: &Document) -> DOMString {
20    let quirks_mode = document.quirks_mode();
21    let base_size = CSSPixelLength::from(Au::from_f32_px(pref!(fonts_default_size) as f32));
22    // Step 1. Let returned size be 1.
23    let mut returned_size = 1;
24    // Step 2. While returned size is less than 7:
25    while returned_size < 7 {
26        // Step 2.1. Let lower bound be the resolved value of "font-size" in pixels
27        // of a font element whose size attribute is set to returned size.
28        let FontSize::Keyword(lower_keyword) = FontSize::from_html_size(returned_size) else {
29            unreachable!("Always computed as keyword");
30        };
31        let lower_bound = lower_keyword
32            .kw
33            .to_length_without_context(quirks_mode, base_size);
34        // Step 2.2. Let upper bound be the resolved value of "font-size" in pixels
35        // of a font element whose size attribute is set to one plus returned size.
36        let FontSize::Keyword(upper_keyword) = FontSize::from_html_size(returned_size + 1) else {
37            unreachable!("Always computed as keyword");
38        };
39        let upper_bound = upper_keyword
40            .kw
41            .to_length_without_context(quirks_mode, base_size);
42        // Step 2.3. Let average be the average of upper bound and lower bound.
43        let average = (lower_bound.0.px() + upper_bound.0.px()) / 2.0;
44        // Step 2.4. If pixel size is less than average,
45        // return the one-code unit string consisting of the digit returned size.
46        //
47        // We return once at the end of this method
48        if pixel_size < average {
49            break;
50        }
51        // Step 2.5. Add one to returned size.
52        returned_size += 1;
53    }
54    // Step 3. Return "7".
55    returned_size.to_string().into()
56}
57
58enum ParsingMode {
59    RelativePlus,
60    RelativeMinus,
61    Absolute,
62}
63
64/// <https://w3c.github.io/editing/docs/execCommand/#the-fontsize-command>
65pub(crate) fn execute_fontsize_command(
66    cx: &mut JSContext,
67    document: &Document,
68    selection: &Selection,
69    value: DOMString,
70) -> bool {
71    // Step 1. Strip leading and trailing whitespace from value.
72    let value = {
73        let mut value = value;
74        value.strip_leading_and_trailing_ascii_whitespace();
75        value
76    };
77    // Step 2. If value is not a valid floating point number,
78    // and would not be a valid floating point number if a single leading "+" character were stripped, return false.
79    //
80    // The second part is checked in conjunction with step 3 for optimization
81    if !value.is_valid_floating_point_number_string() {
82        return false;
83    }
84    // Step 3. If the first character of value is "+",
85    // delete the character and let mode be "relative-plus".
86    let (value, mode) = if value.starts_with('+') {
87        let stripped_plus = &value.str()[1..];
88        // FIXME: This is not optimal, but not sure how to both delete the first character and check here
89        if !DOMString::from(stripped_plus).is_valid_floating_point_number_string() {
90            return false;
91        }
92        (stripped_plus.to_owned(), ParsingMode::RelativePlus)
93    } else if value.starts_with('-') {
94        // Step 4. Otherwise, if the first character of value is "-",
95        // delete the character and let mode be "relative-minus".
96        (value.str()[1..].to_owned(), ParsingMode::RelativeMinus)
97    } else {
98        // Step 5. Otherwise, let mode be "absolute".
99        (value.into(), ParsingMode::Absolute)
100    };
101    // Step 6. Apply the rules for parsing non-negative integers to value, and let number be the result.
102    let number = parse_integer(value.chars()).expect("Already validated floating number before");
103    let number = match mode {
104        // Step 7. If mode is "relative-plus", add three to number.
105        ParsingMode::RelativePlus => number + 3,
106        // Step 8. If mode is "relative-minus", negate number, then add three to it.
107        ParsingMode::RelativeMinus => (-number) + 3,
108        ParsingMode::Absolute => number,
109    };
110    // Step 9. If number is less than one, let number equal 1.
111    // Step 10. If number is greater than seven, let number equal 7.
112    let number = number.clamp(1, 7) as u32;
113    // Step 11. Set value to the string here corresponding to number:
114    let value = font_size_to_css_font(&number);
115    // Step 12. Set the selection's value to value.
116    selection.set_the_selection_value(cx, Some(value.into()), CommandName::FontSize, document);
117    // Step 13. Return true.
118    true
119}
120
121/// <https://w3c.github.io/editing/docs/execCommand/#the-fontsize-command>
122pub(crate) fn value_for_fontsize_command(
123    cx: &mut JSContext,
124    document: &Document,
125) -> Option<DOMString> {
126    // Step 1. If the active range is null, return the empty string.
127    let selection = document.GetSelection(cx)?;
128    let active_range = selection.active_range()?;
129    // Step 2. Let pixel size be the effective command value of the first formattable
130    // node that is effectively contained in the active range, or if there is no such node,
131    // the effective command value of the active range's start node,
132    // in either case interpreted as a number of pixels.
133    let command_value = active_range
134        .first_formattable_contained_node()
135        .unwrap_or_else(|| active_range.start_container())
136        .effective_command_value(&CommandName::FontSize)?;
137    // Step 3. Return the legacy font size for pixel size.
138    maybe_normalize_pixels(&command_value, document)
139}
140
141/// Only in the case we have resolved to actual pixels, we need to
142/// do its conversion. In other cases, we already have the relevant
143/// font size or corresponding css value. This avoids expensive
144/// conversions of pixels to other values.
145pub(crate) fn maybe_normalize_pixels(
146    command_value: &DOMString,
147    document: &Document,
148) -> Option<DOMString> {
149    if command_value.ends_with_str("px") {
150        command_value.str()[0..command_value.len() - 2]
151            .parse::<f32>()
152            .ok()
153            .map(|value| legacy_font_size_for(value, document))
154    } else {
155        Some(css_font_to_font_size(&command_value.str()).into())
156    }
157}
158
159fn css_font_to_font_size(str_: &str) -> &str {
160    match str_ {
161        "x-small" => "1",
162        "small" => "2",
163        "medium" => "3",
164        "large" => "4",
165        "x-large" => "5",
166        "xx-large" => "6",
167        "xxx-large" => "7",
168        _ => str_,
169    }
170}
171
172pub(crate) fn font_size_to_css_font(value: &u32) -> &str {
173    match value {
174        1 => "x-small",
175        2 => "small",
176        3 => "medium",
177        4 => "large",
178        5 => "x-large",
179        6 => "xx-large",
180        7 => "xxx-large",
181        _ => unreachable!(),
182    }
183}
184
185/// Handles fontsize command part of
186/// <https://w3c.github.io/editing/docs/execCommand/#loosely-equivalent-values>
187pub(crate) fn font_size_loosely_equivalent(first: &DOMString, second: &DOMString) -> bool {
188    // > one of the quantities is one of "x-small", "small", "medium", "large", "x-large", "xx-large", or "xxx-large";
189    // > and the other quantity is the resolved value of "font-size" on a font element whose size attribute
190    // > has the corresponding value set ("1" through "7" respectively).
191    css_font_to_font_size(&first.str()) == second || first == css_font_to_font_size(&second.str())
192}