Script syntax coloring for an editable TextField

Syntax coloring makes a code developer’s life much easier. For those unfamiliar with the concept, consider how ActionScript renders in shades of blue with green strings (by default) within the Flash Actions panel. That color-coding helps focus the eye on relevant keywords in the script.

As I’ve been moving forward on developing Lassie Shepherd’s script panel, I wanted to incorporate some form of syntax coloring to aid the game developer. So, I wrote up a quick class to parse and apply keyword coloring based on standard scripting syntax. Valid keywords can be assigned within the class’ static “keywords” array. Also, this generation of the script only accepts single quotes (apostrophes) as string delimiters.

1) Save as: com/gmacwilliam/parse/SyntaxColoring.as

package com.gmacwilliam.parse
{
    import flash.text.TextField;
    import flash.text.TextFormat;

    public class SyntaxColoring
    {
        public static var keywords:Array = new Array("if", "gotoAndStop", "play", "true", "false");
        public static var backgroundColor:Number = 0xFFFFFF;
        public static var foregroundColor:Number = 0x000000;
        public static var keywordColor:Number = 0x0000FF;
        public static var stringColor:Number = 0x009900;

        public static function renderField(tf:TextField):void
        {
            // apply field background color
            tf.backgroundColor = backgroundColor;

            // set field-read starting index
            var index:int = 0;

            // render all lines of text
            while(index < tf.text.length)
            {
                index = renderLine(tf, index) + 1;
            }
        }

        public static function renderLine(tf:TextField, caretIndex:int):int
        {
            // create text formats
            var colorBase:TextFormat = tf.getTextFormat();
            colorBase.color = foregroundColor;

            var colorKeyword:TextFormat = tf.getTextFormat();
            colorKeyword.color = keywordColor;

            var colorString:TextFormat = tf.getTextFormat();
            colorString.color = stringColor;

            // get start and end indicies of the current line
            var sindex:int = tf.getFirstCharInParagraph(caretIndex);
            var eindex:int = sindex + tf.getParagraphLength(caretIndex);
            eindex = Math.min(eindex, tf.text.length);

            // if paragraph has any text
            if (sindex < eindex)
            {
                // apply black default text format
                tf.setTextFormat(colorBase, sindex, eindex);

                // extract source block of text to format
                var src:String = tf.text.substr(sindex, eindex-sindex);

                // strip all parenthesis and spaces from line and split into an array
                var raw:String = src.split(" ").join("");
                raw = raw.split("(").join(",");
                raw = raw.split(")").join(",");
                var lineWords:Array = raw.split(",");
                var index:int = 0;

                // KEYWORDS
                // loop through array of line words
                for (var j:int = 0; j < lineWords.length; j++)
                {
                    var word:String = lineWords[j];

                    // color any words that match a keyword
                    if (keywords.indexOf(word) > -1)
                    {
                        index = src.indexOf(word, index);
                        tf.setTextFormat(colorKeyword, sindex+index, sindex+index+word.length);
                    }
                }

                // STRINGS
                // reset render index
                index = 0;

                while(index > -1)
                {
                    // find index of next quote
                    index = src.indexOf("'", index);

                    if (index > -1)
                    {
                        // if quote was found...
                        var qo:int = index; // quote open
                        var qc:int = src.indexOf("'", index+1); // quote close
                        var so:int = sindex+qo; // string open
                        var sc:int = (qc < 0) ? eindex : so+(qc-qo)+1; // string close

                        // apply text color to closed string or to everything after quote
                        tf.setTextFormat(colorString, so, sc);

                        // update render index with close-quote position
                        index = (qc < 0) ? -1 : qc+1;
                    }
                }
            }

            // return end-index of rendered text line
            return eindex;
        }
    }
}

Implementation of this class on a TextField is very simple, and the script is surprisingly efficient. Since AS3 can now track the caret’s line index within an editable text field, the full field only needs to be parsed and rendered once on initial load. After that, only the line containing the caret needs to be re-rendered when the field is changed. Here’s the implementation:

import com.gmacwilliam.parse.SyntaxColoring;

var color_txt:TextField; // << create an editable text field on stage
color_txt.addEventListener(Event.CHANGE, this.handleColorize);

// render field with initial formatting
SyntaxColoring.renderField(color_txt);

// handle field updates
function handleColorize(evt:Event):void
{
    SyntaxColoring.renderLine(color_txt, color_txt.caretIndex);
}

Wire it up and try entering “gotoAndStop(‘sfoo’);” in your editable TextField. It should look quite a bit like ActionScript!

Advertisements

4 comments so far

  1. Tucker on

    These posts make my head hurt.
    I’m quite amazed you can figure this stuff out.

  2. Og2t on

    Hey Greg, I just found your blog by chance, really nice articles, tips and classes – it would be awesome if you would zip a few source files and provide some online demos of your classes – usually I don’t have that much time to copy/paste all the files, then recreate and compile everything and I think it’s a shame because your solutions are very good and could help a lot. Please consider that option. I am adding your blog to my RSS now.

    Cheers,
    Tomek

    • bigmac on

      Very good point! Unfortunately, I’m in about the same boat – I don’t bother with the zips and demo posts since I’m generating content in a hurry.

  3. kabuby on

    Your code is simple and works great! I was looking for something like this! Thanks


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: