html: Rewrite `FormatSpans::to_html`

The new implementation doesn't use the `xml` crate, nor `quick-xml`,
but rather just iterates the `TextSpan`s and builds the formatted HTML
string.
This commit is contained in:
relrelb 2021-11-13 01:18:51 +02:00 committed by relrelb
parent 2ad5c644b0
commit b1151b2ab2
5 changed files with 271 additions and 346 deletions

View File

@ -333,8 +333,7 @@ pub fn html_text<'gc>(
this: EditText<'gc>, this: EditText<'gc>,
activation: &mut Activation<'_, 'gc, '_>, activation: &mut Activation<'_, 'gc, '_>,
) -> Result<Value<'gc>, Error<'gc>> { ) -> Result<Value<'gc>, Error<'gc>> {
let html_text = this.html_text(&mut activation.context); Ok(AvmString::new(activation.context.gc_context, this.html_text()).into())
Ok(AvmString::new(activation.context.gc_context, html_text).into())
} }
pub fn set_html_text<'gc>( pub fn set_html_text<'gc>(

View File

@ -323,11 +323,7 @@ pub fn html_text<'gc>(
.and_then(|this| this.as_display_object()) .and_then(|this| this.as_display_object())
.and_then(|this| this.as_edit_text()) .and_then(|this| this.as_edit_text())
{ {
return Ok(AvmString::new( return Ok(AvmString::new(activation.context.gc_context, this.html_text()).into());
activation.context.gc_context,
this.html_text(&mut activation.context),
)
.into());
} }
Ok(Value::Undefined) Ok(Value::Undefined)

View File

@ -24,7 +24,6 @@ use crate::string::{utils as string_utils, AvmString, WStr, WString};
use crate::tag_utils::SwfMovie; use crate::tag_utils::SwfMovie;
use crate::transform::Transform; use crate::transform::Transform;
use crate::vminterface::{AvmObject, AvmType, Instantiator}; use crate::vminterface::{AvmObject, AvmType, Instantiator};
use crate::xml::XmlDocument;
use chrono::Utc; use chrono::Utc;
use gc_arena::{Collect, Gc, GcCell, MutationContext}; use gc_arena::{Collect, Gc, GcCell, MutationContext};
use std::{cell::Ref, cell::RefMut, sync::Arc}; use std::{cell::Ref, cell::RefMut, sync::Arc};
@ -414,19 +413,9 @@ impl<'gc> EditText<'gc> {
Ok(()) Ok(())
} }
pub fn html_text(self, context: &mut UpdateContext<'_, 'gc, '_>) -> WString { pub fn html_text(self) -> WString {
if self.is_html() { if self.is_html() {
let html_tree = self.html_tree(context).as_node(); self.0.read().text_spans.to_html()
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())
} else { } else {
// Non-HTML text fields always return plain text. // Non-HTML text fields always return plain text.
self.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 { pub fn text_length(self) -> usize {
self.0.read().text_spans.text().len() self.0.read().text_spans.text().len()
} }
@ -1129,8 +1114,6 @@ impl<'gc> EditText<'gc> {
if let Ok(Some((object, property))) = if let Ok(Some((object, property))) =
activation.resolve_variable_path(self.avm1_parent().unwrap(), &variable_path) 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 // Note that this can call virtual setters, even though the opposite direction won't work
// (virtual property changes do not affect the text field) // (virtual property changes do not affect the text field)
activation.run_with_child_frame_for_display_object( 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 property = AvmString::new(activation.context.gc_context, property);
let _ = object.set( let _ = object.set(
property, property,
AvmString::new(activation.context.gc_context, html_text).into(), AvmString::new(activation.context.gc_context, self.html_text())
.into(),
activation, activation,
); );
}, },

View File

@ -2,14 +2,14 @@
use crate::context::UpdateContext; use crate::context::UpdateContext;
use crate::html::iterators::TextSpanIter; 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::tag_utils::SwfMovie;
use crate::xml::{XmlDocument, XmlName, XmlNode}; use gc_arena::Collect;
use gc_arena::{Collect, MutationContext}; use quick_xml::{escape::escape, events::Event, Reader};
use quick_xml::events::Event;
use quick_xml::Reader;
use std::borrow::Cow; use std::borrow::Cow;
use std::cmp::{min, Ordering}; use std::cmp::{min, Ordering};
use std::collections::VecDeque;
use std::fmt::Write;
use std::sync::Arc; use std::sync::Arc;
/// Replace HTML entities with their equivalent characters. /// Replace HTML entities with their equivalent characters.
@ -1107,346 +1107,278 @@ impl FormatSpans {
/// character covered by the span, plus one) /// character covered by the span, plus one)
/// 3. The string contents of the text span /// 3. The string contents of the text span
/// 4. The formatting applied to the text span. /// 4. The formatting applied to the text span.
pub fn iter_spans(&self) -> impl Iterator<Item = (usize, usize, &WStr, &TextSpan)> { pub fn iter_spans(&self) -> TextSpanIter {
TextSpanIter::for_format_spans(self) TextSpanIter::for_format_spans(self)
} }
#[allow(clippy::float_cmp)] pub fn to_html(&self) -> WString {
pub fn raise_to_html<'gc>(&self, mc: MutationContext<'gc, '_>) -> XmlDocument<'gc> { let mut spans = self.iter_spans();
let document = XmlDocument::new(mc); let mut state = if let Some((_start, _end, text, span)) = spans.next() {
let mut root = document.as_node(); 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. state.close_tags();
//Some of them nest within themselves, but we only store the last one, state.result
//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;
for (start, _end, text, span) in self.iter_spans() { /// Holds required state for HTML formatting.
let ls = &last_span.unwrap(); struct FormatState<'a> {
result: WString,
font_stack: VecDeque<&'a TextSpan>,
span: &'a TextSpan,
is_open: bool,
}
if ls.left_margin != span.left_margin impl<'a> FormatState<'a> {
|| ls.right_margin != span.right_margin fn open_tags(&mut self) {
|| ls.indent != span.indent if self.is_open {
|| ls.block_indent != span.block_indent return;
|| ls.leading != span.leading }
|| ls.tab_stops != span.tab_stops
|| last_text_format_element.is_none() 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()
{ {
let new_tf = XmlNode::new_element(mc, "TEXTFORMAT".into(), document); self.result.push_str(WStr::from_units(b"<TEXTFORMAT"));
if self.span.left_margin != 0.0 {
if ls.left_margin != 0.0 { let _ = write!(self.result, " LEFTMARGIN=\"{}\"", self.span.left_margin);
new_tf.set_attribute_value(
mc,
XmlName::from_str("LEFTMARGIN"),
AvmString::new_utf8(mc, span.left_margin.to_string()),
);
} }
if self.span.right_margin != 0.0 {
if ls.right_margin != 0.0 { let _ = write!(self.result, " RIGHTMARGIN=\"{}\"", self.span.right_margin);
new_tf.set_attribute_value(
mc,
XmlName::from_str("RIGHTMARGIN"),
AvmString::new_utf8(mc, span.right_margin.to_string()),
);
} }
if self.span.indent != 0.0 {
if ls.indent != 0.0 { let _ = write!(self.result, " INDENT=\"{}\"", self.span.indent);
new_tf.set_attribute_value(
mc,
XmlName::from_str("INDENT"),
AvmString::new_utf8(mc, span.indent.to_string()),
);
} }
if self.span.leading != 0.0 {
if ls.block_indent != 0.0 { let _ = write!(self.result, " LEADING=\"{}\"", self.span.leading);
new_tf.set_attribute_value(
mc,
XmlName::from_str("BLOCKINDENT"),
AvmString::new_utf8(mc, span.block_indent.to_string()),
);
} }
if self.span.block_indent != 0.0 {
if ls.leading != 0.0 { let _ = write!(self.result, " BLOCKINDENT=\"{}\"", self.span.block_indent);
new_tf.set_attribute_value(
mc,
XmlName::from_str("LEADING"),
AvmString::new_utf8(mc, span.leading.to_string()),
);
} }
if !self.span.tab_stops.is_empty() {
if !ls.tab_stops.is_empty() { let _ = write!(
let tab_stops = span self.result,
" TABSTOPS=\"{}\"",
self.span
.tab_stops .tab_stops
.iter() .iter()
.map(f64::to_string) .map(f64::to_string)
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(","); .join(",")
new_tf.set_attribute_value(
mc,
XmlName::from_str("TABSTOPS"),
AvmString::new_utf8(mc, tab_stops),
); );
} }
self.result.push_byte(b'>');
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();
} }
let mut can_span_create_bullets = start == 0; if self.span.bullet {
for line in text.split([b'\n', b'\r'].as_ref()) { self.result.push_str(WStr::from_units(b"<LI>"));
if can_span_create_bullets && span.bullet } else {
|| !can_span_create_bullets && last_span.map(|ls| ls.bullet).unwrap_or(false) let _ = write!(
{ self.result,
let new_li = XmlNode::new_element(mc, "LI".into(), document); "<P ALIGN=\"{}\">",
match self.span.align {
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 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::Left => "LEFT",
swf::TextAlign::Center => "CENTER", swf::TextAlign::Center => "CENTER",
swf::TextAlign::Right => "RIGHT", swf::TextAlign::Right => "RIGHT",
swf::TextAlign::Justify => "JUSTIFY", swf::TextAlign::Justify => "JUSTIFY",
}; }
);
new_p.set_attribute_value(mc, XmlName::from_str("ALIGN"), align.into());
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 ls.font != span.font let _ = write!(
|| ls.size != span.size self.result,
|| ls.color != span.color "<FONT FACE=\"{}\" SIZE=\"{}\" COLOR=\"#{:0>2X}{:0>2X}{:0>2X}\" LETTERSPACING=\"{}\" KERNING=\"{}\">",
|| ls.letter_spacing != span.letter_spacing self.span.font,
|| ls.kerning != span.kerning self.span.size,
|| last_font.is_none() 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);
if !self.span.url.is_empty() {
let _ = write!(
self.result,
"<A HREF=\"{}\" TARGET=\"{}\">",
self.span.url, self.span.target
);
}
if self.span.bold {
self.result.push_str(WStr::from_units(b"<B>"));
}
if self.span.italic {
self.result.push_str(WStr::from_units(b"<I>"));
}
if self.span.underline {
self.result.push_str(WStr::from_units(b"<U>"));
}
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"</U>"));
}
if self.span.italic {
self.result.push_str(WStr::from_units(b"</I>"));
}
if self.span.bold {
self.result.push_str(WStr::from_units(b"</B>"));
}
if !self.span.url.is_empty() {
self.result.push_str(WStr::from_units(b"</A>"));
}
self.result
.push_str(&WStr::from_units(b"</FONT>").repeat(self.font_stack.len()));
self.font_stack.clear();
if self.span.bullet {
self.result.push_str(WStr::from_units(b"</LI>"));
} else {
self.result.push_str(WStr::from_units(b"</P>"));
}
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()
{ {
let new_font = XmlNode::new_element(mc, "FONT".into(), document); self.result.push_str(WStr::from_units(b"</TEXTFORMAT>"));
if ls.font != span.font || last_font.is_none() {
new_font.set_attribute_value(
mc,
XmlName::from_str("FACE"),
AvmString::new(mc, span.font.clone()),
);
} }
if ls.size != span.size || last_font.is_none() { self.is_open = false;
new_font.set_attribute_value(
mc,
XmlName::from_str("SIZE"),
AvmString::new_utf8(mc, span.size.to_string()),
);
} }
if ls.color != span.color || last_font.is_none() { fn set_span(&mut self, span: &'a TextSpan) {
let color = format!( if !span.underline && self.span.underline {
"#{:0>2X}{:0>2X}{:0>2X}", self.result.push_str(WStr::from_units(b"</U>"));
}
if !span.italic && self.span.italic {
self.result.push_str(WStr::from_units(b"</I>"));
}
if !span.bold && self.span.bold {
self.result.push_str(WStr::from_units(b"</B>"));
}
if span.url != self.span.url && !self.span.url.is_empty() {
self.result.push_str(WStr::from_units(b"</A>"));
}
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"</FONT>").repeat(pos));
self.font_stack.drain(0..pos);
} else {
self.result.push_str(WStr::from_units(b"<FONT"));
if span.font != self.span.font {
let _ = write!(self.result, " FACE=\"{}\"", span.font);
}
if span.size != self.span.size {
let _ = write!(self.result, " SIZE=\"{}\"", span.size);
}
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 span.color.r, span.color.g, span.color.b
); );
new_font.set_attribute_value( }
mc, if span.letter_spacing != self.span.letter_spacing {
XmlName::from_str("COLOR"), let _ = write!(self.result, " LETTERSPACING=\"{}\"", span.letter_spacing);
AvmString::new_utf8(mc, color), }
if span.kerning != self.span.kerning {
let _ = write!(
self.result,
" KERNING=\"{}\"",
if span.kerning { "1" } else { "0" }
);
}
self.result.push_byte(b'>');
self.font_stack.push_front(span);
}
}
if span.url != self.span.url && !span.url.is_empty() {
let _ = write!(
self.result,
"<A HREF=\"{}\" TARGET=\"{}\">",
span.url, span.target
); );
} }
if ls.letter_spacing != span.letter_spacing || last_font.is_none() { if span.bold && !self.span.bold {
new_font.set_attribute_value( self.result.push_str(WStr::from_units(b"<B>"));
mc,
XmlName::from_str("LETTERSPACING"),
AvmString::new_utf8(mc, span.letter_spacing.to_string()),
);
} }
if ls.kerning != span.kerning || last_font.is_none() { if span.italic && !self.span.italic {
new_font.set_attribute_value( self.result.push_str(WStr::from_units(b"<I>"));
mc,
XmlName::from_str("KERNING"),
if span.kerning { "1".into() } else { "0".into() },
);
} }
last_font if span.underline && !self.span.underline {
.or(last_paragraph) self.result.push_str(WStr::from_units(b"<U>"));
.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.url.is_empty() && (ls.url != span.url || last_a.is_none()) { self.span = span;
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.target.is_empty() {
new_a.set_attribute_value(
mc,
XmlName::from_str("TARGET"),
AvmString::new(mc, span.target.clone()),
);
} }
last_font fn push_text(&mut self, text: &WStr) {
.or(last_paragraph) for (i, text) in text.split(&[b'\n', b'\r'][..]).enumerate() {
.or(last_bullet) self.open_tags();
.or(last_text_format_element) if i > 0 {
.unwrap_or(root) self.close_tags();
.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;
} }
let encoded = text.to_utf8_lossy();
if span.bold && last_b.is_none() { let escaped = escape(encoded.as_bytes());
let new_b = XmlNode::new_element(mc, "B".into(), document); self.result.push_str(WStr::from_units(&*escaped));
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.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.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;
}
}
document
} }
} }

View File

@ -399,3 +399,17 @@ impl AsMut<WStr> for WString {
self.deref_mut() 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(())
}
}