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