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