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.
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;
}