web-dev-qa-db-fra.com

Comment créer des objets Document avec JavaScript

Fondamentalement, c'est la question, comment est-on censé construire un objet Document à partir d'une chaîne de code HTML dynamiquement en javascript?

34
jayarjo

Il existe deux méthodes définies dans les spécifications, createDocument à partir de DOM Core Level 2 et createHTMLDocument à partir de HTML5. Le premier crée un document XML (y compris XHTML), le second crée un document HTML. Les deux résident, en tant que fonctions, sur l'interface DOMImplementation.

var impl    = document.implementation,
    xmlDoc  = impl.createDocument(namespaceURI, qualifiedNameStr, documentType),
    htmlDoc = impl.createHTMLDocument(title);

En réalité, ces méthodes sont plutôt jeunes et ne sont implémentées que dans les versions récentes des navigateurs. Selon http://quirksmode.org et MDN , les navigateurs suivants prennent en charge createHTMLDocument:

  • Chrome 4
  • Opera 10
  • Firefox 4
  • Internet Explorer 9
  • Safari 4

Chose intéressante, vous pouvez (en quelque sorte) créer un document HTML dans les anciennes versions d'Internet Explorer, en utilisant ActiveXObject:

var htmlDoc = new ActiveXObject("htmlfile");

L'objet résultant sera un nouveau document, qui peut être manipulé comme n'importe quel autre document.

29
Andy E

En supposant que vous essayez de créer un objet Document entièrement analysé à partir d'une chaîne de balisage et d'un type de contenu que vous connaissez également (peut-être parce que vous avez obtenu le code html à partir d'une requête xmlhttprequest, et donc obtenu le type de contenu dans son Content-Type En-tête http; probablement généralement text/html) - cela devrait être aussi simple:

var doc = (new DOMParser).parseFromString(markup, mime_type);

dans un monde futur idéal où les implémentations du navigateur DOMParser sont aussi solides et compétentes que leur rendu de document - c'est peut-être une bonne exigence de rêve pour les futurs efforts de normalisation de HTML6. Il s'avère cependant qu'aucun navigateur actuel ne le fait.

Vous avez probablement le problème le plus facile (mais toujours compliqué) d'avoir une chaîne html pour laquelle vous voulez obtenir un objet Document entièrement analysé. Voici une autre façon de procéder, qui devrait également fonctionner dans tous les navigateurs - vous créez d'abord un objet HTML Document:

var doc = document.implementation.createHTMLDocument('');

puis remplissez-le avec votre fragment html :

doc.open();
doc.write(html);
doc.close();

Maintenant, vous devriez avoir un DOM entièrement analysé dans doc, que vous pouvez exécuter alert(doc.title) on, tranche avec des sélecteurs css comme doc.querySelectorAll('p') ou idem XPath en utilisant doc.evaluate.

Cela fonctionne réellement dans les navigateurs WebKit modernes comme Chrome et Safari (je viens de tester dans Chrome 22 et Safari 6 respectivement) - voici un exemple qui prend le courant le code source de la page, le recrée dans une nouvelle variable de document src, lit son titre, le remplace par une version citée en html du même code source et affiche le résultat dans un iframe: http://codepen.io/johan/full/KLIeE

Malheureusement, je ne pense pas que d'autres navigateurs contemporains aient encore des implémentations aussi solides.

22
ecmanaut

Selon la spécification ( doc ), on peut utiliser la méthode createHTMLDocument de DOMImplementation, accessible via document.implementation comme suit:

var doc = document.implementation.createHTMLDocument('My title');  
var body = document.createElementNS('http://www.w3.org/1999/xhtml', 'body'); 
doc.documentElement.appendChild(body);
// and so on
4
Chris Baker

Une réponse mise à jour pour 2014, avec l'évolution du DOMparser. Cela fonctionne dans tous les navigateurs actuels que je peux trouver, et devrait également fonctionner dans les versions antérieures d'IE, en utilisant l'approche document.implementation.createHTMLDocument ('') d'ecManaut ci-dessus.

Essentiellement, IE, Opera, Firefox peuvent tous analyser en "texte/html". Safari analyse comme "text/xml".

Attention cependant à l'analyse XML intolérante. L'analyse de Safari se décompose dans les espaces insécables et autres caractères HTML (accents français/allemands) désignés par des esperluettes. Plutôt que de gérer chaque caractère séparément, le code ci-dessous remplace toutes les esperluettes par une chaîne de caractères vide de sens "j! J!". Cette chaîne peut ensuite être restituée comme esperluette lors de l'affichage des résultats dans un navigateur (plus simple, j'ai trouvé, que d'essayer de gérer les esperluettes dans une "fausse" analyse XML).

function parseHTML(sText) {
try {

    console.log("Domparser: " + typeof window.DOMParser);

    if (typeof window.DOMParser !=null) {
        // modern IE, Firefox, Opera  parse text/html
        var parser = new DOMParser();
        var doc = parser.parseFromString(sText, "text/html");
        if (doc != null) {
            console.log("parsed as HTML");
            return doc

        }
        else {

            //replace ampersands with harmless character string to avoid XML parsing issues
            sText = sText.replace(/&/gi, "j!J!");
            //safari parses as text/xml
            var doc = parser.parseFromString(sText, "text/xml");
            console.log("parsed as XML");
            return doc;
        }

    } 
    else  {
        // older IE 
        doc= document.implementation.createHTMLDocument('');
        doc.write(sText);           
        doc.close;
        return doc; 
    }
} catch (err) {
    alert("Error parsing html:\n" + err.message);
}
}
4
Neil F

Les éléments suivants fonctionnent dans la plupart des navigateurs courants, mais pas dans certains. C'est aussi simple que cela devrait être (mais pas):

// Fails if UA doesn't support parseFromString for text/html (e.g. IE)
function htmlToDoc(markup) {
  var parser = new DOMParser();
  return parser.parseFromString(markup, "text/html");
}

var htmlString = "<title>foo bar</title><div>a div</div>";
alert(htmlToDoc(htmlString).title);

Pour tenir compte des aléas des agents utilisateurs, les éléments suivants peuvent être meilleurs (veuillez noter l'attribution):

/*
 * DOMParser HTML extension
 * 2012-02-02
 *
 * By Eli Grey, http://eligrey.com
 * Public domain.
 * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
 *
 * Modified to work with IE 9 by RobG
 * 2012-08-29
 *
 * Notes:
 *
 *  1. Supplied markup should be avalid HTML document with or without HTML tags and
 *     no DOCTYPE (DOCTYPE support can be added, I just didn't do it)
 *
 *  2. Host method used where Host supports text/html
 */

/*! @source https://Gist.github.com/1129031 */
/*! @source https://developer.mozilla.org/en-US/docs/DOM/DOMParser */

/*global document, DOMParser*/

(function(DOMParser) {
    "use strict";

    var DOMParser_proto;
    var real_parseFromString;
    var textHTML;         // Flag for text/html support
    var textXML;          // Flag for text/xml support
    var htmlElInnerHTML;  // Flag for support for setting html element's innerHTML

    // Stop here if DOMParser not defined
    if (!DOMParser) return;

    // Firefox, Opera and IE throw errors on unsupported types
    try {
        // WebKit returns null on unsupported types
        textHTML = !!(new DOMParser).parseFromString('', 'text/html');

    } catch (er) {
      textHTML = false;
    }

    // If text/html supported, don't need to do anything.
    if (textHTML) return;

    // Next try setting innerHTML of a created document
    // IE 9 and lower will throw an error (can't set innerHTML of its HTML element)
    try {
      var doc = document.implementation.createHTMLDocument('');
      doc.documentElement.innerHTML = '<title></title><div></div>';
      htmlElInnerHTML = true;

    } catch (er) {
      htmlElInnerHTML = false;
    }

    // If if that failed, try text/xml
    if (!htmlElInnerHTML) {

        try {
            textXML = !!(new DOMParser).parseFromString('', 'text/xml');

        } catch (er) {
            textHTML = false;
        }
    }

    // Mess with DOMParser.prototype (less than optimal...) if one of the above worked
    // Assume can write to the prototype, if not, make this a stand alone function
    if (DOMParser.prototype && (htmlElInnerHTML || textXML)) { 
        DOMParser_proto = DOMParser.prototype;
        real_parseFromString = DOMParser_proto.parseFromString;

        DOMParser_proto.parseFromString = function (markup, type) {

            // Only do this if type is text/html
            if (/^\s*text\/html\s*(?:;|$)/i.test(type)) {
                var doc, doc_el, first_el;

                // Use innerHTML if supported
                if (htmlElInnerHTML) {
                    doc = document.implementation.createHTMLDocument("");
                    doc_el = doc.documentElement;
                    doc_el.innerHTML = markup;
                    first_el = doc_el.firstElementChild;

                // Otherwise use XML method
                } else if (textXML) {

                    // Make sure markup is wrapped in HTML tags
                    // Should probably allow for a DOCTYPE
                    if (!(/^<html.*html>$/i.test(markup))) {
                        markup = '<html>' + markup + '<\/html>'; 
                    }
                    doc = (new DOMParser).parseFromString(markup, 'text/xml');
                    doc_el = doc.documentElement;
                    first_el = doc_el.firstElementChild;
                }

                // RG: I don't understand the point of this, I'll leave it here though 
                //     In IE, doc_el is the HTML element and first_el is the HEAD.
                //
                // Is this an entire document or a fragment?
                if (doc_el.childElementCount == 1 && first_el.localName.toLowerCase() == 'html') {
                    doc.replaceChild(first_el, doc_el);
                }

                return doc;

            // If not text/html, send as-is to Host method
            } else {
                return real_parseFromString.apply(this, arguments);
            }
        };
    }
}(DOMParser));

// Now some test code
var htmlString = '<html><head><title>foo bar</title></head><body><div>a div</div></body></html>';
var dp = new DOMParser();
var doc = dp.parseFromString(htmlString, 'text/html');

// Treat as an XML document and only use DOM Core methods
alert(doc.documentElement.getElementsByTagName('title')[0].childNodes[0].data);

Ne vous laissez pas rebuter par la quantité de code, il y a beaucoup de commentaires, il peut être raccourci un peu mais devient moins lisible.

Oh, et si le balisage est du XML valide, la vie est beaucoup plus simple:

var stringToXMLDoc = (function(global) {

  // W3C DOMParser support
  if (global.DOMParser) {
    return function (text) {
      var parser = new global.DOMParser();
      return parser.parseFromString(text,"application/xml");
    }

  // MS ActiveXObject support
  } else {
    return function (text) {
      var xmlDoc;

      // Can't assume support and can't test, so try..catch
      try {
        xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
        xmlDoc.async="false";
        xmlDoc.loadXML(text);
      } catch (e){}
      return xmlDoc;
    }
  }
}(this));


var doc = stringToXMLDoc('<books><book title="foo"/><book title="bar"/><book title="baz"/></books>');
alert(
  doc.getElementsByTagName('book')[2].getAttribute('title')
);
3
RobG