diff --git a/core/src/avm1/globals/text_field.rs b/core/src/avm1/globals/text_field.rs index fc9e4bb8b..6a0d8acf9 100644 --- a/core/src/avm1/globals/text_field.rs +++ b/core/src/avm1/globals/text_field.rs @@ -333,8 +333,7 @@ pub fn html_text<'gc>( this: EditText<'gc>, activation: &mut Activation<'_, 'gc, '_>, ) -> Result, Error<'gc>> { - let html_text = this.html_text(&mut activation.context); - Ok(AvmString::new(activation.context.gc_context, html_text).into()) + Ok(AvmString::new(activation.context.gc_context, this.html_text()).into()) } pub fn set_html_text<'gc>( diff --git a/core/src/avm2/globals/flash/text/textfield.rs b/core/src/avm2/globals/flash/text/textfield.rs index 5b1cfb43e..bb4b00042 100644 --- a/core/src/avm2/globals/flash/text/textfield.rs +++ b/core/src/avm2/globals/flash/text/textfield.rs @@ -323,11 +323,7 @@ pub fn html_text<'gc>( .and_then(|this| this.as_display_object()) .and_then(|this| this.as_edit_text()) { - return Ok(AvmString::new( - activation.context.gc_context, - this.html_text(&mut activation.context), - ) - .into()); + return Ok(AvmString::new(activation.context.gc_context, this.html_text()).into()); } Ok(Value::Undefined) diff --git a/core/src/display_object/edit_text.rs b/core/src/display_object/edit_text.rs index 6b72bc67b..33d72e982 100644 --- a/core/src/display_object/edit_text.rs +++ b/core/src/display_object/edit_text.rs @@ -24,7 +24,6 @@ use crate::string::{utils as string_utils, AvmString, WStr, WString}; use crate::tag_utils::SwfMovie; use crate::transform::Transform; use crate::vminterface::{AvmObject, AvmType, Instantiator}; -use crate::xml::XmlDocument; use chrono::Utc; use gc_arena::{Collect, Gc, GcCell, MutationContext}; use std::{cell::Ref, cell::RefMut, sync::Arc}; @@ -414,19 +413,9 @@ impl<'gc> EditText<'gc> { Ok(()) } - pub fn html_text(self, context: &mut UpdateContext<'_, 'gc, '_>) -> WString { + pub fn html_text(self) -> WString { if self.is_html() { - let html_tree = self.html_tree(context).as_node(); - let html_string_result = html_tree.into_string(&|_node| true); - - if let Err(err) = &html_string_result { - log::warn!( - "Serialization error when reading TextField.htmlText: {}", - err - ); - } - - WString::from_utf8_owned(html_string_result.unwrap_or_default()) + self.0.read().text_spans.to_html() } else { // Non-HTML text fields always return plain text. self.text() @@ -452,10 +441,6 @@ impl<'gc> EditText<'gc> { } } - pub fn html_tree(self, context: &mut UpdateContext<'_, 'gc, '_>) -> XmlDocument<'gc> { - self.0.read().text_spans.raise_to_html(context.gc_context) - } - pub fn text_length(self) -> usize { self.0.read().text_spans.text().len() } @@ -1129,8 +1114,6 @@ impl<'gc> EditText<'gc> { if let Ok(Some((object, property))) = activation.resolve_variable_path(self.avm1_parent().unwrap(), &variable_path) { - let html_text = self.html_text(&mut activation.context); - // Note that this can call virtual setters, even though the opposite direction won't work // (virtual property changes do not affect the text field) activation.run_with_child_frame_for_display_object( @@ -1141,7 +1124,8 @@ impl<'gc> EditText<'gc> { let property = AvmString::new(activation.context.gc_context, property); let _ = object.set( property, - AvmString::new(activation.context.gc_context, html_text).into(), + AvmString::new(activation.context.gc_context, self.html_text()) + .into(), activation, ); }, diff --git a/core/src/html/text_format.rs b/core/src/html/text_format.rs index 546be59d7..6c4368cc1 100644 --- a/core/src/html/text_format.rs +++ b/core/src/html/text_format.rs @@ -2,14 +2,14 @@ use crate::context::UpdateContext; use crate::html::iterators::TextSpanIter; -use crate::string::{AvmString, Integer, Units, WStr, WString}; +use crate::string::{Integer, Units, WStr, WString}; use crate::tag_utils::SwfMovie; -use crate::xml::{XmlDocument, XmlName, XmlNode}; -use gc_arena::{Collect, MutationContext}; -use quick_xml::events::Event; -use quick_xml::Reader; +use gc_arena::Collect; +use quick_xml::{escape::escape, events::Event, Reader}; use std::borrow::Cow; use std::cmp::{min, Ordering}; +use std::collections::VecDeque; +use std::fmt::Write; use std::sync::Arc; /// Replace HTML entities with their equivalent characters. @@ -1107,346 +1107,278 @@ impl FormatSpans { /// character covered by the span, plus one) /// 3. The string contents of the text span /// 4. The formatting applied to the text span. - pub fn iter_spans(&self) -> impl Iterator { + pub fn iter_spans(&self) -> TextSpanIter { TextSpanIter::for_format_spans(self) } - #[allow(clippy::float_cmp)] - pub fn raise_to_html<'gc>(&self, mc: MutationContext<'gc, '_>) -> XmlDocument<'gc> { - let document = XmlDocument::new(mc); - let mut root = document.as_node(); + pub fn to_html(&self) -> WString { + let mut spans = self.iter_spans(); + let mut state = if let Some((_start, _end, text, span)) = spans.next() { + let mut state = FormatState { + result: WString::new(), + font_stack: VecDeque::new(), + span, + is_open: false, + }; + state.push_text(text); + state + } else { + return WString::new(); + }; - let mut last_span = self.span(0); + for (_start, _end, text, span) in spans { + state.set_span(span); + state.push_text(text); + } - //HTML elements are nested roughly in this order. - //Some of them nest within themselves, but we only store the last one, - //as Flash doesn't seem to un-nest them at all. - let mut last_text_format_element = None; - let mut last_bullet = None; - let mut last_paragraph = None; - let mut last_font = None; - let mut last_a = None; - let mut last_b = None; - let mut last_i = None; - let mut last_u = None; + state.close_tags(); + state.result + } +} - for (start, _end, text, span) in self.iter_spans() { - let ls = &last_span.unwrap(); +/// Holds required state for HTML formatting. +struct FormatState<'a> { + result: WString, + font_stack: VecDeque<&'a TextSpan>, + span: &'a TextSpan, + is_open: bool, +} - if ls.left_margin != span.left_margin - || ls.right_margin != span.right_margin - || ls.indent != span.indent - || ls.block_indent != span.block_indent - || ls.leading != span.leading - || ls.tab_stops != span.tab_stops - || last_text_format_element.is_none() - { - let new_tf = XmlNode::new_element(mc, "TEXTFORMAT".into(), document); +impl<'a> FormatState<'a> { + fn open_tags(&mut self) { + if self.is_open { + return; + } - if ls.left_margin != 0.0 { - new_tf.set_attribute_value( - mc, - XmlName::from_str("LEFTMARGIN"), - AvmString::new_utf8(mc, span.left_margin.to_string()), - ); - } - - if ls.right_margin != 0.0 { - new_tf.set_attribute_value( - mc, - XmlName::from_str("RIGHTMARGIN"), - AvmString::new_utf8(mc, span.right_margin.to_string()), - ); - } - - if ls.indent != 0.0 { - new_tf.set_attribute_value( - mc, - XmlName::from_str("INDENT"), - AvmString::new_utf8(mc, span.indent.to_string()), - ); - } - - if ls.block_indent != 0.0 { - new_tf.set_attribute_value( - mc, - XmlName::from_str("BLOCKINDENT"), - AvmString::new_utf8(mc, span.block_indent.to_string()), - ); - } - - if ls.leading != 0.0 { - new_tf.set_attribute_value( - mc, - XmlName::from_str("LEADING"), - AvmString::new_utf8(mc, span.leading.to_string()), - ); - } - - if !ls.tab_stops.is_empty() { - let tab_stops = span + if self.span.left_margin != 0.0 + || self.span.right_margin != 0.0 + || self.span.indent != 0.0 + || self.span.leading != 0.0 + || self.span.block_indent != 0.0 + || !self.span.tab_stops.is_empty() + { + self.result.push_str(WStr::from_units(b">() - .join(","); - new_tf.set_attribute_value( - mc, - XmlName::from_str("TABSTOPS"), - AvmString::new_utf8(mc, tab_stops), - ); - } - - last_text_format_element = Some(new_tf); - last_bullet = None; - last_paragraph = None; - last_font = None; - last_a = None; - last_b = None; - last_i = None; - last_u = None; - - root.append_child(mc, new_tf).unwrap(); + .join(",") + ); } + self.result.push_byte(b'>'); + } - let mut can_span_create_bullets = start == 0; - for line in text.split([b'\n', b'\r'].as_ref()) { - if can_span_create_bullets && span.bullet - || !can_span_create_bullets && last_span.map(|ls| ls.bullet).unwrap_or(false) - { - let new_li = XmlNode::new_element(mc, "LI".into(), document); - - last_bullet = Some(new_li); - last_paragraph = None; - last_font = None; - last_a = None; - last_b = None; - last_i = None; - last_u = None; - - last_text_format_element - .unwrap_or(root) - .append_child(mc, new_li) - .unwrap(); + if self.span.bullet { + self.result.push_str(WStr::from_units(b"
  • ")); + } else { + let _ = write!( + self.result, + "

    ", + match self.span.align { + swf::TextAlign::Left => "LEFT", + swf::TextAlign::Center => "CENTER", + swf::TextAlign::Right => "RIGHT", + swf::TextAlign::Justify => "JUSTIFY", } + ); + } - if ls.align != span.align || last_paragraph.is_none() { - let new_p = XmlNode::new_element(mc, "P".into(), document); - let align: &str = match span.align { - swf::TextAlign::Left => "LEFT", - swf::TextAlign::Center => "CENTER", - swf::TextAlign::Right => "RIGHT", - swf::TextAlign::Justify => "JUSTIFY", - }; + let _ = write!( + self.result, + "2X}{:0>2X}{:0>2X}\" LETTERSPACING=\"{}\" KERNING=\"{}\">", + self.span.font, + self.span.size, + self.span.color.r, + self.span.color.g, + self.span.color.b, + self.span.letter_spacing, + if self.span.kerning { "1" } else { "0" }, + ); + self.font_stack.push_front(self.span); - new_p.set_attribute_value(mc, XmlName::from_str("ALIGN"), align.into()); + if !self.span.url.is_empty() { + let _ = write!( + self.result, + "", + self.span.url, self.span.target + ); + } - last_bullet - .or(last_text_format_element) - .unwrap_or(root) - .append_child(mc, new_p) - .unwrap(); - last_paragraph = Some(new_p); - last_font = None; - last_a = None; - last_b = None; - last_i = None; - last_u = None; + if self.span.bold { + self.result.push_str(WStr::from_units(b"")); + } + + if self.span.italic { + self.result.push_str(WStr::from_units(b"")); + } + + if self.span.underline { + self.result.push_str(WStr::from_units(b"")); + } + + self.is_open = true; + } + + fn close_tags(&mut self) { + if !self.is_open { + return; + } + + if self.span.underline { + self.result.push_str(WStr::from_units(b"")); + } + + if self.span.italic { + self.result.push_str(WStr::from_units(b"")); + } + + if self.span.bold { + self.result.push_str(WStr::from_units(b"")); + } + + if !self.span.url.is_empty() { + self.result.push_str(WStr::from_units(b"")); + } + + self.result + .push_str(&WStr::from_units(b"").repeat(self.font_stack.len())); + self.font_stack.clear(); + + if self.span.bullet { + self.result.push_str(WStr::from_units(b"

  • ")); + } else { + self.result.push_str(WStr::from_units(b"

    ")); + } + + if self.span.left_margin != 0.0 + || self.span.right_margin != 0.0 + || self.span.indent != 0.0 + || self.span.leading != 0.0 + || self.span.block_indent != 0.0 + || !self.span.tab_stops.is_empty() + { + self.result.push_str(WStr::from_units(b"")); + } + + self.is_open = false; + } + + fn set_span(&mut self, span: &'a TextSpan) { + if !span.underline && self.span.underline { + self.result.push_str(WStr::from_units(b"")); + } + + if !span.italic && self.span.italic { + self.result.push_str(WStr::from_units(b"")); + } + + if !span.bold && self.span.bold { + self.result.push_str(WStr::from_units(b"")); + } + + if span.url != self.span.url && !self.span.url.is_empty() { + self.result.push_str(WStr::from_units(b"")); + } + + if span.font != self.span.font + || span.size != self.span.size + || span.color != self.span.color + || span.letter_spacing != self.span.letter_spacing + || span.kerning != self.span.kerning + { + let pos = self.font_stack.iter().position(|font| { + span.font == font.font + && span.size == font.size + && span.color == font.color + && span.letter_spacing == font.letter_spacing + && span.kerning == font.kerning + }); + if let Some(pos) = pos { + self.result + .push_str(&WStr::from_units(b"").repeat(pos)); + self.font_stack.drain(0..pos); + } else { + self.result.push_str(WStr::from_units(b"2X}{:0>2X}{:0>2X}", - span.color.r, span.color.g, span.color.b - ); - new_font.set_attribute_value( - mc, - XmlName::from_str("COLOR"), - AvmString::new_utf8(mc, color), - ); - } - - if ls.letter_spacing != span.letter_spacing || last_font.is_none() { - new_font.set_attribute_value( - mc, - XmlName::from_str("LETTERSPACING"), - AvmString::new_utf8(mc, span.letter_spacing.to_string()), - ); - } - - if ls.kerning != span.kerning || last_font.is_none() { - new_font.set_attribute_value( - mc, - XmlName::from_str("KERNING"), - if span.kerning { "1".into() } else { "0".into() }, - ); - } - - last_font - .or(last_paragraph) - .or(last_bullet) - .or(last_text_format_element) - .unwrap_or(root) - .append_child(mc, new_font) - .unwrap(); - - last_font = Some(new_font); - last_a = None; - last_b = None; - last_i = None; - last_u = None; + if span.size != self.span.size { + let _ = write!(self.result, " SIZE=\"{}\"", span.size); } - - if !span.url.is_empty() && (ls.url != span.url || last_a.is_none()) { - let new_a = XmlNode::new_element(mc, "A".into(), document); - - new_a.set_attribute_value( - mc, - XmlName::from_str("HREF"), - AvmString::new(mc, span.url.clone()), + if span.color != self.span.color { + let _ = write!( + self.result, + " COLOR=\"#{:0>2X}{:0>2X}{:0>2X}\"", + span.color.r, span.color.g, span.color.b ); - - if !span.target.is_empty() { - new_a.set_attribute_value( - mc, - XmlName::from_str("TARGET"), - AvmString::new(mc, span.target.clone()), - ); - } - - last_font - .or(last_paragraph) - .or(last_bullet) - .or(last_text_format_element) - .unwrap_or(root) - .append_child(mc, new_a) - .unwrap(); - - last_b = None; - last_i = None; - last_u = None; - } else if span.url.is_empty() && (ls.url != span.url || last_a.is_some()) { - last_a = None; - last_b = None; - last_i = None; - last_u = None; } - - if span.bold && last_b.is_none() { - let new_b = XmlNode::new_element(mc, "B".into(), document); - - last_a - .or(last_font) - .or(last_paragraph) - .or(last_bullet) - .or(last_text_format_element) - .unwrap_or(root) - .append_child(mc, new_b) - .unwrap(); - - last_b = Some(new_b); - last_i = None; - last_u = None; - } else if !span.bold && last_b.is_some() { - last_b = None; - last_i = None; - last_u = None; + if span.letter_spacing != self.span.letter_spacing { + let _ = write!(self.result, " LETTERSPACING=\"{}\"", span.letter_spacing); } - - if span.italic && last_i.is_none() { - let new_i = XmlNode::new_element(mc, "I".into(), document); - - last_b - .or(last_a) - .or(last_font) - .or(last_paragraph) - .or(last_bullet) - .or(last_text_format_element) - .unwrap_or(root) - .append_child(mc, new_i) - .unwrap(); - - last_i = Some(new_i); - last_u = None; - } else if !span.italic && last_i.is_some() { - last_i = None; - last_u = None; + if span.kerning != self.span.kerning { + let _ = write!( + self.result, + " KERNING=\"{}\"", + if span.kerning { "1" } else { "0" } + ); } - - if span.underline && last_u.is_none() { - let new_u = XmlNode::new_element(mc, "U".into(), document); - - last_i - .or(last_b) - .or(last_a) - .or(last_font) - .or(last_paragraph) - .or(last_bullet) - .or(last_text_format_element) - .unwrap_or(root) - .append_child(mc, new_u) - .unwrap(); - - last_u = Some(new_u); - } else if !span.underline && last_u.is_some() { - last_u = None; - } - - let span_text = if last_bullet.is_some() { - XmlNode::new_text(mc, AvmString::new(mc, line), document) - } else { - let line_start = line.offset_in(text).unwrap(); - let line_with_newline = if line_start > 0 { - text.slice(line_start - 1..line.len() + 1).unwrap_or(line) - } else { - line - }; - - XmlNode::new_text(mc, AvmString::new(mc, line_with_newline), document) - }; - - last_u - .or(last_i) - .or(last_b) - .or(last_a) - .or(last_font) - .or(last_paragraph) - .or(last_bullet) - .or(last_text_format_element) - .unwrap_or(root) - .append_child(mc, span_text) - .unwrap(); - - last_span = Some(span); - can_span_create_bullets = true; + self.result.push_byte(b'>'); + self.font_stack.push_front(span); } } - document + if span.url != self.span.url && !span.url.is_empty() { + let _ = write!( + self.result, + "", + span.url, span.target + ); + } + + if span.bold && !self.span.bold { + self.result.push_str(WStr::from_units(b"")); + } + + if span.italic && !self.span.italic { + self.result.push_str(WStr::from_units(b"")); + } + + if span.underline && !self.span.underline { + self.result.push_str(WStr::from_units(b"")); + } + + self.span = span; + } + + fn push_text(&mut self, text: &WStr) { + for (i, text) in text.split(&[b'\n', b'\r'][..]).enumerate() { + self.open_tags(); + if i > 0 { + self.close_tags(); + } + let encoded = text.to_utf8_lossy(); + let escaped = escape(encoded.as_bytes()); + self.result.push_str(WStr::from_units(&*escaped)); + } } } diff --git a/core/src/string/buf.rs b/core/src/string/buf.rs index 78e657a46..da80f0abc 100644 --- a/core/src/string/buf.rs +++ b/core/src/string/buf.rs @@ -399,3 +399,17 @@ impl AsMut for WString { self.deref_mut() } } + +impl std::fmt::Write for WString { + #[inline] + fn write_str(&mut self, s: &str) -> std::fmt::Result { + self.push_utf8(s); + Ok(()) + } + + #[inline] + fn write_char(&mut self, c: char) -> std::fmt::Result { + self.push_char(c); + Ok(()) + } +}