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::hilitecolor::execute_hilitecolor_command;
32use crate::dom::execcommand::commands::insertparagraph::execute_insert_paragraph_command;
33use crate::dom::execcommand::commands::italic::execute_italic_command;
34use crate::dom::execcommand::commands::removeformat::execute_removeformat_command;
35use crate::dom::execcommand::commands::strikethrough::execute_strikethrough_command;
36use crate::dom::execcommand::commands::stylewithcss::execute_style_with_css_command;
37use crate::dom::execcommand::commands::subscript::execute_subscript_command;
38use crate::dom::execcommand::commands::superscript::execute_superscript_command;
39use crate::dom::execcommand::commands::underline::execute_underline_command;
40use crate::dom::execcommand::commands::unlink::execute_unlink_command;
41use crate::dom::html::htmlelement::HTMLElement;
42use crate::dom::html::htmlfontelement::HTMLFontElement;
43use crate::dom::iterators::ShadowIncluding;
44use crate::dom::node::{Node, NodeTraits};
45use crate::dom::selection::Selection;
46
47#[derive(Default, Clone, Copy, MallocSizeOf)]
48pub(crate) enum DefaultSingleLineContainerName {
49 #[default]
50 Div,
51 Paragraph,
52}
53
54impl DefaultSingleLineContainerName {
55 pub(crate) fn str(&self) -> &str {
56 match self {
57 DefaultSingleLineContainerName::Div => "div",
58 DefaultSingleLineContainerName::Paragraph => "p",
59 }
60 }
61}
62
63impl From<DefaultSingleLineContainerName> for DOMString {
64 fn from(default_single_line_container_name: DefaultSingleLineContainerName) -> Self {
65 match default_single_line_container_name {
66 DefaultSingleLineContainerName::Div => DOMString::from("div"),
67 DefaultSingleLineContainerName::Paragraph => DOMString::from("p"),
68 }
69 }
70}
71
72pub(crate) enum BoolOrOptionalString {
73 Bool(bool),
74 OptionalString(Option<DOMString>),
75}
76
77impl From<Option<DOMString>> for BoolOrOptionalString {
78 fn from(optional_string: Option<DOMString>) -> Self {
79 Self::OptionalString(optional_string)
80 }
81}
82
83impl From<bool> for BoolOrOptionalString {
84 fn from(bool_: bool) -> Self {
85 Self::Bool(bool_)
86 }
87}
88
89pub(crate) struct RecordedStateOfCommand {
90 pub(crate) command: CommandName,
91 pub(crate) value: BoolOrOptionalString,
92}
93
94impl RecordedStateOfCommand {
95 pub(crate) fn for_command_node(command: CommandName, node: &Node) -> Self {
96 let value = node.effective_command_value(&command).into();
97 Self { command, value }
98 }
99
100 pub(crate) fn for_command_node_with_inline_activated_values(
101 command: CommandName,
102 node: &Node,
103 ) -> Self {
104 let effective_command_value = node.effective_command_value(&command);
105 let value = effective_command_value
106 .is_some_and(|effective_command_value| {
107 command
108 .inline_command_activated_values()
109 .contains(&effective_command_value.str().as_ref())
110 })
111 .into();
112 Self { command, value }
113 }
114
115 pub(crate) fn for_command_node_with_value(
116 cx: &mut JSContext,
117 command: CommandName,
118 document: &Document,
119 ) -> Self {
120 let value = command.current_value(cx, document).into();
121 Self { command, value }
122 }
123
124 fn for_command_state_override(command: CommandName, document: &Document) -> Option<Self> {
125 let value = document.state_override(&command)?.into();
126 Some(Self { command, value })
127 }
128
129 fn for_command_value_override(command: CommandName, document: &Document) -> Option<Self> {
130 let value_override = document.value_override(&command)?;
131 let value = Some(value_override).into();
132 Some(Self { command, value })
133 }
134}
135
136#[derive(Clone, Copy, Eq, PartialEq)]
138pub(crate) enum CssPropertyName {
139 BackgroundColor,
140 Color,
141 FontFamily,
142 FontSize,
143 FontWeight,
144 FontStyle,
145 TextDecoration,
146 TextDecorationLine,
147}
148
149impl CssPropertyName {
150 pub(crate) fn resolved_value_for_node(&self, element: &Element) -> Option<DOMString> {
151 let style = element.style()?;
152
153 Some(
154 match self {
155 CssPropertyName::BackgroundColor => {
156 let background_color = style.clone_background_color();
157 if let Some(absolute_color) = background_color.as_absolute() {
158 if absolute_color.is_transparent() {
161 return None;
162 }
163 let mut absolute_color = *absolute_color;
167 absolute_color.flags.insert(ColorFlags::IS_LEGACY_SRGB);
168 return Some(absolute_color.to_css_string().into());
169 }
170 background_color.to_css_string()
171 },
172 CssPropertyName::Color => {
173 if let Some(ancestor_font) = element.downcast::<HTMLFontElement>() {
180 let color = ancestor_font.Color();
181 if !color.is_empty() {
182 return Some(color);
183 }
184 }
185 style.clone_color().to_css_string()
186 },
187 CssPropertyName::FontFamily => {
188 if let Some(ancestor_font) = element.downcast::<HTMLFontElement>() {
195 let face = ancestor_font.Face();
196 if !face.is_empty() {
197 return Some(face);
198 }
199 }
200 style.clone_font_family().to_css_string()
201 },
202 CssPropertyName::FontSize => {
203 return element
217 .upcast::<Node>()
218 .inclusive_ancestors(ShadowIncluding::No)
219 .find_map(|ancestor| {
220 if let Some(ancestor_font) = ancestor.downcast::<HTMLFontElement>() {
221 Some(ancestor_font.Size())
222 } else {
223 self.value_set_for_style(ancestor.downcast::<Element>()?)
224 }
225 })
226 .or_else(|| {
227 let pixels = style.get_font().font_size.computed_size().px();
228 Some(format!("{}px", pixels).into())
229 });
230 },
231 CssPropertyName::FontWeight => style.clone_font_weight().to_css_string(),
232 CssPropertyName::FontStyle => style.clone_font_style().to_css_string(),
233 CssPropertyName::TextDecoration => unreachable!("Should use longhands instead"),
234 CssPropertyName::TextDecorationLine => {
235 let text_decoration_line = style.get_text().text_decoration_line;
236 if text_decoration_line == TextDecorationLine::NONE {
237 return None;
238 }
239 text_decoration_line.to_css_string()
240 },
241 }
242 .into(),
243 )
244 }
245
246 pub(crate) fn value_set_for_style(&self, element: &Element) -> Option<DOMString> {
250 let style_attribute = element.style_attribute().borrow();
251 let declarations = style_attribute.as_ref()?;
252 let document = element.owner_document();
253 let shared_lock = document.style_shared_author_lock();
254 let read_lock = shared_lock.read();
255 let style = declarations.read_with(&read_lock);
256
257 let longhand_id = match self {
258 CssPropertyName::BackgroundColor => LonghandId::BackgroundColor,
259 CssPropertyName::Color => LonghandId::Color,
260 CssPropertyName::FontFamily => LonghandId::FontFamily,
261 CssPropertyName::FontSize => LonghandId::FontSize,
262 CssPropertyName::FontWeight => LonghandId::FontWeight,
263 CssPropertyName::FontStyle => LonghandId::FontStyle,
264 CssPropertyName::TextDecoration => {
265 let mut dest = String::new();
266 style
267 .shorthand_to_css(ShorthandId::TextDecoration, &mut dest)
268 .ok()?;
269 return Some(dest.into());
270 },
271 CssPropertyName::TextDecorationLine => LonghandId::TextDecorationLine,
272 };
273 style
274 .get(PropertyDeclarationId::Longhand(longhand_id))
275 .and_then(|value| {
276 let mut dest = String::new();
277 value.0.to_css(&mut dest).ok()?;
278 Some(dest.into())
279 })
280 }
281
282 fn property_name(&self) -> DOMString {
283 match self {
284 CssPropertyName::BackgroundColor => "background-color",
285 CssPropertyName::Color => "color",
286 CssPropertyName::FontFamily => "font-family",
287 CssPropertyName::FontSize => "font-size",
288 CssPropertyName::FontWeight => "font-weight",
289 CssPropertyName::FontStyle => "font-style",
290 CssPropertyName::TextDecoration => "text-decoration",
291 CssPropertyName::TextDecorationLine => "text-decoration-line",
292 }
293 .into()
294 }
295
296 pub(crate) fn set_for_element(
297 &self,
298 cx: &mut JSContext,
299 element: &HTMLElement,
300 new_value: DOMString,
301 ) {
302 let style = element.Style(cx);
303
304 let _ = style.SetProperty(cx, self.property_name(), new_value, "".into());
305 }
306
307 pub(crate) fn remove_from_element(&self, cx: &mut JSContext, element: &HTMLElement) {
308 let _ = element.Style(cx).RemoveProperty(cx, self.property_name());
309 }
310}
311
312#[derive(Clone, Copy, Eq, Hash, MallocSizeOf, PartialEq)]
313#[expect(unused)] pub(crate) enum CommandName {
315 BackColor,
316 Bold,
317 Copy,
318 CreateLink,
319 Cut,
320 DefaultParagraphSeparator,
321 Delete,
322 FontName,
323 FontSize,
324 ForeColor,
325 FormatBlock,
326 ForwardDelete,
327 HiliteColor,
328 Indent,
329 InsertHorizontalRule,
330 InsertHtml,
331 InsertImage,
332 InsertLineBreak,
333 InsertOrderedList,
334 InsertParagraph,
335 InsertText,
336 InsertUnorderedList,
337 Italic,
338 JustifyCenter,
339 JustifyFull,
340 JustifyLeft,
341 JustifyRight,
342 Outdent,
343 Paste,
344 Redo,
345 RemoveFormat,
346 SelectAll,
347 Strikethrough,
348 StyleWithCss,
349 Subscript,
350 Superscript,
351 Underline,
352 Undo,
353 Unlink,
354 Usecss,
355}
356
357impl CommandName {
358 pub(crate) fn is_indeterminate(&self, cx: &mut JSContext, document: &Document) -> bool {
360 if !self.is_standard_inline_value_command() {
361 return false;
362 }
363 let Some(selection) = document.GetSelection(cx) else {
367 return false;
368 };
369 let Some(active_range) = selection.active_range() else {
370 return false;
371 };
372 let mut at_least_two_different_effective_values = false;
373 let mut previous_effective_value: Option<DOMString> = None;
374 active_range.for_each_effectively_contained_child(|node| {
375 if at_least_two_different_effective_values || !node.is_formattable() {
376 return;
377 }
378 if let Some(effective_command_value) = node.effective_command_value(self) {
379 if matches!(self, CommandName::Subscript | CommandName::Superscript) &&
384 effective_command_value == "mixed"
385 {
386 at_least_two_different_effective_values = true;
387 }
388 if let Some(previous_effective_value) = &previous_effective_value {
389 if &effective_command_value != previous_effective_value {
390 at_least_two_different_effective_values = true;
391 }
392 } else {
393 previous_effective_value = Some(effective_command_value);
394 }
395 }
396 });
397 at_least_two_different_effective_values
398 }
399
400 pub(crate) fn current_state(&self, cx: &mut JSContext, document: &Document) -> Option<bool> {
402 Some(match self {
403 CommandName::StyleWithCss => {
404 document.css_styling_flag()
407 },
408 _ => {
409 let inline_command_activated_values = self.inline_command_activated_values();
416 if inline_command_activated_values.is_empty() {
417 return None;
418 }
419 let selection = document.GetSelection(cx)?;
420 let active_range = selection.active_range()?;
421 let mut at_least_one_child_is_formattable = false;
422 let mut all_children_have_matching_command_values = true;
423 active_range.for_each_effectively_contained_child(|node| {
424 if !node.is_formattable() {
425 return;
426 }
427 at_least_one_child_is_formattable = true;
428 all_children_have_matching_command_values &= node
429 .effective_command_value(self)
430 .is_some_and(|effective_value| {
431 inline_command_activated_values.contains(&&*effective_value.str())
432 });
433 });
434 if at_least_one_child_is_formattable {
435 all_children_have_matching_command_values
436 } else {
437 active_range
438 .start_container()
439 .effective_command_value(self)
440 .is_some_and(|effective_value| {
441 inline_command_activated_values.contains(&&*effective_value.str())
442 })
443 }
444 },
445 })
446 }
447
448 pub(crate) fn current_value(
450 &self,
451 cx: &mut JSContext,
452 document: &Document,
453 ) -> Option<DOMString> {
454 Some(match self {
455 CommandName::DefaultParagraphSeparator => {
456 document.default_single_line_container_name().into()
459 },
460 CommandName::FontSize => value_for_fontsize_command(cx, document)?,
461 _ if self.is_standard_inline_value_command() => {
462 let selection = document.GetSelection(cx)?;
468 let active_range = selection.active_range()?;
469
470 active_range
471 .first_formattable_contained_node()
472 .unwrap_or_else(|| active_range.start_container())
473 .effective_command_value(self)
474 .unwrap_or_default()
475 },
476 _ => return None,
477 })
478 }
479
480 pub(crate) fn are_equivalent_values(
482 &self,
483 first: Option<&DOMString>,
484 second: Option<&DOMString>,
485 ) -> bool {
486 match (first, second) {
487 (None, None) => true,
489 (Some(first_str), Some(second_str)) => {
490 match self {
492 CommandName::Bold => {
493 first_str == second_str ||
497 matches!(
498 (first_str.str().as_ref(), second_str.str().as_ref()),
499 ("bold", "700") |
500 ("700", "bold") |
501 ("normal", "400") |
502 ("400", "normal")
503 )
504 },
505 CommandName::BackColor | CommandName::ForeColor | CommandName::HiliteColor => {
506 match (
512 parse_legacy_color(&first_str.str()),
513 parse_legacy_color(&second_str.str()),
514 ) {
515 (Ok(first_legacy_color), Ok(second_legacy_color)) => {
516 first_legacy_color == second_legacy_color
517 },
518 (Err(_), Err(_)) => true,
519 _ => false,
520 }
521 },
522 _ => first_str == second_str,
524 }
525 },
526 _ => false,
527 }
528 }
529
530 pub(crate) fn are_loosely_equivalent_values(
532 &self,
533 first: Option<&DOMString>,
534 second: Option<&DOMString>,
535 ) -> bool {
536 if self.are_equivalent_values(first, second) {
538 return true;
539 }
540 if let (CommandName::FontSize, Some(first), Some(second)) = (self, first, second) {
545 font_size_loosely_equivalent(first, second)
546 } else {
547 false
548 }
549 }
550
551 fn record_current_overrides(document: &Document) -> Vec<RecordedStateOfCommand> {
553 let mut overrides = vec![];
555 if let Some(value_override) =
558 RecordedStateOfCommand::for_command_value_override(CommandName::CreateLink, document)
559 {
560 overrides.push(value_override);
561 }
562 for command in [
566 CommandName::Bold,
567 CommandName::Italic,
568 CommandName::Strikethrough,
569 CommandName::Subscript,
570 CommandName::Superscript,
571 CommandName::Underline,
572 ] {
573 if let Some(state_override) =
574 RecordedStateOfCommand::for_command_state_override(command, document)
575 {
576 overrides.push(state_override);
577 }
578 }
579 for command in [
583 CommandName::FontName,
584 CommandName::FontSize,
585 CommandName::ForeColor,
586 CommandName::HiliteColor,
587 ] {
588 if let Some(value_override) =
589 RecordedStateOfCommand::for_command_value_override(command, document)
590 {
591 overrides.push(value_override);
592 }
593 }
594 overrides
596 }
597
598 pub(crate) fn relevant_css_property(&self) -> Option<CssPropertyName> {
600 Some(match self {
603 CommandName::BackColor => CssPropertyName::BackgroundColor,
604 CommandName::Bold => CssPropertyName::FontWeight,
605 CommandName::FontName => CssPropertyName::FontFamily,
606 CommandName::FontSize => CssPropertyName::FontSize,
607 CommandName::ForeColor => CssPropertyName::Color,
608 CommandName::HiliteColor => CssPropertyName::BackgroundColor,
609 CommandName::Italic => CssPropertyName::FontStyle,
610 _ => return None,
612 })
613 }
614
615 pub(crate) fn resolved_value_for_node(&self, element: &Element) -> Option<DOMString> {
616 let property = self.relevant_css_property()?;
617 property.resolved_value_for_node(element)
618 }
619
620 pub(crate) fn is_standard_inline_value_command(&self) -> bool {
622 matches!(
623 self,
624 CommandName::BackColor |
625 CommandName::FontName |
626 CommandName::ForeColor |
627 CommandName::HiliteColor
628 )
629 }
630
631 pub(crate) fn is_enabled_in_plaintext_only_state(&self) -> bool {
632 matches!(
633 self,
634 CommandName::Copy |
635 CommandName::Cut |
636 CommandName::DefaultParagraphSeparator |
637 CommandName::FormatBlock |
638 CommandName::ForwardDelete |
639 CommandName::InsertHtml |
640 CommandName::InsertLineBreak |
641 CommandName::InsertParagraph |
642 CommandName::InsertText |
643 CommandName::Paste |
644 CommandName::Redo |
645 CommandName::StyleWithCss |
646 CommandName::Undo |
647 CommandName::Usecss |
648 CommandName::Delete
649 )
650 }
651
652 fn preserves_overrides(&self) -> bool {
654 matches!(
655 self,
656 CommandName::Delete |
657 CommandName::FormatBlock |
658 CommandName::ForwardDelete |
659 CommandName::Indent |
660 CommandName::InsertHorizontalRule |
661 CommandName::InsertHtml |
662 CommandName::InsertImage |
663 CommandName::InsertLineBreak |
664 CommandName::InsertOrderedList |
665 CommandName::InsertParagraph |
666 CommandName::InsertUnorderedList |
667 CommandName::JustifyCenter |
668 CommandName::JustifyFull |
669 CommandName::JustifyLeft |
670 CommandName::JustifyRight |
671 CommandName::Outdent
672 )
673 }
674
675 pub(crate) fn execute(
677 &self,
678 cx: &mut JSContext,
679 document: &Document,
680 selection: &Selection,
681 value: DOMString,
682 ) -> bool {
683 let overrides = if self.preserves_overrides() {
687 Self::record_current_overrides(document)
688 } else {
689 vec![]
690 };
691 let result = match self {
692 CommandName::BackColor => execute_backcolor_command(cx, document, selection, value),
693 CommandName::Bold => execute_bold_command(cx, document, selection),
694 CommandName::CreateLink => execute_createlink_command(cx, document, selection, value),
695 CommandName::DefaultParagraphSeparator => {
696 execute_default_paragraph_separator_command(document, value)
697 },
698 CommandName::Delete => execute_delete_command(cx, document, selection),
699 CommandName::FontName => execute_fontname_command(cx, document, selection, value),
700 CommandName::FontSize => execute_fontsize_command(cx, document, selection, value),
701 CommandName::ForeColor => execute_forecolor_command(cx, document, selection, value),
702 CommandName::HiliteColor => execute_hilitecolor_command(cx, document, selection, value),
703 CommandName::InsertParagraph => {
704 execute_insert_paragraph_command(cx, document, selection)
705 },
706 CommandName::Italic => execute_italic_command(cx, document, selection),
707 CommandName::RemoveFormat => execute_removeformat_command(cx, document, selection),
708 CommandName::Strikethrough => execute_strikethrough_command(cx, document, selection),
709 CommandName::StyleWithCss => execute_style_with_css_command(document, value),
710 CommandName::Subscript => execute_subscript_command(cx, document, selection),
711 CommandName::Superscript => execute_superscript_command(cx, document, selection),
712 CommandName::Underline => execute_underline_command(cx, document, selection),
713 CommandName::Unlink => execute_unlink_command(cx, selection),
714 _ => false,
715 };
716
717 if let Some(active_range) = selection
721 .active_range()
722 .filter(|active_range| active_range.collapsed())
723 {
724 active_range.restore_states_and_values(cx, selection, document, overrides);
725 }
726
727 result
728 }
729
730 pub(crate) fn inline_command_activated_values(&self) -> Vec<&str> {
732 match self {
733 CommandName::Bold => vec!["bold", "600", "700", "800", "900"],
735 CommandName::Italic => vec!["italic", "oblique"],
737 CommandName::Strikethrough => vec!["line-through"],
739 CommandName::Subscript => vec!["subscript"],
741 CommandName::Superscript => vec!["superscript"],
743 CommandName::Underline => vec!["underline"],
745 _ => vec![],
746 }
747 }
748}