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