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.
This commit is contained in:
David Wendt 2020-06-20 00:29:39 -04:00
parent e036d6594b
commit c1ad37a0f6
7 changed files with 307 additions and 6 deletions

View File

@ -84,13 +84,13 @@ pub fn set_html<'gc>(
pub fn get_html_text<'gc>( pub fn get_html_text<'gc>(
_avm: &mut Avm1<'gc>, _avm: &mut Avm1<'gc>,
_context: &mut UpdateContext<'_, 'gc, '_>, context: &mut UpdateContext<'_, 'gc, '_>,
this: Object<'gc>, this: Object<'gc>,
_args: &[Value<'gc>], _args: &[Value<'gc>],
) -> Result<ReturnValue<'gc>, Error> { ) -> Result<ReturnValue<'gc>, Error> {
if let Some(display_object) = this.as_display_object() { if let Some(display_object) = this.as_display_object() {
if let Some(text_field) = display_object.as_edit_text() { 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); let html_string_result = html_tree.into_string(&mut |_node| true);
if let Err(err) = &html_string_result { if let Err(err) = &html_string_result {

View File

@ -244,9 +244,8 @@ impl<'gc> EditText<'gc> {
Ok(()) Ok(())
} }
pub fn html_tree(self) -> XMLDocument<'gc> { pub fn html_tree(self, context: &mut UpdateContext<'_, 'gc, '_>) -> XMLDocument<'gc> {
//TODO: This should raise the text span representation. self.0.read().text_spans.raise_to_html(context.gc_context)
self.0.read().document
} }
/// Set the HTML tree for the given display object. /// Set the HTML tree for the given display object.

View File

@ -4,7 +4,7 @@ use crate::context::UpdateContext;
use crate::html::iterators::TextSpanIter; use crate::html::iterators::TextSpanIter;
use crate::tag_utils::SwfMovie; use crate::tag_utils::SwfMovie;
use crate::xml::{Step, XMLDocument, XMLName, XMLNode}; use crate::xml::{Step, XMLDocument, XMLName, XMLNode};
use gc_arena::Collect; use gc_arena::{Collect, MutationContext};
use std::cmp::{min, Ordering}; use std::cmp::{min, Ordering};
use std::sync::Arc; 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::<Vec<_>>()
.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
}
} }

View File

@ -202,6 +202,7 @@ swf_tests! {
(edittext_leading, "avm1/edittext_leading", 1), (edittext_leading, "avm1/edittext_leading", 1),
#[ignore] (edittext_newlines, "avm1/edittext_newlines", 1), #[ignore] (edittext_newlines, "avm1/edittext_newlines", 1),
(edittext_html_entity, "avm1/edittext_html_entity", 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. // TODO: These tests have some inaccuracies currently, so we use approx_eq to test that numeric values are close enough.

View File

@ -0,0 +1,17 @@
//trace(test.htmlText);
<TEXTFORMAT LEADING="2"><P ALIGN="LEFT"><FONT FACE="Times New Roman" SIZE="12" COLOR="#000000" LETTERSPACING="0" KERNING="1">test</FONT></P></TEXTFORMAT>
//test.htmlText = ...;
//trace(test.htmlText);
<TEXTFORMAT LEFTMARGIN="5" LEADING="2"><P ALIGN="LEFT"><FONT FACE="Times New Roman" SIZE="12" COLOR="#000000" LETTERSPACING="0" KERNING="0">hello <B>bolded</B> world</FONT></P></TEXTFORMAT><TEXTFORMAT LEADING="2"><P ALIGN="LEFT"><FONT FACE="Times New Roman" SIZE="12" COLOR="#000000" LETTERSPACING="0" KERNING="0">next line</FONT></P></TEXTFORMAT>
//test.setTextFormat(6, 12, unbold_tf);
//trace(test.htmlText);
<TEXTFORMAT LEFTMARGIN="5" LEADING="2"><P ALIGN="LEFT"><FONT FACE="Times New Roman" SIZE="12" COLOR="#000000" LETTERSPACING="0" KERNING="0">hello bolded world</FONT></P></TEXTFORMAT><TEXTFORMAT LEADING="2"><P ALIGN="LEFT"><FONT FACE="Times New Roman" SIZE="12" COLOR="#000000" LETTERSPACING="0" KERNING="0">next line</FONT></P></TEXTFORMAT>
//test.setTextFormat(4, 10, noto_tf);
//trace(test.htmlText);
<TEXTFORMAT LEFTMARGIN="5" LEADING="2"><P ALIGN="LEFT"><FONT FACE="Times New Roman" SIZE="12" COLOR="#000000" LETTERSPACING="0" KERNING="0">hell<FONT FACE="Noto Sans">o bold</FONT>ed world</FONT></P></TEXTFORMAT><TEXTFORMAT LEADING="2"><P ALIGN="LEFT"><FONT FACE="Times New Roman" SIZE="12" COLOR="#000000" LETTERSPACING="0" KERNING="0">next line</FONT></P></TEXTFORMAT>
//test.setTextFormat(0, 300, center_tf);
//trace(test.htmlText);
<TEXTFORMAT LEFTMARGIN="5" LEADING="2"><P ALIGN="CENTER"><FONT FACE="Times New Roman" SIZE="12" COLOR="#000000" LETTERSPACING="0" KERNING="0">hell<FONT FACE="Noto Sans">o bold</FONT>ed world</FONT></P></TEXTFORMAT><TEXTFORMAT LEADING="2"><P ALIGN="CENTER"><FONT FACE="Times New Roman" SIZE="12" COLOR="#000000" LETTERSPACING="0" KERNING="0">next line</FONT></P></TEXTFORMAT>
//test.setTextFormat(8, 15, center_tf);
//trace(test.htmlText);
<TEXTFORMAT LEFTMARGIN="5" LEADING="2"><P ALIGN="CENTER"><FONT FACE="Times New Roman" SIZE="12" COLOR="#000000" LETTERSPACING="0" KERNING="0">hell<FONT FACE="Noto Sans">o bo<FONT SIZE="14">ld<FONT FACE="Times New Roman">ed wo</FONT></FONT></FONT>rld</FONT></P></TEXTFORMAT><TEXTFORMAT LEADING="2"><P ALIGN="CENTER"><FONT FACE="Times New Roman" SIZE="12" COLOR="#000000" LETTERSPACING="0" KERNING="0">next line</FONT></P></TEXTFORMAT>

Binary file not shown.

Binary file not shown.