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(())
+ }
+}