From c1ad37a0f661861454590d806e17a21fb66f3f99 Mon Sep 17 00:00:00 2001 From: David Wendt Date: Sat, 20 Jun 2020 00:29:39 -0400 Subject: [PATCH] Implement text span raising, sans list items. During the raising process, we maintain a list of pointers to the lowest-most `textformat`, `p`, `font`, `a`, `b`, `i`, and `u` in the document that we are appending to. When we get a new one of any of those elements, we clear the rest off the stack. This forces us to add HTML in the same order Flash does. LIs are not yet supported because they require us to process text line-by-line which doesn't mesh with this model. There's also a test but the XML DOM generates HTML strings with the wrong attribute order, so the test fails spuriously. --- core/src/avm1/globals/text_field.rs | 4 +- core/src/display_object/edit_text.rs | 5 +- core/src/html/text_format.rs | 286 +++++++++++++++++- core/tests/regression_tests.rs | 1 + .../avm1/edittext_html_roundtrip/output.txt | 17 ++ .../avm1/edittext_html_roundtrip/test.fla | Bin 0 -> 44032 bytes .../avm1/edittext_html_roundtrip/test.swf | Bin 0 -> 3491 bytes 7 files changed, 307 insertions(+), 6 deletions(-) create mode 100644 core/tests/swfs/avm1/edittext_html_roundtrip/output.txt create mode 100644 core/tests/swfs/avm1/edittext_html_roundtrip/test.fla create mode 100644 core/tests/swfs/avm1/edittext_html_roundtrip/test.swf diff --git a/core/src/avm1/globals/text_field.rs b/core/src/avm1/globals/text_field.rs index b48c449de..459c0b2de 100644 --- a/core/src/avm1/globals/text_field.rs +++ b/core/src/avm1/globals/text_field.rs @@ -84,13 +84,13 @@ pub fn set_html<'gc>( pub fn get_html_text<'gc>( _avm: &mut Avm1<'gc>, - _context: &mut UpdateContext<'_, 'gc, '_>, + context: &mut UpdateContext<'_, 'gc, '_>, this: Object<'gc>, _args: &[Value<'gc>], ) -> Result, Error> { if let Some(display_object) = this.as_display_object() { if let Some(text_field) = display_object.as_edit_text() { - let html_tree = text_field.html_tree().as_node(); + let html_tree = text_field.html_tree(context).as_node(); let html_string_result = html_tree.into_string(&mut |_node| true); if let Err(err) = &html_string_result { diff --git a/core/src/display_object/edit_text.rs b/core/src/display_object/edit_text.rs index 3d270d618..1fb4dbe21 100644 --- a/core/src/display_object/edit_text.rs +++ b/core/src/display_object/edit_text.rs @@ -244,9 +244,8 @@ impl<'gc> EditText<'gc> { Ok(()) } - pub fn html_tree(self) -> XMLDocument<'gc> { - //TODO: This should raise the text span representation. - self.0.read().document + pub fn html_tree(self, context: &mut UpdateContext<'_, 'gc, '_>) -> XMLDocument<'gc> { + self.0.read().text_spans.raise_to_html(context.gc_context) } /// Set the HTML tree for the given display object. diff --git a/core/src/html/text_format.rs b/core/src/html/text_format.rs index ada7c6c36..b28762178 100644 --- a/core/src/html/text_format.rs +++ b/core/src/html/text_format.rs @@ -4,7 +4,7 @@ use crate::context::UpdateContext; use crate::html::iterators::TextSpanIter; use crate::tag_utils::SwfMovie; use crate::xml::{Step, XMLDocument, XMLName, XMLNode}; -use gc_arena::Collect; +use gc_arena::{Collect, MutationContext}; use std::cmp::{min, Ordering}; use std::sync::Arc; @@ -1245,4 +1245,288 @@ impl FormatSpans { }; } } + + #[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(); + + let mut last_span = self.span(0); + + //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_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() { + let ls = &last_span.unwrap(); + + 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", document); + + if ls.left_margin != 0.0 { + new_tf.set_attribute_value( + mc, + &XMLName::from_str("LEFTMARGIN"), + &format!("{}", span.left_margin), + ); + } + + if ls.right_margin != 0.0 { + new_tf.set_attribute_value( + mc, + &XMLName::from_str("RIGHTMARGIN"), + &format!("{}", span.right_margin), + ); + } + + if ls.indent != 0.0 { + new_tf.set_attribute_value( + mc, + &XMLName::from_str("INDENT"), + &format!("{}", span.indent), + ); + } + + if ls.block_indent != 0.0 { + new_tf.set_attribute_value( + mc, + &XMLName::from_str("BLOCKINDENT"), + &format!("{}", span.block_indent), + ); + } + + if ls.leading != 0.0 { + new_tf.set_attribute_value( + mc, + &XMLName::from_str("LEADING"), + &format!("{}", span.leading), + ); + } + + if !ls.tab_stops.is_empty() { + new_tf.set_attribute_value( + mc, + &XMLName::from_str("TABSTOPS"), + &span + .tab_stops + .iter() + .map(|s| format!("{}", s)) + .collect::>() + .join(","), + ); + } + + last_text_format_element = Some(new_tf); + 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(); + } + + if ls.align != span.align || last_paragraph.is_none() { + let new_p = XMLNode::new_element(mc, "P", document); + + new_p.set_attribute_value( + mc, + &XMLName::from_str("ALIGN"), + match span.align { + swf::TextAlign::Left => "LEFT", + swf::TextAlign::Center => "CENTER", + swf::TextAlign::Right => "RIGHT", + swf::TextAlign::Justify => "JUSTIFY", + }, + ); + + 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 + || ls.size != span.size + || ls.color != span.color + || ls.letter_spacing != span.letter_spacing + || ls.kerning != span.kerning + || last_font.is_none() + { + let new_font = XMLNode::new_element(mc, "FONT", document); + + if ls.font != span.font || last_font.is_none() { + new_font.set_attribute_value(mc, &XMLName::from_str("FACE"), &span.font); + } + + if ls.size != span.size || last_font.is_none() { + new_font.set_attribute_value( + mc, + &XMLName::from_str("SIZE"), + &format!("{}", span.size), + ); + } + + if ls.color != span.color || last_font.is_none() { + new_font.set_attribute_value( + mc, + &XMLName::from_str("COLOR"), + &format!( + "#{:0>2X}{:0>2X}{:0>2X}", + span.color.r, span.color.g, span.color.b + ), + ); + } + + if ls.letter_spacing != span.letter_spacing || last_font.is_none() { + new_font.set_attribute_value( + mc, + &XMLName::from_str("LETTERSPACING"), + &format!("{}", span.letter_spacing), + ); + } + + if ls.kerning != span.kerning || last_font.is_none() { + new_font.set_attribute_value( + mc, + &XMLName::from_str("KERNING"), + if span.kerning { "1" } else { "0" }, + ); + } + + last_font + .or(last_paragraph) + .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 != "" && (ls.url != span.url || last_a.is_none()) { + let new_a = XMLNode::new_element(mc, "A", document); + + new_a.set_attribute_value(mc, &XMLName::from_str("HREF"), &span.url); + + if span.target != "" { + new_a.set_attribute_value(mc, &XMLName::from_str("TARGET"), &span.target); + } + + last_font + .or(last_paragraph) + .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 == "" && (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", document); + + last_a + .or(last_font) + .or(last_paragraph) + .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", document); + + last_b + .or(last_a) + .or(last_font) + .or(last_paragraph) + .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", document); + + last_i + .or(last_b) + .or(last_a) + .or(last_font) + .or(last_paragraph) + .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 = XMLNode::new_text(mc, text, document); + last_u + .or(last_i) + .or(last_b) + .or(last_a) + .or(last_font) + .or(last_paragraph) + .or(last_text_format_element) + .unwrap_or(root) + .append_child(mc, span_text) + .unwrap(); + + last_span = Some(span); + } + + document + } } diff --git a/core/tests/regression_tests.rs b/core/tests/regression_tests.rs index f6cc5459a..3e229ad0f 100644 --- a/core/tests/regression_tests.rs +++ b/core/tests/regression_tests.rs @@ -202,6 +202,7 @@ swf_tests! { (edittext_leading, "avm1/edittext_leading", 1), #[ignore] (edittext_newlines, "avm1/edittext_newlines", 1), (edittext_html_entity, "avm1/edittext_html_entity", 1), + #[ignore] (edittext_html_roundtrip, "avm1/edittext_html_roundtrip", 1), } // TODO: These tests have some inaccuracies currently, so we use approx_eq to test that numeric values are close enough. diff --git a/core/tests/swfs/avm1/edittext_html_roundtrip/output.txt b/core/tests/swfs/avm1/edittext_html_roundtrip/output.txt new file mode 100644 index 000000000..ad5b53e17 --- /dev/null +++ b/core/tests/swfs/avm1/edittext_html_roundtrip/output.txt @@ -0,0 +1,17 @@ +//trace(test.htmlText); +

test

+//test.htmlText = ...; +//trace(test.htmlText); +

hello bolded world

next line

+//test.setTextFormat(6, 12, unbold_tf); +//trace(test.htmlText); +

hello bolded world

next line

+//test.setTextFormat(4, 10, noto_tf); +//trace(test.htmlText); +

hello bolded world

next line

+//test.setTextFormat(0, 300, center_tf); +//trace(test.htmlText); +

hello bolded world

next line

+//test.setTextFormat(8, 15, center_tf); +//trace(test.htmlText); +

hello bolded world

next line

diff --git a/core/tests/swfs/avm1/edittext_html_roundtrip/test.fla b/core/tests/swfs/avm1/edittext_html_roundtrip/test.fla new file mode 100644 index 0000000000000000000000000000000000000000..b871a47c0d48613cd6ff520a411adc639170877c GIT binary patch literal 44032 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 literal 0 HcmV?d00001