Flash TextField <img>

This wasn't the first time I struggled to incorporate <img> tag support into a project, but it might be the last. The bugs with this "feature" are so bad, that for all but the simplest of cases (and some of those), it's rendered completely useless. Today, I worked around another problem with links exhibiting wild layout schizophrenia. This time, it would appear to be a problem with the combination of a:hover styles and <img> tags.

Now, I've held back talking trash on the TextField because of all the version 10 metric goodness I'd heard we were getting. Finally the TextField has gotten some much needed attention. Decent support for multiple languages going different directions in the same document. Solid, hard stuff. So surely Adobe fixed all (some of?) those bugs that have been around for, what, 2, 3, maybe even 4 versions (god I'm getting old)? ... No.

Can we maybe have html support that doesn't suck, with stylesheet support that doesn't suck? Together? At the same time? I'll give you a pass on font embedding even though that's been ... superbly difficult even longer because I understand the pressure from the foundaries.

In our next episode, I'll show you how to carve up your htmlText into multiple TextFields so your links stay where you put them (I tried everything else). Because what else are you going to do but hack around it?

Load HTML from XML – Part II

Update: had to take another look at this due to Flash busting my character entities!

In the last installment, I showed you a function that will walk through an XML node's (multi-part) children and return an HTML string. This approach is unfortunately flawed -- whitespace collapsing is a bit over-eager resulting in XML such as the following:
This is a <a href="page.html">link</a>

converting to:
This is alink
(Note the missing space.)

Fortunately, I have not only a solution, but since it uses regex, it ought to be a good bit more efficient:

package {

	import StringUtils;
	import CharacterEntity;

	public class XmlUtil {

		static public function getHTMLContent (xml:*):String {
			//trace (typeof(xml) + "   " + xml.toXMLString())

			if (typeof (xml) == 'string') xml = new XML(xml)

			var html = ""
			var prettyPrint = XML.prettyPrinting
			XML.prettyPrinting = false
			var ignoreWhite = XML.ignoreWhitespace
			XML.ignoreWhitespace = false

			var children = xml.children()
			var len = children.length()
			if (len)
			{
				//trace ('Multiple Children')
				for ( var i=0; i<len; i++ )
				{
					var decoded = CharacterEntity.decodeXHTML(children[i].toXMLString() , true)
					html += decoded
				}
				html = StringUtils.removeExtraWhitespace( html )

			}
			else
			{
				//trace ('Simple Content')
				var str = StringUtils.removeExtraWhitespace( CharacterEntity.decodeXHTML(xml.toXMLString(), true) )
				html += str
			}

			XML.prettyPrinting = prettyPrint
			XML.ignoreWhitespace = ignoreWhite

			//logger.info ("HTML " + escape(html))

			return html
		}
	}
}

You'll need two libraries: StringUtils from the worship-worthy studio of Grant Skinner CharacterEntity, originally written for AS2 by Jim Cheng and kindly converted to AS3 by Thirdparty Labs.

The code is a lot simpler now, but for completeness, I'll give you a quick run-down. If you pass in a String (accessing an attribute or text node could actually cause this), we convert it to XML first. First, we turn ignoreWhitespace off since it's the source of the issue above. Walk through the children (if they exist) decoding the entities and remove any additional whitespace. The "true" parameter on the decodeXHTML method is explained in this post.

Load HTML from XML source – Part I

UPDATE:
I managed to forget TextField.condenseWhite = true!

Now, although that will handle 90% of your XML -> HTML whitespace issues. There are a few scenarios where the hints in this series come in handy:

  • You want more control over XML -> HTML tag handling. For instance you want to avoid an extraneous wrapping tag.
  • The CharacterEntity class is still relevant and useful.
  • You're using TextField.styleSheet. If you're not using a stylesheet, you can set the htmlText and then access the html with collapsed whitespace by reading the htmlText property back. If you do use a stylesheet, sniffing TextField.htmlText won't show collapsed whitespace).
  • We often, pull our dynamic content into Flash via XML. A Lot. As in pretty much exclusively. Sometimes, we'll have honest-to-goodness information architecture to carve up that data semantically. We end up with a structured tree of simple text nodes. The XML parsing routine figures out how to turn this into a view. Great for all kinds of reasons. But sometimes, we just want to treat a node as a block of HTML and pass it into the htmlText property of a TextField.

    This is unnecessarily hard. Neither XML.toString() nor XML.toXMLString() do what you want for this case. But the following function will:

    private function getHTMLContent (xmlString:String):String
    {
      var html = ""
      var prettyPrint = XML.prettyPrinting
      XML.prettyPrinting = false
      var ignoreWhite = XML.ignoreWhitespace
      XML.ignoreWhitespace = true

      var xml:XML = new XML (xmlString)
      var children = xml.children()

      if (children.length())
      {
        for each (var i in children)
        {
          html += i.toXMLString()
        }
      }
      else
      {
        html += xml
      }

      XML.prettyPrinting = prettyPrint
      XML.ignoreWhitespace = ignoreWhite

      return html
    }

    To use it, pass the XML node containing the HTML, and the function will return the appropriate HTML String. Let's say your XML looks like:

    <services>
    <description>Lorem ipsum dolor sit amet, consectetur adipiscing elit.<br /><br />Aliquam in lectus quis nisl lacinia dignissim.
    <ul>
      <li>Proin viverra.</li>
      <li>Phasellus tristique.</li>
    </ul>
    </description>
    </services>

    Then, after loading the XML document, you'd do something like:

    var desc = getHTMLContent(myXML.description)
    content_tf.condenseWhite = true
    content_tf.htmlText = desc

    If you skip this step, you'll end up including the description node (which isn't what you want) or mangling the mixed-content nature (omitting the br tags or the parent-less "Lorem ipsum" opening sentence or something equally as wrong). The script should also works if your content is a straight-forward text node.

    The prettyPrint and ignoreWhitespace stuff keeps your whitespace from wreaking havok with Flash's HTML format (which is different from a browser in that it'll happily add extra newlines when they appear in the source).

    Next time, I'll incorporate Rob's entity decoding script since Flash's built-in entity support is pretty lacking.

    FDT trace w/o Debug Revisited

    My last post on this subject outlined a pretty good solution to this problem. I had one minor annoyance, and the further I chased it, the further from my original solution I drifted. Tail-ing the Flash debug log works great. It's fast, simple, easy to set up -- but I wasn't quite satisfied. I wanted to clear the log at compile so that Eclipse's console window would only contain text from the current run. Clearing the log file is easy enough, but getting Eclipse's console window to refresh turned out to be next to impossible -- at least via an Ant workflow (comments very welcome).

    I dropped a note on the FDT message board, and one of the moderators pointed me to SosMax -- their free XML-based logging server. I was immediately resistant. That's just way way too much overhead for something as simple as trace output. But I gave it a chance, and am I ever glad I did. After struggling with the initial setup (as I always do with these packages), I quickly became a convert. SosMax really is a sweet little system, and after a bit of hackery, I was able to get it incorporated into my project without converting all my trace statements to myLogger.debug calls.

    In addition to the great filtering, color-coding, etc features you'd expect with a custom logger, SosMax will also pull the trace log file, and it has an option for clearing the console automatically when a new connection is established. Ah ha!

    Rather than write a log connector class from scratch, I downloaded one from Sönke Rohde . His class was almost perfect for my needs: by default, he only connects to the logging server if there's a message to be sent (ie, the first time you attempt to send a message). I only wanted to connect for the purpose of clearing the console, so I moved the connection logic into its own public method. Then, in my Main class:


    import com.soenkerohde.logging.SOSLoggingTarget; 

    import mx.logging.Log;
    import mx.logging.ILogger;
    import mx.logging.LogEventLevel;

    public class Main {

    private static const logger:ILogger = Log.getLogger("Main");

    public function Main()
      {

       var sosLoggerTarget = new SOSLoggingTarget();
       //sosLoggerTarget.includeCategory = true;
        sosLoggerTarget.includeLevel = true;

       Log.addTarget(sosLoggerTarget) 
        sosLoggerTarget.connect()

     }
    }