script/dom/execcommand/contenteditable/
element.rs1use html5ever::{LocalName, local_name, ns};
6use js::context::JSContext;
7use script_bindings::inheritance::Castable;
8use style::attr::AttrValue;
9use style::properties::{LonghandId, PropertyDeclaration, PropertyDeclarationId, ShorthandId};
10use style::values::specified::TextDecorationLine;
11use style::values::specified::box_::DisplayOutside;
12
13use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
14use crate::dom::bindings::inheritance::{ElementTypeId, HTMLElementTypeId, NodeTypeId};
15use crate::dom::bindings::root::DomRoot;
16use crate::dom::bindings::str::DOMString;
17use crate::dom::element::Element;
18use crate::dom::execcommand::basecommand::{CommandName, CssPropertyName};
19use crate::dom::execcommand::commands::fontsize::font_size_to_css_font;
20use crate::dom::execcommand::contenteditable::node::move_preserving_ranges;
21use crate::dom::html::htmlanchorelement::HTMLAnchorElement;
22use crate::dom::html::htmlfontelement::HTMLFontElement;
23use crate::dom::node::node::{Node, NodeTraits};
24
25impl Element {
26 pub(crate) fn resolved_display_value(&self) -> Option<DisplayOutside> {
27 self.style().map(|style| style.get_box().display.outside())
28 }
29
30 pub(crate) fn specified_command_value(&self, command: &CommandName) -> Option<DOMString> {
32 match command {
33 CommandName::BackColor | CommandName::HiliteColor
35 if self
36 .resolved_display_value()
37 .is_none_or(|display| display != DisplayOutside::Inline) =>
38 {
39 return None;
40 },
41 CommandName::CreateLink | CommandName::Unlink => {
43 if let Some(anchor) = self.downcast::<HTMLAnchorElement>() {
46 return anchor
47 .upcast::<Element>()
48 .get_attribute_string_value(&local_name!("href"))
49 .map(|value| value.into());
50 }
51
52 return None;
54 },
55 CommandName::Subscript | CommandName::Superscript => {
57 if matches!(*self.local_name(), local_name!("sup")) {
59 return Some("superscript".into());
60 }
61 if matches!(*self.local_name(), local_name!("sub")) {
63 return Some("subscript".into());
64 }
65 return None;
67 },
68 CommandName::Strikethrough => {
69 if let Some(value) = CssPropertyName::TextDecorationLine.value_set_for_style(self) {
71 return value
74 .contains("line-through")
75 .then_some("line-through".into());
76 }
77 if matches!(*self.local_name(), local_name!("s") | local_name!("strike")) {
79 return Some("line-through".into());
80 }
81 },
82 CommandName::Underline => {
83 if let Some(value) = CssPropertyName::TextDecorationLine.value_set_for_style(self) {
85 return value.contains("underline").then_some("underline".into());
88 }
89 if *self.local_name() == local_name!("u") {
91 return Some("underline".into());
92 }
93 },
94 _ => {},
95 };
96 let property = command.relevant_css_property()?;
99 if let Some(value) = property.value_set_for_style(self) {
102 return Some(value);
103 }
104 if self.is::<HTMLFontElement>() &&
107 let Some(font_size) = self
108 .with_attribute(&ns!(), &local_name!("size"), |attribute| {
109 if let AttrValue::UInt(_, value) = *attribute.value() {
110 Some(value)
111 } else {
112 None
113 }
114 })
115 .flatten()
116 {
117 return Some(font_size_to_css_font(&font_size).into());
118 }
119
120 let element_name = self.local_name();
123 match property {
124 CssPropertyName::FontWeight
125 if element_name == &local_name!("b") || element_name == &local_name!("strong") =>
126 {
127 Some("bold".into())
128 },
129 CssPropertyName::FontStyle
130 if element_name == &local_name!("i") || element_name == &local_name!("em") =>
131 {
132 Some("italic".into())
133 },
134 _ => None,
136 }
137 }
138
139 pub(crate) fn is_modifiable_element(&self) -> bool {
141 let attrs = self.attrs().borrow();
142 let mut attrs = attrs.iter();
143 let type_id = self.upcast::<Node>().type_id();
144
145 if matches!(
148 type_id,
149 NodeTypeId::Element(ElementTypeId::HTMLElement(
150 HTMLElementTypeId::HTMLSpanElement,
151 ))
152 ) || matches!(
153 *self.local_name(),
154 local_name!("b") |
155 local_name!("em") |
156 local_name!("i") |
157 local_name!("s") |
158 local_name!("strike") |
159 local_name!("strong") |
160 local_name!("sub") |
161 local_name!("sup") |
162 local_name!("u")
163 ) {
164 return attrs.all(|attr| attr.local_name() == &local_name!("style"));
165 }
166
167 if matches!(
169 type_id,
170 NodeTypeId::Element(ElementTypeId::HTMLElement(
171 HTMLElementTypeId::HTMLFontElement,
172 ))
173 ) {
174 return attrs.all(|attr| {
175 matches!(
176 *attr.local_name(),
177 local_name!("style") |
178 local_name!("color") |
179 local_name!("face") |
180 local_name!("size")
181 )
182 });
183 }
184
185 if matches!(
187 type_id,
188 NodeTypeId::Element(ElementTypeId::HTMLElement(
189 HTMLElementTypeId::HTMLAnchorElement,
190 ))
191 ) {
192 return attrs.all(|attr| {
193 matches!(
194 *attr.local_name(),
195 local_name!("style") | local_name!("href")
196 )
197 });
198 }
199
200 false
201 }
202
203 pub(crate) fn is_indentation_element(&self) -> bool {
205 if !matches!(
207 *self.local_name(),
208 local_name!("blockquote") | local_name!("div")
209 ) {
210 return false;
211 }
212
213 let style_attribute = self.style_attribute().borrow();
215 let Some(declarations) = style_attribute.as_ref() else {
216 return false;
217 };
218 let document = self.owner_document();
219 let shared_lock = document.style_shared_author_lock();
220 let read_lock = shared_lock.read();
221 let style = declarations.read_with(&read_lock);
222
223 ShorthandId::Margin
224 .longhands()
225 .any(|longhand| style.contains(PropertyDeclarationId::Longhand(longhand)))
226 }
227
228 pub(crate) fn has_empty_style_attribute(&self) -> bool {
229 let style_attribute = self.style_attribute().borrow();
230 style_attribute.as_ref().is_some_and(|declarations| {
231 let document = self.owner_document();
232 let shared_lock = document.style_shared_author_lock();
233 let read_lock = shared_lock.read();
234 let style = declarations.read_with(&read_lock);
235
236 style.is_empty()
237 })
238 }
239
240 pub(crate) fn is_non_list_single_line_container(&self) -> bool {
242 matches!(
245 *self.local_name(),
246 local_name!("address") |
247 local_name!("div") |
248 local_name!("h1") |
249 local_name!("h2") |
250 local_name!("h3") |
251 local_name!("h4") |
252 local_name!("h5") |
253 local_name!("h6") |
254 local_name!("listing") |
255 local_name!("p") |
256 local_name!("pre") |
257 local_name!("xmp")
258 )
259 }
260
261 pub(crate) fn is_simple_modifiable_element(&self) -> bool {
263 let attrs = self.attrs().borrow();
264 let attr_count = attrs.len();
265 let type_id = self.upcast::<Node>().type_id();
266
267 if matches!(
268 type_id,
269 NodeTypeId::Element(ElementTypeId::HTMLElement(
270 HTMLElementTypeId::HTMLAnchorElement,
271 )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
272 HTMLElementTypeId::HTMLFontElement,
273 )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
274 HTMLElementTypeId::HTMLSpanElement,
275 ))
276 ) || matches!(
277 *self.local_name(),
278 local_name!("b") |
279 local_name!("em") |
280 local_name!("i") |
281 local_name!("s") |
282 local_name!("strike") |
283 local_name!("strong") |
284 local_name!("sub") |
285 local_name!("sup") |
286 local_name!("u")
287 ) {
288 if attr_count == 0 {
290 return true;
291 }
292
293 if attr_count == 1 &&
297 attrs.first().expect("Size is 1").local_name() == &local_name!("style") &&
298 self.has_empty_style_attribute()
299 {
300 return true;
301 }
302 }
303
304 if attr_count != 1 {
305 return false;
306 }
307
308 let first_attr = attrs.first().expect("Size is 1");
309 let only_attribute = first_attr.local_name();
310
311 if matches!(
313 type_id,
314 NodeTypeId::Element(ElementTypeId::HTMLElement(
315 HTMLElementTypeId::HTMLAnchorElement,
316 ))
317 ) {
318 return only_attribute == &local_name!("href");
319 }
320
321 if matches!(
323 type_id,
324 NodeTypeId::Element(ElementTypeId::HTMLElement(
325 HTMLElementTypeId::HTMLFontElement,
326 ))
327 ) {
328 return only_attribute == &local_name!("color") ||
329 only_attribute == &local_name!("face") ||
330 only_attribute == &local_name!("size");
331 }
332
333 if only_attribute != &local_name!("style") {
334 return false;
335 }
336 let style_attribute = self.style_attribute().borrow();
337 let Some(declarations) = style_attribute.as_ref() else {
338 return false;
339 };
340 let document = self.owner_document();
341 let shared_lock = document.style_shared_author_lock();
342 let read_lock = shared_lock.read();
343 let style = declarations.read_with(&read_lock);
344
345 if matches!(*self.local_name(), local_name!("b") | local_name!("strong")) {
349 return style.len() == 1 &&
350 style.contains(PropertyDeclarationId::Longhand(LonghandId::FontWeight));
351 }
352
353 if matches!(*self.local_name(), local_name!("i") | local_name!("em")) {
357 return style.len() == 1 &&
358 style.contains(PropertyDeclarationId::Longhand(LonghandId::FontStyle));
359 }
360
361 let a_font_or_span = matches!(
362 type_id,
363 NodeTypeId::Element(ElementTypeId::HTMLElement(
364 HTMLElementTypeId::HTMLAnchorElement,
365 )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
366 HTMLElementTypeId::HTMLFontElement,
367 )) | NodeTypeId::Element(ElementTypeId::HTMLElement(
368 HTMLElementTypeId::HTMLSpanElement,
369 ))
370 );
371 let s_strike_or_u = matches!(
372 *self.local_name(),
373 local_name!("s") | local_name!("strike") | local_name!("u")
374 );
375 if a_font_or_span || s_strike_or_u {
376 if style.len() == ShorthandId::TextDecoration.longhands().count() &&
379 style
380 .shorthand_to_css(ShorthandId::TextDecoration, &mut String::new())
381 .is_ok()
382 {
383 if let Some((text_decoration, _)) = style.get(PropertyDeclarationId::Longhand(
384 LonghandId::TextDecorationLine,
385 )) {
386 return matches!(
391 text_decoration,
392 PropertyDeclaration::TextDecorationLine(
393 TextDecorationLine::LINE_THROUGH |
394 TextDecorationLine::UNDERLINE |
395 TextDecorationLine::OVERLINE |
396 TextDecorationLine::NONE
397 )
398 );
399 }
400 } else if a_font_or_span {
401 return style.len() == 1;
405 }
406 }
407
408 false
409 }
410
411 pub(crate) fn is_simple_indentation_element(&self) -> bool {
412 if !self.is_indentation_element() {
414 return false;
415 }
416
417 let attrs = self.attrs().borrow();
419 let mut attrs = attrs.iter();
420 attrs.all(|attr| {
421 if matches!(*attr.local_name(), local_name!("style")) {
423 let style_attribute = self.style_attribute().borrow();
424 let Some(declarations) = style_attribute.as_ref() else {
425 return false;
426 };
427 let document = self.owner_document();
428 let shared_lock = document.style_shared_author_lock();
429 let read_lock = shared_lock.read();
430 let style = declarations.read_with(&read_lock);
431
432 let properties: Vec<_> = ShorthandId::Margin
433 .longhands()
434 .chain(ShorthandId::Border.longhands())
435 .chain(ShorthandId::Padding.longhands())
436 .collect();
437 return style.declarations().iter().all(|declaration| {
438 declaration
439 .id()
440 .as_longhand()
441 .is_some_and(|longhand| properties.contains(&longhand))
442 });
443 }
444
445 if matches!(*attr.local_name(), local_name!("dir")) {
447 return true;
448 }
449
450 false
451 })
452 }
453
454 pub(crate) fn set_the_tag_name(&self, cx: &mut JSContext, new_name: &str) -> DomRoot<Node> {
456 if self.local_name() == &LocalName::from(new_name) {
458 return DomRoot::upcast(DomRoot::from_ref(self));
459 }
460 let node = self.upcast::<Node>();
462 let Some(parent) = node.GetParentNode() else {
463 return DomRoot::upcast(DomRoot::from_ref(self));
464 };
465 let document = node.owner_document();
467 let replacement = document.create_element(cx, new_name);
468 let replacement_node = replacement.upcast::<Node>();
469 if parent
471 .InsertBefore(cx, replacement_node, Some(node))
472 .is_err()
473 {
474 unreachable!("Must always be able to insert");
475 }
476 self.copy_all_attributes_to_other_element(cx, &replacement);
478 for child in node.children() {
480 move_preserving_ranges(cx, &child, |cx| replacement_node.AppendChild(cx, &child));
481 }
482 node.remove_self(cx);
484 DomRoot::upcast(replacement)
486 }
487}