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