3

In the XML document I want to find the node that the cursor is in.

I don't think Ace Editor can do this. But I think maybe it keeps track by using indexes system.

What do I mean? Well, in XML there is a hierarchical structure of leaves and branches or ancestors and descendants. If you are keeping track of the number of nodes and locations you can create a system to find it again.

For example take this code:

enter image description here

The root node would be item [0]. The first descendant of that would be [0][0]. The second descendant would be [0][1]. If the second descendant had three descendants then their position would be [0][1][0], [0][1][1], [0][1][2].

Is there a way to get that position in Ace Editor? The reason is I have an XML object in my application. BUT Ace Editor is in JavaScript and it does not support XML or E4X. I can only get a string values from it to pass back to my application.

So I first have to get the node that the cursor is in JavaScript, then I have to find out how to map that back to the XML object in my application.

Right now I've got this far:

var xml:XML = new XML(ace.text);
var token:Object = ace.getTokenAt(ace.row, ace.column);
var type:String = token ? token.type : "";
var tagName:String;
var index:int = token ? token.index : -1; // index = 2
var start:int = token ? token.start : -1; // start = 5

if (type=="meta.tag.tag-name.xml") {
    tagName = token.value; // head
}

var matchingTagsList:XMLList = xml.descendants(tagName);

if (matchingTagsList.length()==1) {
     var match:XML = matchingTagsList[0];
}
else {
     // numerous matches. how to find one from the other?
}

BTW Ace Editor returns an index and start value. I could use that to try to find the matching tag.

I could also, possibly, convert all the XML matches to strings, then if I could get from the range in Ace from the start tag to the end tag I could compare each. But that is really hacky because there are a lot of points of failure. If there are namespaces the strings won't match, if there is whitespace characters the strings won't match, or encoded entities, etc.

1.21 gigawatts
  • 16,517
  • 32
  • 123
  • 231

2 Answers2

2

Sorry this is several months late but I just had the same problem and found my own solution. I have had success correlating cursor row to xml node using a recursive function to match my function's line counter to the cursor row. This function go through child nodes until the line counter equals the cursor row. I'm just looking a element nodes. If your xml has comment rows, you'll have to count them too. I assume the xml has one node per line. Otherwise, you would have to also use cursor.column and some more logic. The code is a little crude. You may need to clean it up.

var cursor = ace.selection.getCursor();
var line = 0;

var insertXmlAtChildNode = function(parent, xmlToInsert) {
  var foundit = false;
  if (parent.nodeType === 1) {
    //element node
    if (cursor.row == line) {
      // found the xml node that matches the cursor row.
      // add your code here to use that node.
      console.log("found the node that matches cursor row " + cursor.row);
      parent.appendChild(xmlToInsert);
      foundit = true;
    }
    if (!foundit && parent && parent.childNodes && parent.childNodes.length > 0) {
      var i;
      for (i = 0; i < parent.childNodes.length; i++) {
        if (parent.childNodes[i].nodeType === 1) {
          // found a child element node
          line++;
        }
        foundit = insertXmlAtChildNode(parent.childNodes[i], xmlToInsert);
        if (foundit) {
          break;
        }
      }
    }
  }
  if (parent.nodeType === 2) {
    //attribute node
  }
  if (parent.nodeType === 3) {
    //text node
  }
  if (parent.nodeType === 8) {
    //comment node
  }
  if (!foundit && parent && parent.childNodes && parent.childNodes.length > 0) {
    // inc line before exiting recursion. this accounts for line number of the closing tag on multi-line element.
    line++;
  }
  return foundit;
};

This starts the searching.

var xml = new XML();

console.log("start looking");
if (xml && xml.childNodes && xml.childNodes.length > 0) {
  var i;
  var foundit = false;
  for (i = 0; i < xml.childNodes.length; i++) {
    foundit = insertXmlAtChildNode(xml.childNodes[i], xmlToInsert);
    if (foundit) {
      break;
    }
  }
}
console.log("end");
dskow
  • 924
  • 6
  • 9
  • 1
    Thanks for posting. I'll look into this. I found another solution in one of the Apache FlexJS examples that solves some of the problems but not all of them. But it's a start. I'll post it as an answer. – 1.21 gigawatts Jun 22 '16 at 21:44
0

I found part of the answer in a solution in one of the Apache FlexJS examples for building a tree structure that someone can eventually use by correlating it with the ace editor position.

When the text/XML changes, we parse it and compare it to it's previous values. We create a map of nodes by giving each node an id.

It doesn't answer the whole question but I wanted to show what I have so far. Combining this with the other answer may provide a full solution (still going through it).

You call the checkForDifferences whenever the XML file or XML text value changes, for example when a user saves an .xml file or when the user enters new text into the editor. That creates a tree of objects and tags. Then in Ace Editor, you would climb back up the tree if there is one, to get the node index in the parent and then the parent depth. With that information you should be able to find the node in the tree object and then, if you are keeping references to the nodes in the tree object, you can get the current XML node the cursor is in.

<fx:Script>
    <![CDATA[           
        private function checkForDifferences(filePath:String = null):void {
            var mxmlFile:File;

            if (filePath!=null && filePath!="") {
                try {
                    mxmlFile = new File(filePath);

                    if (mxmlFile.exists && mxmlFile.modificationDate.time == lastModifiedTime) {
                        return;
                    }
                } catch (e:Error) {
                    // might check while file is open to be written so just ignore
                    // and check on the next interval;
                    return;
                }
            }

            parseFile();
            computeChanges();
            applyChanges();
        }

        private function parseFile():void {
            var xml:XML = new XML(aceEditor.text);
            newDB = {};
            generatedIDCounter = 0;
            parseChildren(newDB, xml);
        }

        private function parseChildren(newDB:Object, parent:XML):void {
            var effectiveID:String;
            var elementAttributes:XMLList;
            var numberOfAttributes:int;
            var attributeMap:Object;
            var attributeName:String;
            var children:XMLList;
            var childNode:XML;
            var childNodeName:String;
            var numberOfChildren:int;
            var memberName:String;
            var isStateSpecific:Boolean;
            var metaData:MetaData;

            children = parent.children();
            numberOfChildren = children.length();

            for (var i:int = 0; i < numberOfChildren; i++){
                childNode = children[i];
                childNodeName = childNode.name();

                if (childNodeName == null) {
                    continue; // saw this for CDATA children
                }

                // items to ignore
                if (filteredMXMLNodes[childNodeName]) {
                    continue;
                }

                // we go deep first because that's how the Falcon compiler
                // generates IDs for tags that don't have id attributes set.
                parseChildren(newDB, childNode);

                // check if a class rather than property, style or event
                if (isInstance(childNodeName)) {
                    if (childNode.@id.length() == 0) {
                        effectiveID = "#" + generatedIDCounter++;
                    }
                    else {
                        effectiveID = childNode.@id;
                    }

                    elementAttributes = childNode.attributes();
                    numberOfAttributes = elementAttributes.length();

                    attributeMap = {};
                    newDB[effectiveID] = attributeMap;

                    for (var j:int = 0; j < numberOfAttributes; j++) {
                        attributeName = elementAttributes[j].name();
                        isStateSpecific = attributeName.indexOf(".")!=-1;
                        memberName = getAttributeName(attributeName);
                        //metaData = ClassUtils.getMetaDataOfMember(childNodeName, memberName);

                        //if (supportedAttributes.hasOwnProperty()) {
                        //if (supportedAttributes.hasOwnProperty(getAttributeName(attributeName))) {
                            attributeMap[attributeName] = childNode["@" + attributeName].toString();
                        //}
                    }
                }
            }
        }

        // assume it is an instance if the tag name starts with a capital letter
        private function isInstance(tagName:String):Boolean {
            var hasNamespace:int = tagName.indexOf("::");
            var firstCharacter:String;
            var isCapitalLetter:Boolean;

            if (hasNamespace > -1) {
                tagName = tagName.substring(hasNamespace + 2);
            }

            firstCharacter = tagName.charAt(0);
            isCapitalLetter = firstCharacter >= "A" && firstCharacter <= "Z";

            return isCapitalLetter;
        }

        /**
         * If it contains a period we need to set the attribute in that state if the state exists
         * */
        private function getAttributeName(attributeName:String):String {
            var containsPeriod:int = attributeName.indexOf(".");

            if (containsPeriod > -1) {
                attributeName = attributeName.substring(0, containsPeriod);
            }

            return attributeName;
        }


        private var lastModifiedTime:Number = 0;
        private var generatedIDCounter:int = 0;
        private var newDB:Object;
        private var oldDB:Object;
        private var changes:Object;
        private var removals:Object;

        private var filteredMXMLNodes:Object = {
            "http://ns.adobe.com/mxml/2009::Script": 1,
            "http://ns.adobe.com/mxml/2009::Declarations": 1,
            "http://ns.adobe.com/mxml/2009::Style": 1
        }


        private function applyChanges():void {
            var changedValues:Object;
            var removedValues:Object;
            var attributeName:String;
            var nodeID:String;

            for (nodeID in changes) {
                changedValues = changes[nodeID];
                trace("Node ID:" + nodeID);

                for (attributeName in changedValues) {
                    trace(" - Attribute to change: " + attributeName);
                    trace(" - New value: " + changedValues[attributeName]);
                    //commandconnection.send("_MXMLLiveEditPluginCommands", "setValue", nodeID, attributeName, changedValues[attributeName]);
                }
            }

            for (nodeID in removals) {
                removedValues = removals[nodeID];
                trace(nodeID);

                for (attributeName in removedValues)
                {
                    trace(" - Attribute removed: " + attributeName);
                    //commandconnection.send("_MXMLLiveEditPluginCommands", "setValue", p, q, removedValues[q]);
                }
            }
        }
    ]]>
</fx:Script>

Note: There may be a function or variable missing since I copied this from a more complex example.

Note: Ace editor is finding the matching node as you can see in the picture, by creating an outline around the node (see picture).

1.21 gigawatts
  • 16,517
  • 32
  • 123
  • 231