Search code examples
actionscript-3flashapache-flex

How do I get the node in an XML document when given a row and column or an index in AS3?


I have a text area that is populated with the contents of an XML document. I'm trying to get the node that the user has placed his cursor in.

I can easily get the row and column or the index of the cursor. I can also create an XML object via new XML(textarea.text).

What would be great is if there is a XML.getNodeAtPosition(index) or XML.getNodeAtPosition(row, column) method.

Here is example code:

var row:int = 100;
var column:int = 10;
var xmlText:String = fileLoader.data as String;
textarea.text = xmlText;
textarea.setAnchor(row, column); // simulate user cursor
var xml:XML = new XML(textarea.text);
var node:XML = xml.getNodeAt(row, column);

Note:
The XML may be edited by the user and may not be valid XML. I don't think Flash will create an XML object unless it is completely valid but it would be great if it was possible.


Solution

  • Oook, I made this thing working. First, let me explain the results. You must feed a valid XML string to this class:

    var S:String = '<?xml version="123" ?><node b=\'"2\'><? oook ?><innernode a = "\'1" /><!-- 123 --><![CDATA[ 456 ]]>text</node>';
    var A:ParXMLer =  new ParXMLer(S);
    

    It traverses source string and resulting XML simultaneously and produces a list of slices with begin/end text indices, a part of source text that represent the slice and a reference to appropriate XML node (yes, the leading <?xml ... ?> clause is ignored as it does not go to XML object):

    [NODE 22:107] <node b='"2'><? oook ?><innernode a = "'1" /><!-- 123 --><![CDATA[ 456 ]]>text</node>
    [HEAD 22:35] <node b='"2'>
    [PI 35:45] <? oook ?>
    [NODE 45:67] <innernode a = "'1" />
    [COMMENT 67:79] <!-- 123 -->
    [CDATA 79:96] <![CDATA[ 456 ]]>
    [TEXT 96:100] text
    [TAIL 100:107] </node>
    

    Then, the class. Use the method .nodeByIndex(index:int):XML to obtain the reference to the most relevant XML node.

    package
    {
        public class ParXMLer
        {
            public var targetXML:XML;
            public var parseIndex:int;
            public var sourceText:String;
    
            public var slices:Vector.<XMLSlice> = new Vector.<XMLSlice>();
    
            public function ParXMLer(source:String, target:XML = null)
            {
                XML.ignoreComments = false;
                XML.ignoreProcessingInstructions = false;
    
                try
                {
                    if (target == null) target = new XML(source);
                }
                catch (fail:Error)
                {
                    trace(fail);
                }
    
                parseIndex = 0;
                targetXML = target;
                sourceText = source;
    
                parseNode(targetXML);
            }
    
            // Obtain the most relevant XML node by its source string index.
            public function nodeByIndex(index:int):XML
            {
                var result:XMLSlice;
    
                for each (var aSlice:XMLSlice in slices)
                {
                    if (aSlice.begin > index) continue;
                    if (aSlice.end <= index) continue;
    
                    if (result == null)
                    {
                        result = aSlice;
                    }
                    else if (aSlice.begin > result.begin)
                    {
                        result = aSlice;
                    }
                }
    
                if (result == null) return null;
    
                return result.node;
            }
    
            public function toString():String
            {
                return slices.join("\n");
            }
    
            // Figure the given node type and parse it accordingly.
            private function parseNode(X:XML):void
            {
                var aKind:String = X.nodeKind();
    
                switch (aKind)
                {
                    case "element":
                        parseElement(X);
                        break;
    
                    case "text":
                        parseText(X);
                        break;
    
                    case "comment":
                        parseComment(X);
                        break;
    
                    case "processing-instruction":
                        parsePi(X);
                        break;
                }
            }
    
            // Parse normal XML node.
            private function parseElement(X:XML):void
            {
                var aHead:XMLSlice = parseHead(X);
    
                if (aHead.type == XMLSlice.WHOLE)
                {
                    slices.push(aHead);
                    return;
                }
    
                var result:XMLSlice = new XMLSlice();
    
                result.node = X;
                result.begin = aHead.begin;
                result.type = XMLSlice.WHOLE;
    
                slices.push(result, aHead);
    
                parseKids(X);
    
                var aTail:XMLSlice = parseTail(X);
    
                slices.push(aTail);
    
                result.end = aTail.end;
                result.text = sourceText.substring(result.begin, result.end);
            }
    
            // Parse </close> tailing tag.
            private function parseTail(X:XML):XMLSlice
            {
                var result:XMLSlice = new XMLSlice();
    
                result.node = X;
                result.type = XMLSlice.TAIL;
                result.begin = sourceText.indexOf("</", parseIndex);
    
                parseIndex = result.begin + 2;
    
                result.end = sourceText.indexOf(">", parseIndex) + 1;
    
                parseIndex = result.end;
    
                result.text = sourceText.substring(result.begin, result.end);
    
                return result;
            }
    
            // Parse XML node children.
            private function parseKids(X:XML):void
            {
                var aList:XMLList = X.children();
    
                for (var i:int = 0; i < aList.length(); i++)
                {
                    var aChild:XML = aList[i];
                    parseNode(aChild);
                }
            }
    
            // Parse XML node <open ... > tag.
            private function parseHead(X:XML):XMLSlice
            {
                var result:XMLSlice = new XMLSlice();
                var aTag:String = "<" + X.name();
    
                result.node = X;
                result.begin = sourceText.indexOf(aTag, parseIndex);
    
                parseIndex = result.begin + aTag.length;
    
                var aClause:XMLClause = avoidQuotes("/>", ">");
    
                result.end = aClause.index + aClause.text.length;
                result.text = sourceText.substring(result.begin, result.end);
    
                switch (aClause.text)
                {
                    case "/>":
                        result.type = XMLSlice.WHOLE;
                        break;
    
                    case ">":
                        result.type = XMLSlice.HEAD;
                        break;
                }
    
                return result;
            }
    
            // Search for the foremost occurrence of ANY of the given arguments.
            private function search(...rest:Array):XMLClause
            {
                var result:XMLClause = new XMLClause();
    
                for each (var anItem:String in rest)
                {
                    var anIndex:int = sourceText.indexOf(anItem, parseIndex);
    
                    if (anIndex < 0) continue;
                    if (anIndex < result.index)
                    {
                        result.index = anIndex;
                        result.text = anItem;
                    }
                }
    
                return result;
            }
    
            // Search for matching quote with regard to "\"" and '\'' cases.
            private function unquote(quote:String):void
            {
                while (true)
                {
                    var aClause:XMLClause = search("\\" + quote, quote);
    
                    parseIndex = aClause.index + aClause.text.length;
                    if (aClause.text == quote) return;
                }
            }
    
            // Find the end of the tag avoiding text in the quotes.
            private function avoidQuotes(...rest:Array):XMLClause
            {
                var aList:Array = ['"', "'"].concat(rest);
    
                while (true)
                {
                    var result:XMLClause = search.apply(this, aList);
    
                    switch (result.text)
                    {
                        case '"':
                        case "'":
                            unquote(result.text);
                            break;
    
                        default:
                            return result;
                    }
                }
    
                return null;
            }
    
            // Parse <? ... ?> tag.
            private function parsePi(X:XML):void
            {
                var result:XMLSlice = new XMLSlice();
    
                result.node = X;
                result.type = XMLSlice.PI;
                result.begin = sourceText.indexOf("<?", parseIndex);
    
                parseIndex = result.begin + 2;
    
                var aClause:XMLClause = avoidQuotes("?>");
    
                result.end = aClause.index + 2;
                result.text = sourceText.substring(result.begin, result.end);
    
                slices.push(result);
            }
    
            // Parse <!-- ... --> tag.
            private function parseComment(X:XML):void
            {
                var result:XMLSlice = new XMLSlice();
    
                result.node = X;
                result.type = XMLSlice.COMMENT;
                result.begin = sourceText.indexOf("<!--", parseIndex);
                result.end = sourceText.indexOf("-->", result.begin) + 3;
                result.text = sourceText.substring(result.begin, result.end);
    
                parseIndex = result.end;
                slices.push(result);
            }
    
            static private const SPACES:String = " \n\r\t";
    
            private function eatWhitespaces():void
            {
                while (SPACES.indexOf(sourceText.charAt(parseIndex)) > -1) parseIndex++;
            }
    
            // Parse plain text tag or <![CDATA[ ... ]]> tag.
            private function parseText(X:XML):void
            {
                eatWhitespaces();
    
                var result:XMLSlice = new XMLSlice();
    
                if (sourceText.indexOf("<![CDATA[", parseIndex) == parseIndex)
                {
                    result.type = XMLSlice.CDATA;
                    result.begin = sourceText.indexOf("<![CDATA[", parseIndex);
                    result.end = sourceText.indexOf("]]>", result.begin) + 3;
                }
                else
                {
                    result.type = XMLSlice.TEXT;
                    result.begin = parseIndex;
                    result.end = sourceText.indexOf("<", parseIndex);
                }
    
                result.node = X;
                result.text = sourceText.substring(result.begin, result.end);
    
                parseIndex = result.end;
                slices.push(result);
            }
        }
    }
    
    internal class XMLSlice
    {
        static public const COMMENT:String = "COMMENT";
        static public const CDATA:String = "CDATA";
        static public const TEXT:String = "TEXT";
        static public const PI:String = "PI";
        static public const WHOLE:String = "NODE";
        static public const HEAD:String = "HEAD";
        static public const TAIL:String = "TAIL";
    
        public var type:String;
        public var begin:int;
        public var end:int;
        public var node:XML;
        public var text:String;
    
        public function get length():int { return text.length; }
        public function toString():String { return "[" + type + " " + begin + ":" + end + "] " + text; }
    }
    
    internal class XMLClause
    {
        public var index:int = int.MAX_VALUE;
        public var text:String = null;
    }