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::node::{Node, NodeTraits, ShadowIncluding};
44use crate::dom::selection::Selection;
45use crate::script_runtime::CanGc;
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(CanGc::from_cx(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
309 .Style(CanGc::from_cx(cx))
310 .RemoveProperty(cx, self.property_name());
311 }
312}
313
314#[derive(Clone, Copy, Eq, Hash, MallocSizeOf, PartialEq)]
315#[expect(unused)] pub(crate) enum CommandName {
317 BackColor,
318 Bold,
319 Copy,
320 CreateLink,
321 Cut,
322 DefaultParagraphSeparator,
323 Delete,
324 FontName,
325 FontSize,
326 ForeColor,
327 FormatBlock,
328 ForwardDelete,
329 HiliteColor,
330 Indent,
331 InsertHorizontalRule,
332 InsertHtml,
333 InsertImage,
334 InsertLineBreak,
335 InsertOrderedList,
336 InsertParagraph,
337 InsertText,
338 InsertUnorderedList,
339 Italic,
340 JustifyCenter,
341 JustifyFull,
342 JustifyLeft,
343 JustifyRight,
344 Outdent,
345 Paste,
346 Redo,
347 RemoveFormat,
348 SelectAll,
349 Strikethrough,
350 StyleWithCss,
351 Subscript,
352 Superscript,
353 Underline,
354 Undo,
355 Unlink,
356 Usecss,
357}
358
359impl CommandName {
360 pub(crate) fn is_indeterminate(&self, cx: &mut JSContext, document: &Document) -> bool {
362 if !self.is_standard_inline_value_command() {
363 return false;
364 }
365 let Some(selection) = document.GetSelection(cx) else {
369 return false;
370 };
371 let Some(active_range) = selection.active_range() else {
372 return false;
373 };
374 let mut at_least_two_different_effective_values = false;
375 let mut previous_effective_value: Option<DOMString> = None;
376 active_range.for_each_effectively_contained_child(|node| {
377 if at_least_two_different_effective_values || !node.is_formattable() {
378 return;
379 }
380 if let Some(effective_command_value) = node.effective_command_value(self) {
381 if matches!(self, CommandName::Subscript | CommandName::Superscript) &&
386 effective_command_value == "mixed"
387 {
388 at_least_two_different_effective_values = true;
389 }
390 if let Some(previous_effective_value) = &previous_effective_value {
391 if &effective_command_value != previous_effective_value {
392 at_least_two_different_effective_values = true;
393 }
394 } else {
395 previous_effective_value = Some(effective_command_value);
396 }
397 }
398 });
399 at_least_two_different_effective_values
400 }
401
402 pub(crate) fn current_state(&self, cx: &mut JSContext, document: &Document) -> Option<bool> {
404 Some(match self {
405 CommandName::StyleWithCss => {
406 document.css_styling_flag()
409 },
410 _ => {
411 let inline_command_activated_values = self.inline_command_activated_values();
418 if inline_command_activated_values.is_empty() {
419 return None;
420 }
421 let selection = document.GetSelection(cx)?;
422 let active_range = selection.active_range()?;
423 let mut at_least_one_child_is_formattable = false;
424 let mut all_children_have_matching_command_values = true;
425 active_range.for_each_effectively_contained_child(|node| {
426 if !node.is_formattable() {
427 return;
428 }
429 at_least_one_child_is_formattable = true;
430 all_children_have_matching_command_values &= node
431 .effective_command_value(self)
432 .is_some_and(|effective_value| {
433 inline_command_activated_values.contains(&&*effective_value.str())
434 });
435 });
436 if at_least_one_child_is_formattable {
437 all_children_have_matching_command_values
438 } else {
439 active_range
440 .start_container()
441 .effective_command_value(self)
442 .is_some_and(|effective_value| {
443 inline_command_activated_values.contains(&&*effective_value.str())
444 })
445 }
446 },
447 })
448 }
449
450 pub(crate) fn current_value(
452 &self,
453 cx: &mut JSContext,
454 document: &Document,
455 ) -> Option<DOMString> {
456 Some(match self {
457 CommandName::DefaultParagraphSeparator => {
458 document.default_single_line_container_name().into()
461 },
462 CommandName::FontSize => value_for_fontsize_command(cx, document)?,
463 _ if self.is_standard_inline_value_command() => {
464 let selection = document.GetSelection(cx)?;
470 let active_range = selection.active_range()?;
471
472 active_range
473 .first_formattable_contained_node()
474 .unwrap_or_else(|| active_range.start_container())
475 .effective_command_value(self)
476 .unwrap_or_default()
477 },
478 _ => return None,
479 })
480 }
481
482 pub(crate) fn are_equivalent_values(
484 &self,
485 first: Option<&DOMString>,
486 second: Option<&DOMString>,
487 ) -> bool {
488 match (first, second) {
489 (None, None) => true,
491 (Some(first_str), Some(second_str)) => {
492 match self {
494 CommandName::Bold => {
495 first_str == second_str ||
499 matches!(
500 (first_str.str().as_ref(), second_str.str().as_ref()),
501 ("bold", "700") |
502 ("700", "bold") |
503 ("normal", "400") |
504 ("400", "normal")
505 )
506 },
507 CommandName::BackColor | CommandName::ForeColor | CommandName::HiliteColor => {
508 match (
514 parse_legacy_color(&first_str.str()),
515 parse_legacy_color(&second_str.str()),
516 ) {
517 (Ok(first_legacy_color), Ok(second_legacy_color)) => {
518 first_legacy_color == second_legacy_color
519 },
520 (Err(_), Err(_)) => true,
521 _ => false,
522 }
523 },
524 _ => first_str == second_str,
526 }
527 },
528 _ => false,
529 }
530 }
531
532 pub(crate) fn are_loosely_equivalent_values(
534 &self,
535 first: Option<&DOMString>,
536 second: Option<&DOMString>,
537 ) -> bool {
538 if self.are_equivalent_values(first, second) {
540 return true;
541 }
542 if let (CommandName::FontSize, Some(first), Some(second)) = (self, first, second) {
547 font_size_loosely_equivalent(first, second)
548 } else {
549 false
550 }
551 }
552
553 fn record_current_overrides(document: &Document) -> Vec<RecordedStateOfCommand> {
555 let mut overrides = vec![];
557 if let Some(value_override) =
560 RecordedStateOfCommand::for_command_value_override(CommandName::CreateLink, document)
561 {
562 overrides.push(value_override);
563 }
564 for command in [
568 CommandName::Bold,
569 CommandName::Italic,
570 CommandName::Strikethrough,
571 CommandName::Subscript,
572 CommandName::Superscript,
573 CommandName::Underline,
574 ] {
575 if let Some(state_override) =
576 RecordedStateOfCommand::for_command_state_override(command, document)
577 {
578 overrides.push(state_override);
579 }
580 }
581 for command in [
585 CommandName::FontName,
586 CommandName::FontSize,
587 CommandName::ForeColor,
588 CommandName::HiliteColor,
589 ] {
590 if let Some(value_override) =
591 RecordedStateOfCommand::for_command_value_override(command, document)
592 {
593 overrides.push(value_override);
594 }
595 }
596 overrides
598 }
599
600 pub(crate) fn relevant_css_property(&self) -> Option<CssPropertyName> {
602 Some(match self {
605 CommandName::BackColor => CssPropertyName::BackgroundColor,
606 CommandName::Bold => CssPropertyName::FontWeight,
607 CommandName::FontName => CssPropertyName::FontFamily,
608 CommandName::FontSize => CssPropertyName::FontSize,
609 CommandName::ForeColor => CssPropertyName::Color,
610 CommandName::HiliteColor => CssPropertyName::BackgroundColor,
611 CommandName::Italic => CssPropertyName::FontStyle,
612 _ => return None,
614 })
615 }
616
617 pub(crate) fn resolved_value_for_node(&self, element: &Element) -> Option<DOMString> {
618 let property = self.relevant_css_property()?;
619 property.resolved_value_for_node(element)
620 }
621
622 pub(crate) fn is_standard_inline_value_command(&self) -> bool {
624 matches!(
625 self,
626 CommandName::BackColor |
627 CommandName::FontName |
628 CommandName::ForeColor |
629 CommandName::HiliteColor
630 )
631 }
632
633 pub(crate) fn is_enabled_in_plaintext_only_state(&self) -> bool {
634 matches!(
635 self,
636 CommandName::Copy |
637 CommandName::Cut |
638 CommandName::DefaultParagraphSeparator |
639 CommandName::FormatBlock |
640 CommandName::ForwardDelete |
641 CommandName::InsertHtml |
642 CommandName::InsertLineBreak |
643 CommandName::InsertParagraph |
644 CommandName::InsertText |
645 CommandName::Paste |
646 CommandName::Redo |
647 CommandName::StyleWithCss |
648 CommandName::Undo |
649 CommandName::Usecss |
650 CommandName::Delete
651 )
652 }
653
654 fn preserves_overrides(&self) -> bool {
656 matches!(
657 self,
658 CommandName::Delete |
659 CommandName::FormatBlock |
660 CommandName::ForwardDelete |
661 CommandName::Indent |
662 CommandName::InsertHorizontalRule |
663 CommandName::InsertHtml |
664 CommandName::InsertImage |
665 CommandName::InsertLineBreak |
666 CommandName::InsertOrderedList |
667 CommandName::InsertParagraph |
668 CommandName::InsertUnorderedList |
669 CommandName::JustifyCenter |
670 CommandName::JustifyFull |
671 CommandName::JustifyLeft |
672 CommandName::JustifyRight |
673 CommandName::Outdent
674 )
675 }
676
677 pub(crate) fn execute(
679 &self,
680 cx: &mut JSContext,
681 document: &Document,
682 selection: &Selection,
683 value: DOMString,
684 ) -> bool {
685 let overrides = if self.preserves_overrides() {
689 Self::record_current_overrides(document)
690 } else {
691 vec![]
692 };
693 let result = match self {
694 CommandName::BackColor => execute_backcolor_command(cx, document, selection, value),
695 CommandName::Bold => execute_bold_command(cx, document, selection),
696 CommandName::CreateLink => execute_createlink_command(cx, document, selection, value),
697 CommandName::DefaultParagraphSeparator => {
698 execute_default_paragraph_separator_command(document, value)
699 },
700 CommandName::Delete => execute_delete_command(cx, document, selection),
701 CommandName::FontName => execute_fontname_command(cx, document, selection, value),
702 CommandName::FontSize => execute_fontsize_command(cx, document, selection, value),
703 CommandName::ForeColor => execute_forecolor_command(cx, document, selection, value),
704 CommandName::HiliteColor => execute_hilitecolor_command(cx, document, selection, value),
705 CommandName::InsertParagraph => {
706 execute_insert_paragraph_command(cx, document, selection)
707 },
708 CommandName::Italic => execute_italic_command(cx, document, selection),
709 CommandName::RemoveFormat => execute_removeformat_command(cx, document, selection),
710 CommandName::Strikethrough => execute_strikethrough_command(cx, document, selection),
711 CommandName::StyleWithCss => execute_style_with_css_command(document, value),
712 CommandName::Subscript => execute_subscript_command(cx, document, selection),
713 CommandName::Superscript => execute_superscript_command(cx, document, selection),
714 CommandName::Underline => execute_underline_command(cx, document, selection),
715 CommandName::Unlink => execute_unlink_command(cx, selection),
716 _ => false,
717 };
718
719 if let Some(active_range) = selection
723 .active_range()
724 .filter(|active_range| active_range.collapsed())
725 {
726 active_range.restore_states_and_values(cx, selection, document, overrides);
727 }
728
729 result
730 }
731
732 pub(crate) fn inline_command_activated_values(&self) -> Vec<&str> {
734 match self {
735 CommandName::Bold => vec!["bold", "600", "700", "800", "900"],
737 CommandName::Italic => vec!["italic", "oblique"],
739 CommandName::Strikethrough => vec!["line-through"],
741 CommandName::Subscript => vec!["subscript"],
743 CommandName::Superscript => vec!["superscript"],
745 CommandName::Underline => vec!["underline"],
747 _ => vec![],
748 }
749 }
750}