Tuesday, July 22, 2008

An ActionScript Class for Converting Between HTML and Plain Text

I've written an HtmlConverter class which allows ActionScript to convert between HTML and plain text, in both directions, without using regular expressions.

The main functionality is provided by the TextInput component since it automatically converts plain text to HTML, and vice versa. The tricky part is that this conversion only happens when the TextInput control is on the stage. (After looking at the code for the TextInput control, my sense is that the conversion is done by the flash player, not by the control.) In order to minimize the impact of using the control for the conversion, the HtmlConverter object adds a TextInput control to the stage, waits for it to perform the conversion, and then immediately removes it from the stage.

Since we have to wait for the TextInput control to be fully placed on the stage, and to perform the conversion, the result of the conversion will not be available until at least one frame after the conversion is requested. This means that interactions with the HtmlConverter object have to be asynchronous: i.e. they have to use events rather than return values from functions.

Here is the file HtmlConverter.as:

package com.craftyspace.utilities {
import flash.events.EventDispatcher;

[Event(name="textConverted", type="flash.events.Event")]
public class HtmlConverter extends EventDispatcher {
import flash.events.Event;
import mx.events.FlexEvent;
import mx.controls.TextInput;
import mx.core.Application;
import mx.binding.utils.ChangeWatcher;

public static const TEXT_CONVERTED:String = "textConverted";

[Bindable(event="htmlTextChanged")]
public function get htmlText():String {
return this.component.htmlText;
}

[Bindable(event="textChanged")]
public function get text():String {
return this.component.text;
}

private var component:TextInput = new TextInput();

public function HtmlConverter() {
super(null);
}

public function toHtml(plain:String):void {
this.component.text = plain;
this.component.addEventListener(FlexEvent.UPDATE_COMPLETE, onComplete);
Application.application.addChild(this.component);
}

public function toPlain(HTML:String):void {
this.component.htmlText = HTML;
this.component.addEventListener(FlexEvent.UPDATE_COMPLETE, onComplete);
Application.application.addChild(this.component);
}

private function onComplete(event:FlexEvent):void {
component.removeEventListener(FlexEvent.UPDATE_COMPLETE, onComplete);
Application.application.removeChild(this.component);
this.dispatchEvent(new Event("htmlTextChanged"));
this.dispatchEvent(new Event("textChanged"));
this.dispatchEvent(new Event("textConverted"));
}

}
}

Within ActionScript, one would use the class's TEXT_CONVERTED event to get the results, like this:

private function init():void {
var converter:HtmlConverter = new HtmlConverter();
converter.addEventListener(HtmlConverter.TEXT_CONVERTED, onTextConverted);
converter.toPlain('<p><b>my</b> text</p>');
}

private function onTextConverted(event:Event):void {
trace((event.target as HtmlConverter).text);
trace((event.target as HtmlConverter).htmlText);
}

Alternatively, instead of handling the HtmlConverter.TEXT_CONVERTED event, one can bind to either the text or htmlText property of an HtmlConverter object:

<mx:Script>
<![CDATA[
import com.craftyspace.utilities.HtmlConverter;

[Bindable]
private var converter:HtmlConverter;

private function init():void {
converter = new HtmlConverter();
converter.toPlain('<p><b>my</b> text</p>');
}
]]>
</mx:Script>
<mx:Text text="{'html: ' + converter.htmlText}" />
<mx:Text text="{'plain: ' + converter.text}" />

Note that the above code stores the HtmlConverter object in a persistent variable that is declared bindable. If one does not do this, the MXML controls will not be able to bind to the HtmlConverter's properties.

One thing that cannot be done with an HtmlConverter object is to declare it as an MXML tag. This is because the HtmlConverter class is not a descendent of the UIComponent class.

Caveats

Although it was interesting going through the process of writing this class, I suspect that most of the time it would make more sense to use regular expressions.

Putting a TextInput control on the stage for a couple of frames each time it does a conversion has two costs. It requires the flash player to re-render twice: once to display the TextInput control, and once to remove it from the display. It may be the case that this could be mitigated by setting styles on the control, but I have not explored that.

The second cost is that the conversion must be dealt with asynchronously using events. Needless to say, this is significantly less convenient than a situation where one can do something like the following:

myPlaintext:String = myRegexBasedConverter.toPlain(myHtmlText);

and

myHtmlText = myRegexBasedConverter.toHtml(myPlaintext);

Of course, if someone wants not just any HTML, but the HTML which would be rendered by the flash player, then this class might be useful.

No comments: