Convertir XML en objet JavaScript pour Node.js

Code source pour charger un fichier XML dans un objet JavaScript ou inversement, sauver un objet dans un fichier XML.

Dans une page HTML, on utilisera l'objet DOMParser pour convertir le XML, et XMLHttpRequest pour charger le document. Mais il s'agit d'objets du navigateur, ils ne sont pas disponibles sur Node.js. Pour les remplacer, on utilisera le module Sax.js qui charge les balises XML une à une et les convertit en objets JS élémentaires, et un code spécifique pour assembler ces éléments en un seul objet structuré.

Ce code fait partie du runtime du compilateur Scriptol-JavaScript depuis la version 1.4.

Les noms des balises et des attributs XML deviennent des noms de propriétés d'un objet. S'il s'agit un attribut une valeur est assignée à cette propriété. S'il s'agit d'une balise l'élément XML dans son entier est assigné à la propriété.

Exemple:

<voiture vitesse="100" marque="Ferrari">
   <passagers>Alpha, Beta, Delta</passagers>
</voiture>

L'objet sera:

{
  voiture : {
   "vitesse": 100,
   "marque": Ferrari,
   "passagers": {
     "data": "Alpha, Beta, Delta"
   }
 }
} 

Le contenu d'une balise est assigné par convention à la propriété "data".

Cela est simple mais il y a encore un obstacle: si le document XML contient plusieurs balises de même nom sur un même niveau, cela ne peut se convertir directement en propriétés d'un objet qui doivent être uniques. Alors on les place dans un tableau et on assigne ce tableau par convention à la propriété "array".

Exemple:

<voiture vitesse="100" marque="Ferrari">
   <passager>Alpha</passager>
   <passager>Beta</passager>
   <passager>Delta</passager>
</voiture>

L'objet sera:

{
  voiture : {
   "vitesse": 100,
   "marque": Ferrari,
   "array" = [ 
     { "passager" : "Alpha" } ,
     { "passager" : "Beta" } ,
     { "passager" : "Delta" } 
    ]
   }
 }
} 

Charger XML dans un objet (désérialiser)

Voici le code JavaScript qui charge le fichier XML dans un objet. Il convient pour des documents simples comme des fichiers de configuration mais ne prend pas en compte les CDATA et autres éléments de documents complexes (XML peut être très compliqué).

function parseXML(data)
{
  var data = data.toString("utf8");
  var sax = require("sax");
  
  var parser = sax.parser(true, { trim:true });
  parser.onerror = function (e) {
    console.log("XML error: ", e.toString());
    return{};
  };

  var ctag = null;
  var xmlroot = null;
  
  parser.ontext = function (t) {
      if (ctag && t.length > 0) { 
          ctag["data"] = t;
      }   
  }    
  
  parser.onopentag = function (node) {
    var name = node.name;
    var parent = ctag;
    ctag = {};
    ctag.array = [];
    ctag.idFlag = false;   // same tags at same level
    if (xmlroot === null) {
      xmlroot = {};
      xmlroot[name] = ctag;
    }
    else
    {
      ctag.parent = parent;
      var xtag = {};
      xtag[name]= ctag;
      parent.array.push(xtag);
    }
    
    for(var k in node.attributes) {
      ctag[k] = node.attributes[k];
    }

    while(parent && !parent.idFlag) 
    {
        for(var i=0; i < parent.array.length - 1; i++) 
        {
           var elem = parent.array[i];
           for(var key in elem) 
           {
            if(key == name) parent.idFlag=true;
            break;
           }
        }
        break;
    }  
  };

  parser.onclosetag = function(name) {
    if(ctag.idFlag == false) // only one child / all childs different
    {
        for(var i = 0; i < ctag.array.length; i++) {
          var xtag = ctag.array[i];
          for(var u in xtag) {
              ctag[u]=xtag[u];
          } 
        }
        delete ctag.array;
     }
    delete ctag.idFlag;
    if (ctag.parent) {
        var parent = ctag.parent;
        delete ctag.parent;
        ctag = parent;
    }
  }

  parser.write(data).end();
  return xmlroot;
}

var filename="test.xml";
var a = fs.readFileSync(filename).toString();
var obj = parseXML(a);

Remplacer le nom de fichier assigné à filename par tout autre fichier XML.

Accéder au contenu

Pour accéder aux balises individuelles, le runtime Scriptol offre la fonction getById().

function getById(d, idval) {
for(var k in d) {
if(typeof d[k] === "object") {
var dsub = d[k];
if("id" in dsub && dsub.id == idval) return d;
var dret = getById(dsub, idval)
if(dret !== false) return dret;
}
}
return false
}

La fonction prend en compte le problème des balises identiques.

Sauver un objet JavaScript dans un fichier XML (sérialiser)

Pour mettre à jour le fichier que l'on aura modifié dans un programme JavaScript, il faut convertir les propriétés et objets imbriqués en attributs et balises.

La valeur d'une propriété "data" devient le contenu d'une balise, les éléments d'une propriété "array" deviennent chacun une balise.

var XMLStorage = "";
function xmlSub(d, name) 
{
  var flag = true;
  if(name=='array') {
    for(var i = 0; i < d.length; i++)
    {
        var tag = d[i];
        var o;
        for(var k in tag) { o = tag[k]; break; }
        XMLStorage += "<" + k;
        flag = xmlSub(o, k);
        XMLStorage += "\n";
      flag = false;      
      continue;
    }
    if (x == "data") { 
      XMLStorage += ">" + d[x];
      flag = false;
    }  
    else { 
      XMLStorage += " " + x + "=\""+ d[x] + "\"";
      flag = true;
    }    
  }
  return flag;  
}

function saveXML(d, filename) 
{
  XMLStorage = '<?xml version="1.0" encoding="UTF-8"?>';
  if(xmlSub(d)) XMLStorage += ">\n";
  fs.writeFileSync(filename, XMLStorage);
}

Le code complet avec une démonstration sont disponibles en téléchargement.

Pour le faire fonctionner, vous devez installer Node.js et ensuite le module sax avec cette commande:

npm install sax

Après avoir téléchargé et extrait l'archive, allez dans xml-js et tapez:

node xml-js.js