1use js::context::JSContext;
6use script_bindings::inheritance::Castable;
7use style::attr::parse_legacy_color;
8use style::color::ColorFlags;
9use style::properties::PropertyDeclarationId;
10use style::properties::generated::{LonghandId, ShorthandId};
11use style::values::specified::text::TextDecorationLine;
12use style_traits::ToCss;
13
14use crate::dom::bindings::codegen::Bindings::CSSStyleDeclarationBinding::CSSStyleDeclarationMethods;
15use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
16use crate::dom::bindings::codegen::Bindings::HTMLElementBinding::HTMLElementMethods;
17use crate::dom::bindings::codegen::Bindings::HTMLFontElementBinding::HTMLFontElementMethods;
18use crate::dom::bindings::str::DOMString;
19use crate::dom::document::Document;
20use crate::dom::element::Element;
21use crate::dom::execcommand::commands::backcolor::execute_backcolor_command;
22use crate::dom::execcommand::commands::bold::execute_bold_command;
23use crate::dom::execcommand::commands::createlink::execute_createlink_command;
24use crate::dom::execcommand::commands::defaultparagraphseparator::execute_default_paragraph_separator_command;
25use crate::dom::execcommand::commands::delete::execute_delete_command;
26use crate::dom::execcommand::commands::fontname::execute_fontname_command;
27use crate::dom::execcommand::commands::fontsize::{
28 execute_fontsize_command, font_size_loosely_equivalent, value_for_fontsize_command,
29};
30use crate::dom::execcommand::commands::forecolor::execute_forecolor_command;
31use crate::dom::execcommand::commands::forwarddelete::execute_forward_delete_command;
32use crate::dom::execcommand::commands::hilitecolor::execute_hilitecolor_command;
33use crate::dom::execcommand::commands::indent::execute_indent_command;
34use crate::dom::execcommand::commands::inserthorizontalrule::execute_insert_horizontal_rule_command;
35use crate::dom::execcommand::commands::insertimage::execute_insert_image_command;
36use crate::dom::execcommand::commands::insertlinebreak::execute_insert_line_break_command;
37use crate::dom::execcommand::commands::insertparagraph::execute_insert_paragraph_command;
38use crate::dom::execcommand::commands::inserttext::execute_insert_text_command;
39use crate::dom::execcommand::commands::italic::execute_italic_command;
40use crate::dom::execcommand::commands::removeformat::execute_removeformat_command;
41use crate::dom::execcommand::commands::strikethrough::execute_strikethrough_command;
42use crate::dom::execcommand::commands::stylewithcss::execute_style_with_css_command;
43use crate::dom::execcommand::commands::subscript::execute_subscript_command;
44use crate::dom::execcommand::commands::superscript::execute_superscript_command;
45use crate::dom::execcommand::commands::underline::execute_underline_command;
46use crate::dom::execcommand::commands::unlink::execute_unlink_command;
47use crate::dom::html::htmlelement::HTMLElement;
48use crate::dom::html::htmlfontelement::HTMLFontElement;
49use crate::dom::iterators::ShadowIncluding;
50use crate::dom::node::{Node, NodeTraits};
51use crate::dom::range::Range;
52use crate::dom::selection::Selection;
53
54#[derive(Default, Clone, Copy, MallocSizeOf)]
55pub(crate) enum DefaultSingleLineContainerName {
56 #[default]
57 Div,
58 Paragraph,
59}
60
61impl DefaultSingleLineContainerName {
62 pub(crate) fn str(&self) -> &str {
63 match self {
64 DefaultSingleLineContainerName::Div => "div",
65 DefaultSingleLineContainerName::Paragraph => "p",
66 }
67 }
68}
69
70impl From<DefaultSingleLineContainerName> for DOMString {
71 fn from(default_single_line_container_name: DefaultSingleLineContainerName) -> Self {
72 match default_single_line_container_name {
73 DefaultSingleLineContainerName::Div => DOMString::from_static("div"),
74 DefaultSingleLineContainerName::Paragraph => DOMString::from_static("p"),
75 }
76 }
77}
78
79pub(crate) enum BoolOrOptionalString {
80 Bool(bool),
81 OptionalString(Option<DOMString>),
82}
83
84impl From<Option<DOMString>> for BoolOrOptionalString {
85 fn from(optional_string: Option<DOMString>) -> Self {
86 Self::OptionalString(optional_string)
87 }
88}
89
90impl From<bool> for BoolOrOptionalString {
91 fn from(bool_: bool) -> Self {
92 Self::Bool(bool_)
93 }
94}
95
96pub(crate) struct RecordedStateOfCommand {
97 pub(crate) command: CommandName,
98 pub(crate) value: BoolOrOptionalString,
99}
100
101impl RecordedStateOfCommand {
102 pub(crate) fn for_command_node(command: CommandName, node: &Node) -> Self {
103 let value = node.effective_command_value(&command).into();
104 Self { command, value }
105 }
106
107 pub(crate) fn for_command_node_with_inline_activated_values(
108 command: CommandName,
109 node: &Node,
110 ) -> Self {
111 let effective_command_value = node.effective_command_value(&command);
112 let value = effective_command_value
113 .is_some_and(|effective_command_value| {
114 command
115 .inline_command_activated_values()
116 .contains(&effective_command_value.str().as_ref())
117 })
118 .into();
119 Self { command, value }
120 }
121
122 pub(crate) fn for_command_node_with_value(
123 cx: &mut JSContext,
124 command: CommandName,
125 document: &Document,
126 ) -> Self {
127 let value = command.current_value(cx, document).into();
128 Self { command, value }
129 }
130
131 fn for_command_state_override(command: CommandName, document: &Document) -> Option<Self> {
132 let value = document.state_override(&command)?.into();
133 Some(Self { command, value })
134 }
135
136 fn for_command_value_override(command: CommandName, document: &Document) -> Option<Self> {
137 let value_override = document.value_override(&command)?;
138 let value = Some(value_override).into();
139 Some(Self { command, value })
140 }
141}
142
143#[derive(Clone, Copy, Eq, PartialEq)]
145pub(crate) enum CssPropertyName {
146 BackgroundColor,
147 Color,
148 FontFamily,
149 FontSize,
150 FontWeight,
151 FontStyle,
152 TextDecoration,
153 TextDecorationLine,
154}
155
156impl CssPropertyName {
157 pub(crate) fn resolved_value_for_node(&self, element: &Element) -> Option<DOMString> {
158 let style = element.style()?;
159
160 Some(
161 match self {
162 CssPropertyName::BackgroundColor => {
163 let background_color = style.clone_background_color();
164 if let Some(absolute_color) = background_color.as_absolute() {
165 if absolute_color.is_transparent() {
168 return None;
169 }
170 let mut absolute_color = *absolute_color;
174 absolute_color.flags.insert(ColorFlags::IS_LEGACY_SRGB);
175 return Some(absolute_color.to_css_string().into());
176 }
177 background_color.to_css_string()
178 },
179 CssPropertyName::Color => {
180 if let Some(ancestor_font) = element.downcast::<HTMLFontElement>() {
187 let color = ancestor_font.Color();
188 if !color.is_empty() {
189 return Some(color);
190 }
191 }
192 style.clone_color().to_css_string()
193 },
194 CssPropertyName::FontFamily => {
195 if let Some(ancestor_font) = element.downcast::<HTMLFontElement>() {
202 let face = ancestor_font.Face();
203 if !face.is_empty() {
204 return Some(face);
205 }
206 }
207 style.clone_font_family().to_css_string()
208 },
209 CssPropertyName::FontSize => {
210 return element
224 .upcast::<Node>()
225 .inclusive_ancestors(ShadowIncluding::No)
226 .find_map(|ancestor| {
227 if let Some(ancestor_font) = ancestor.downcast::<HTMLFontElement>() {
228 Some(ancestor_font.Size())
229 } else {
230 self.value_set_for_style(ancestor.downcast::<Element>()?)
231 }
232 })
233 .or_else(|| {
234 let pixels = style.get_font().font_size.computed_size().px();
235 Some(format!("{}px", pixels).into())
236 });
237 },
238 CssPropertyName::FontWeight => style.clone_font_weight().to_css_string(),
239 CssPropertyName::FontStyle => style.clone_font_style().to_css_string(),
240 CssPropertyName::TextDecoration => unreachable!("Should use longhands instead"),
241 CssPropertyName::TextDecorationLine => {
242 let text_decoration_line = style.get_text().text_decoration_line;
243 if text_decoration_line == TextDecorationLine::NONE {
244 return None;
245 }
246 text_decoration_line.to_css_string()
247 },
248 }
249 .into(),
250 )
251 }
252
253 pub(crate) fn value_set_for_style(&self, element: &Element) -> Option<DOMString> {
257 let style_attribute = element.style_attribute().borrow();
258 let declarations = style_attribute.as_ref()?;
259 let document = element.owner_document();
260 let shared_lock = document.style_shared_author_lock();
261 let read_lock = shared_lock.read();
262 let style = declarations.read_with(&read_lock);
263
264 let longhand_id = match self {
265 CssPropertyName::BackgroundColor => LonghandId::BackgroundColor,
266 CssPropertyName::Color => LonghandId::Color,
267 CssPropertyName::FontFamily => LonghandId::FontFamily,
268 CssPropertyName::FontSize => LonghandId::FontSize,
269 CssPropertyName::FontWeight => LonghandId::FontWeight,
270 CssPropertyName::FontStyle => LonghandId::FontStyle,
271 CssPropertyName::TextDecoration => {
272 let mut dest = String::new();
273 style
274 .shorthand_to_css(ShorthandId::TextDecoration, &mut dest)
275 .ok()?;
276 return Some(dest.into());
277 },
278 CssPropertyName::TextDecorationLine => LonghandId::TextDecorationLine,
279 };
280 style
281 .get(PropertyDeclarationId::Longhand(longhand_id))
282 .and_then(|value| {
283 let mut dest = String::new();
284 value.0.to_css(&mut dest).ok()?;
285 Some(dest.into())
286 })
287 }
288
289 fn property_name(&self) -> DOMString {
290 match self {
291 CssPropertyName::BackgroundColor => "background-color",
292 CssPropertyName::Color => "color",
293 CssPropertyName::FontFamily => "font-family",
294 CssPropertyName::FontSize => "font-size",
295 CssPropertyName::FontWeight => "font-weight",
296 CssPropertyName::FontStyle => "font-style",
297 CssPropertyName::TextDecoration => "text-decoration",
298 CssPropertyName::TextDecorationLine => "text-decoration-line",
299 }
300 .into()
301 }
302
303 pub(crate) fn set_for_element(
304 &self,
305 cx: &mut JSContext,
306 element: &HTMLElement,
307 new_value: DOMString,
308 ) {
309 let style = element.Style(cx);
310
311 let _ = style.SetProperty(cx, self.property_name(), new_value, "".into());
312 }
313
314 pub(crate) fn remove_from_element(&self, cx: &mut JSContext, element: &HTMLElement) {
315 let _ = element.Style(cx).RemoveProperty(cx, self.property_name());
316 }
317}
318
319#[derive(Clone, Copy, Eq, Hash, MallocSizeOf, PartialEq)]
320#[expect(unused)] pub(crate) enum CommandName {
322 BackColor,
323 Bold,
324 Copy,
325 CreateLink,
326 Cut,
327 DefaultParagraphSeparator,
328 Delete,
329 FontName,
330 FontSize,
331 ForeColor,
332 FormatBlock,
333 ForwardDelete,
334 HiliteColor,
335 Indent,
336 InsertHorizontalRule,
337 InsertHtml,
338 InsertImage,
339 InsertLineBreak,
340 InsertOrderedList,
341 InsertParagraph,
342 InsertText,
343 InsertUnorderedList,
344 Italic,
345 JustifyCenter,
346 JustifyFull,
347 JustifyLeft,
348 JustifyRight,
349 Outdent,
350 Paste,
351 Redo,
352 RemoveFormat,
353 SelectAll,
354 Strikethrough,
355 StyleWithCss,
356 Subscript,
357 Superscript,
358 Underline,
359 Undo,
360 Unlink,
361 Usecss,
362}
363
364impl CommandName {
365 pub(crate) fn is_indeterminate(&self, cx: &mut JSContext, document: &Document) -> bool {
367 if !self.is_standard_inline_value_command() {
368 return false;
369 }
370 let Some(selection) = document.GetSelection(cx) else {
374 return false;
375 };
376 let Some(active_range) = selection.active_range(cx) else {
377 return false;
378 };
379 let mut at_least_two_different_effective_values = false;
380 let mut previous_effective_value: Option<DOMString> = None;
381 active_range.for_each_effectively_contained_child(|node| {
382 if at_least_two_different_effective_values || !node.is_formattable(cx.no_gc()) {
383 return;
384 }
385 if let Some(effective_command_value) = node.effective_command_value(self) {
386 if matches!(self, CommandName::Subscript | CommandName::Superscript) &&
391 effective_command_value == "mixed"
392 {
393 at_least_two_different_effective_values = true;
394 }
395 if let Some(previous_effective_value) = &previous_effective_value {
396 if &effective_command_value != previous_effective_value {
397 at_least_two_different_effective_values = true;
398 }
399 } else {
400 previous_effective_value = Some(effective_command_value);
401 }
402 }
403 });
404 at_least_two_different_effective_values
405 }
406
407 pub(crate) fn current_state(&self, cx: &mut JSContext, document: &Document) -> Option<bool> {
409 Some(match self {
410 CommandName::StyleWithCss => {
411 document.css_styling_flag()
414 },
415 _ => {
416 let inline_command_activated_values = self.inline_command_activated_values();
423 if inline_command_activated_values.is_empty() {
424 return None;
425 }
426 let selection = document.GetSelection(cx)?;
427 let active_range = selection.active_range(cx)?;
428 let mut at_least_one_child_is_formattable = false;
429 let mut all_children_have_matching_command_values = true;
430 active_range.for_each_effectively_contained_child(|node| {
431 if !node.is_formattable(cx.no_gc()) {
432 return;
433 }
434 at_least_one_child_is_formattable = true;
435 all_children_have_matching_command_values &= node
436 .effective_command_value(self)
437 .is_some_and(|effective_value| {
438 inline_command_activated_values.contains(&&*effective_value.str())
439 });
440 });
441 if at_least_one_child_is_formattable {
442 all_children_have_matching_command_values
443 } else {
444 active_range
445 .start_container()
446 .effective_command_value(self)
447 .is_some_and(|effective_value| {
448 inline_command_activated_values.contains(&&*effective_value.str())
449 })
450 }
451 },
452 })
453 }
454
455 pub(crate) fn current_value(
457 &self,
458 cx: &mut JSContext,
459 document: &Document,
460 ) -> Option<DOMString> {
461 Some(match self {
462 CommandName::DefaultParagraphSeparator => {
463 document.default_single_line_container_name().into()
466 },
467 CommandName::FontSize => value_for_fontsize_command(cx, document)?,
468 _ if self.is_standard_inline_value_command() => {
469 let selection = document.GetSelection(cx)?;
475 let active_range = selection.active_range(cx)?;
476
477 active_range
478 .first_formattable_contained_node(cx.no_gc())
479 .unwrap_or_else(|| active_range.start_container())
480 .effective_command_value(self)
481 .unwrap_or_default()
482 },
483 _ => return None,
484 })
485 }
486
487 pub(crate) fn are_equivalent_values(
489 &self,
490 first: Option<&DOMString>,
491 second: Option<&DOMString>,
492 ) -> bool {
493 match (first, second) {
494 (None, None) => true,
496 (Some(first_str), Some(second_str)) => {
497 match self {
499 CommandName::Bold => {
500 first_str == second_str ||
504 matches!(
505 (first_str.str().as_ref(), second_str.str().as_ref()),
506 ("bold", "700") |
507 ("700", "bold") |
508 ("normal", "400") |
509 ("400", "normal")
510 )
511 },
512 CommandName::BackColor | CommandName::ForeColor | CommandName::HiliteColor => {
513 match (
519 parse_legacy_color(&first_str.str()),
520 parse_legacy_color(&second_str.str()),
521 ) {
522 (Ok(first_legacy_color), Ok(second_legacy_color)) => {
523 first_legacy_color == second_legacy_color
524 },
525 (Err(_), Err(_)) => true,
526 _ => false,
527 }
528 },
529 _ => first_str == second_str,
531 }
532 },
533 _ => false,
534 }
535 }
536
537 pub(crate) fn are_loosely_equivalent_values(
539 &self,
540 first: Option<&DOMString>,
541 second: Option<&DOMString>,
542 ) -> bool {
543 if self.are_equivalent_values(first, second) {
545 return true;
546 }
547 if let (CommandName::FontSize, Some(first), Some(second)) = (self, first, second) {
552 font_size_loosely_equivalent(first, second)
553 } else {
554 false
555 }
556 }
557
558 pub(crate) fn record_current_overrides(document: &Document) -> Vec<RecordedStateOfCommand> {
560 let mut overrides = vec![];
562 if let Some(value_override) =
565 RecordedStateOfCommand::for_command_value_override(CommandName::CreateLink, document)
566 {
567 overrides.push(value_override);
568 }
569 for command in [
573 CommandName::Bold,
574 CommandName::Italic,
575 CommandName::Strikethrough,
576 CommandName::Subscript,
577 CommandName::Superscript,
578 CommandName::Underline,
579 ] {
580 if let Some(state_override) =
581 RecordedStateOfCommand::for_command_state_override(command, document)
582 {
583 overrides.push(state_override);
584 }
585 }
586 for command in [
590 CommandName::FontName,
591 CommandName::FontSize,
592 CommandName::ForeColor,
593 CommandName::HiliteColor,
594 ] {
595 if let Some(value_override) =
596 RecordedStateOfCommand::for_command_value_override(command, document)
597 {
598 overrides.push(value_override);
599 }
600 }
601 overrides
603 }
604
605 pub(crate) fn relevant_css_property(&self) -> Option<CssPropertyName> {
607 Some(match self {
610 CommandName::BackColor => CssPropertyName::BackgroundColor,
611 CommandName::Bold => CssPropertyName::FontWeight,
612 CommandName::FontName => CssPropertyName::FontFamily,
613 CommandName::FontSize => CssPropertyName::FontSize,
614 CommandName::ForeColor => CssPropertyName::Color,
615 CommandName::HiliteColor => CssPropertyName::BackgroundColor,
616 CommandName::Italic => CssPropertyName::FontStyle,
617 _ => return None,
619 })
620 }
621
622 pub(crate) fn resolved_value_for_node(&self, element: &Element) -> Option<DOMString> {
623 let property = self.relevant_css_property()?;
624 property.resolved_value_for_node(element)
625 }
626
627 pub(crate) fn is_standard_inline_value_command(&self) -> bool {
629 matches!(
630 self,
631 CommandName::BackColor |
632 CommandName::FontName |
633 CommandName::ForeColor |
634 CommandName::HiliteColor
635 )
636 }
637
638 pub(crate) fn is_enabled(&self, cx: &JSContext, range: &Range, editing_host: &Node) -> bool {
639 match self {
640 CommandName::Delete => {
648 if !range.collapsed() {
649 return true;
650 }
651 let start_container = range.start_container();
652 if *start_container == *editing_host {
653 return false;
659 }
660 let mut current_offset = range.start_offset();
661 for current_ancestor in
662 start_container.inclusive_ancestors_unrooted(cx, ShadowIncluding::Yes)
663 {
664 if current_offset != 0 {
665 return true;
666 }
667 if *current_ancestor == editing_host {
668 return false;
669 }
670 current_offset = current_ancestor.index();
671 }
672 false
673 },
674 _ => true,
675 }
676 }
677
678 pub(crate) fn is_enabled_in_plaintext_only_state(&self) -> bool {
679 matches!(
680 self,
681 CommandName::Copy |
682 CommandName::Cut |
683 CommandName::DefaultParagraphSeparator |
684 CommandName::FormatBlock |
685 CommandName::ForwardDelete |
686 CommandName::InsertHtml |
687 CommandName::InsertLineBreak |
688 CommandName::InsertParagraph |
689 CommandName::InsertText |
690 CommandName::Paste |
691 CommandName::Redo |
692 CommandName::StyleWithCss |
693 CommandName::Undo |
694 CommandName::Usecss |
695 CommandName::Delete
696 )
697 }
698
699 fn preserves_overrides(&self) -> bool {
701 matches!(
702 self,
703 CommandName::Delete |
704 CommandName::FormatBlock |
705 CommandName::ForwardDelete |
706 CommandName::Indent |
707 CommandName::InsertHorizontalRule |
708 CommandName::InsertHtml |
709 CommandName::InsertImage |
710 CommandName::InsertLineBreak |
711 CommandName::InsertOrderedList |
712 CommandName::InsertParagraph |
713 CommandName::InsertUnorderedList |
714 CommandName::JustifyCenter |
715 CommandName::JustifyFull |
716 CommandName::JustifyLeft |
717 CommandName::JustifyRight |
718 CommandName::Outdent
719 )
720 }
721
722 pub(crate) fn execute(
724 &self,
725 cx: &mut JSContext,
726 document: &Document,
727 selection: &Selection,
728 value: DOMString,
729 ) -> bool {
730 let overrides = if self.preserves_overrides() {
734 Self::record_current_overrides(document)
735 } else {
736 vec![]
737 };
738 let result = match self {
739 CommandName::BackColor => execute_backcolor_command(cx, document, selection, value),
740 CommandName::Bold => execute_bold_command(cx, document, selection),
741 CommandName::CreateLink => execute_createlink_command(cx, document, selection, value),
742 CommandName::DefaultParagraphSeparator => {
743 execute_default_paragraph_separator_command(document, value)
744 },
745 CommandName::Delete => execute_delete_command(cx, document, selection),
746 CommandName::FontName => execute_fontname_command(cx, document, selection, value),
747 CommandName::FontSize => execute_fontsize_command(cx, document, selection, value),
748 CommandName::ForeColor => execute_forecolor_command(cx, document, selection, value),
749 CommandName::ForwardDelete => execute_forward_delete_command(cx, document, selection),
750 CommandName::HiliteColor => execute_hilitecolor_command(cx, document, selection, value),
751 CommandName::Indent => execute_indent_command(cx, document, selection),
752 CommandName::InsertHorizontalRule => {
753 execute_insert_horizontal_rule_command(cx, document, selection)
754 },
755 CommandName::InsertImage => {
756 execute_insert_image_command(cx, document, selection, value)
757 },
758 CommandName::InsertLineBreak => {
759 execute_insert_line_break_command(cx, document, selection)
760 },
761 CommandName::InsertParagraph => {
762 execute_insert_paragraph_command(cx, document, selection)
763 },
764 CommandName::InsertText => execute_insert_text_command(cx, document, selection, value),
765 CommandName::Italic => execute_italic_command(cx, document, selection),
766 CommandName::RemoveFormat => execute_removeformat_command(cx, document, selection),
767 CommandName::Strikethrough => execute_strikethrough_command(cx, document, selection),
768 CommandName::StyleWithCss => execute_style_with_css_command(document, value),
769 CommandName::Subscript => execute_subscript_command(cx, document, selection),
770 CommandName::Superscript => execute_superscript_command(cx, document, selection),
771 CommandName::Underline => execute_underline_command(cx, document, selection),
772 CommandName::Unlink => execute_unlink_command(cx, selection),
773 _ => false,
774 };
775
776 if let Some(active_range) = selection
780 .active_range(cx)
781 .filter(|active_range| active_range.collapsed())
782 {
783 active_range.restore_states_and_values(cx, selection, document, overrides);
784 }
785
786 result
787 }
788
789 pub(crate) fn inline_command_activated_values(&self) -> Vec<&str> {
791 match self {
792 CommandName::Bold => vec!["bold", "600", "700", "800", "900"],
794 CommandName::Italic => vec!["italic", "oblique"],
796 CommandName::Strikethrough => vec!["line-through"],
798 CommandName::Subscript => vec!["subscript"],
800 CommandName::Superscript => vec!["superscript"],
802 CommandName::Underline => vec!["underline"],
804 _ => vec![],
805 }
806 }
807}