Можно ли загрузить весь HTML-документ в фрагмент документа в Internet Explorer?

Здесь кое-что, с чем я немного затруднился. У меня есть локальная клиентская сторона script, которая должна позволять пользователю извлекать удаленную веб-страницу и выполнять поиск этой итоговой страницы для форм. Чтобы сделать это (без регулярного выражения), мне нужно проанализировать документ в полностью доступный объект DOM.

Некоторые ограничения, которые я хотел бы подчеркнуть:

  • Я не хочу использовать библиотеки (например, jQuery). Там слишком много раздувания для того, что мне нужно сделать здесь.
  • Ни при каких обстоятельствах сценарии с удаленной страницы не выполняются (по соображениям безопасности).
  • DOM API, такие как getElementsByTagName, должны быть доступны.
  • Он должен работать только в Internet Explorer, но по крайней мере в 7.
  • Предположим, что у меня нет доступа к серверу. Я делаю, но я не могу использовать его для этого.

Что я пробовал

Предполагая, что у меня есть полная строка HTML-документа (включая объявление DOCTYPE) в переменной html, вот что я пробовал до сих пор:

var frag = document.createDocumentFragment(),
div  = frag.appendChild(document.createElement("div"));

div.outerHTML = html;
//-> results in an empty fragment

div.insertAdjacentHTML("afterEnd", html);
//-> HTML is not added to the fragment

div.innerHTML = html;
//-> Error (expected, but I tried it anyway)

var doc = new ActiveXObject("htmlfile");
doc.write(html);
doc.close();
//-> JavaScript executes

Я также попытался извлечь узлы <head> и <body> из HTML и добавить их к элементу <HTML> внутри фрагмента, все равно не повезло.

Есть ли у кого-нибудь идеи?

Ответ 1

Fiddle: http://jsfiddle.net/JFSKe/6/

DocumentFragment не реализует методы DOM. Использование document.createElement в сочетании с innerHTML удаляет теги <head> и <body> (даже если созданный элемент является корневым элементом, <html>). Поэтому решение следует искать в другом месте. Я создал функцию кросс-браузер строка-в-DOM, которая использует невидимый встроенный фрейм.

Все внешние ресурсы и скрипты будут отключены. Дополнительную информацию см. В пояснении кода.

Код

/*
 @param String html    The string with HTML which has be converted to a DOM object
 @param func callback  (optional) Callback(HTMLDocument doc, function destroy)
 @returns              undefined if callback exists, else: Object
                        HTMLDocument doc  DOM fetched from Parameter:html
                        function destroy  Removes HTMLDocument doc.         */
function string2dom(html, callback){
    /* Sanitise the string */
    html = sanitiseHTML(html); /*Defined at the bottom of the answer*/

    /* Create an IFrame */
    var iframe = document.createElement("iframe");
    iframe.style.display = "none";
    document.body.appendChild(iframe);

    var doc = iframe.contentDocument || iframe.contentWindow.document;
    doc.open();
    doc.write(html);
    doc.close();

    function destroy(){
        iframe.parentNode.removeChild(iframe);
    }
    if(callback) callback(doc, destroy);
    else return {"doc": doc, "destroy": destroy};
}

/* @name sanitiseHTML
   @param String html  A string representing HTML code
   @return String      A new string, fully stripped of external resources.
                       All "external" attributes (href, src) are prefixed by data- */

function sanitiseHTML(html){
    /* Adds a <!-\"'--> before every matched tag, so that unterminated quotes
        aren't preventing the browser from splitting a tag. Test case:
       '<input style="foo;b:url(0);><input onclick="<input type=button onclick="too() href=;>">' */
    var prefix = "<!--\"'-->";
    /*Attributes should not be prefixed by these characters. This list is not
     complete, but will be sufficient for this function.
      (see http://www.w3.org/TR/REC-xml/#NT-NameChar) */
    var att = "[^-a-z0-9:._]";
    var tag = "<[a-z]";
    var any = "(?:[^<>\"']*(?:\"[^\"]*\"|'[^']*'))*?[^<>]*";
    var etag = "(?:>|(?=<))";

    /*
      @name ae
      @description          Converts a given string in a sequence of the
                             original input and the HTML entity
      @param String string  String to convert
      */
    var entityEnd = "(?:;|(?!\\d))";
    var ents = {" ":"(?:\\s|&nbsp;?|&#0*32"+entityEnd+"|&#x0*20"+entityEnd+")",
                "(":"(?:\\(|&#0*40"+entityEnd+"|&#x0*28"+entityEnd+")",
                ")":"(?:\\)|&#0*41"+entityEnd+"|&#x0*29"+entityEnd+")",
                ".":"(?:\\.|&#0*46"+entityEnd+"|&#x0*2e"+entityEnd+")"};
                /*Placeholder to avoid tricky filter-circumventing methods*/
    var charMap = {};
    var s = ents[" "]+"*"; /* Short-hand space */
    /* Important: Must be pre- and postfixed by < and >. RE matches a whole tag! */
    function ae(string){
        var all_chars_lowercase = string.toLowerCase();
        if(ents[string]) return ents[string];
        var all_chars_uppercase = string.toUpperCase();
        var RE_res = "";
        for(var i=0; i<string.length; i++){
            var char_lowercase = all_chars_lowercase.charAt(i);
            if(charMap[char_lowercase]){
                RE_res += charMap[char_lowercase];
                continue;
            }
            var char_uppercase = all_chars_uppercase.charAt(i);
            var RE_sub = [char_lowercase];
            RE_sub.push("&#0*" + char_lowercase.charCodeAt(0) + entityEnd);
            RE_sub.push("&#x0*" + char_lowercase.charCodeAt(0).toString(16) + entityEnd);
            if(char_lowercase != char_uppercase){
                RE_sub.push("&#0*" + char_uppercase.charCodeAt(0) + entityEnd);   
                RE_sub.push("&#x0*" + char_uppercase.charCodeAt(0).toString(16) + entityEnd);
            }
            RE_sub = "(?:" + RE_sub.join("|") + ")";
            RE_res += (charMap[char_lowercase] = RE_sub);
        }
        return(ents[string] = RE_res);
    }
    /*
      @name by
      @description  second argument for the replace function.
      */
    function by(match, group1, group2){
        /* Adds a data-prefix before every external pointer */
        return group1 + "data-" + group2 
    }
    /*
      @name cr
      @description            Selects a HTML element and performs a
                                  search-and-replace on attributes
      @param String selector  HTML substring to match
      @param String attribute RegExp-escaped; HTML element attribute to match
      @param String marker    Optional RegExp-escaped; marks the prefix
      @param String delimiter Optional RegExp escaped; non-quote delimiters
      @param String end       Optional RegExp-escaped; forces the match to
                                  end before an occurence of <end> when 
                                  quotes are missing
     */
    function cr(selector, attribute, marker, delimiter, end){
        if(typeof selector == "string") selector = new RegExp(selector, "gi");
        marker = typeof marker == "string" ? marker : "\\s*=";
        delimiter = typeof delimiter == "string" ? delimiter : "";
        end = typeof end == "string" ? end : "";
        var is_end = end && "?";
        var re1 = new RegExp("("+att+")("+attribute+marker+"(?:\\s*\"[^\""+delimiter+"]*\"|\\s*'[^'"+delimiter+"]*'|[^\\s"+delimiter+"]+"+is_end+")"+end+")", "gi");
        html = html.replace(selector, function(match){
            return prefix + match.replace(re1, by);
        });
    }
    /* 
      @name cri
      @description            Selects an attribute of a HTML element, and
                               performs a search-and-replace on certain values
      @param String selector  HTML element to match
      @param String attribute RegExp-escaped; HTML element attribute to match
      @param String front     RegExp-escaped; attribute value, prefix to match
      @param String flags     Optional RegExp flags, default "gi"
      @param String delimiter Optional RegExp-escaped; non-quote delimiters
      @param String end       Optional RegExp-escaped; forces the match to
                                  end before an occurence of <end> when 
                                  quotes are missing
     */
    function cri(selector, attribute, front, flags, delimiter, end){
        if(typeof selector == "string") selector = new RegExp(selector, "gi");
        flags = typeof flags == "string" ? flags : "gi";
         var re1 = new RegExp("("+att+attribute+"\\s*=)((?:\\s*\"[^\"]*\"|\\s*'[^']*'|[^\\s>]+))", "gi");

        end = typeof end == "string" ? end + ")" : ")";
        var at1 = new RegExp('(")('+front+'[^"]+")', flags);
        var at2 = new RegExp("(')("+front+"[^']+')", flags);
        var at3 = new RegExp("()("+front+'(?:"[^"]+"|\'[^\']+\'|(?:(?!'+delimiter+').)+)'+end, flags);

        var handleAttr = function(match, g1, g2){
            if(g2.charAt(0) == '"') return g1+g2.replace(at1, by);
            if(g2.charAt(0) == "'") return g1+g2.replace(at2, by);
            return g1+g2.replace(at3, by);
        };
        html = html.replace(selector, function(match){
             return prefix + match.replace(re1, handleAttr);
        });
    }

    /* <meta http-equiv=refresh content="  ; url= " > */
    html = html.replace(new RegExp("<meta"+any+att+"http-equiv\\s*=\\s*(?:\""+ae("refresh")+"\""+any+etag+"|'"+ae("refresh")+"'"+any+etag+"|"+ae("refresh")+"(?:"+ae(" ")+any+etag+"|"+etag+"))", "gi"), "<!-- meta http-equiv=refresh stripped-->");

    /* Stripping all scripts */
    html = html.replace(new RegExp("<script"+any+">\\s*//\\s*<\\[CDATA\\[[\\S\\s]*?]]>\\s*</script[^>]*>", "gi"), "<!--CDATA script-->");
    html = html.replace(/<script[\S\s]+?<\/script\s*>/gi, "<!--Non-CDATA script-->");
    cr(tag+any+att+"on[-a-z0-9:_.]+="+any+etag, "on[-a-z0-9:_.]+"); /* Event listeners */

    cr(tag+any+att+"href\\s*="+any+etag, "href"); /* Linked elements */
    cr(tag+any+att+"src\\s*="+any+etag, "src"); /* Embedded elements */

    cr("<object"+any+att+"data\\s*="+any+etag, "data"); /* <object data= > */
    cr("<applet"+any+att+"codebase\\s*="+any+etag, "codebase"); /* <applet codebase= > */

    /* <param name=movie value= >*/
    cr("<param"+any+att+"name\\s*=\\s*(?:\""+ae("movie")+"\""+any+etag+"|'"+ae("movie")+"'"+any+etag+"|"+ae("movie")+"(?:"+ae(" ")+any+etag+"|"+etag+"))", "value");

    /* <style> and < style=  > url()*/
    cr(/<style[^>]*>(?:[^"']*(?:"[^"]*"|'[^']*'))*?[^'"]*(?:<\/style|$)/gi, "url", "\\s*\\(\\s*", "", "\\s*\\)");
    cri(tag+any+att+"style\\s*="+any+etag, "style", ae("url")+s+ae("(")+s, 0, s+ae(")"), ae(")"));

    /* IE7- CSS expression() */
    cr(/<style[^>]*>(?:[^"']*(?:"[^"]*"|'[^']*'))*?[^'"]*(?:<\/style|$)/gi, "expression", "\\s*\\(\\s*", "", "\\s*\\)");
    cri(tag+any+att+"style\\s*="+any+etag, "style", ae("expression")+s+ae("(")+s, 0, s+ae(")"), ae(")"));
    return html.replace(new RegExp("(?:"+prefix+")+", "g"), prefix);
}

Объяснение кода

Функция sanitiseHTML основана на моей функции replace_all_rel_by_abs (см. этот ответ). Функция sanitiseHTML полностью переписана, хотя для достижения максимальной эффективности и надежности.

Кроме того, добавлен новый набор RegExps для удаления всех скриптов и обработчиков событий (включая CSS expression(), IE7-). Чтобы убедиться, что все теги проанализированы как ожидалось, скорректированные теги имеют префикс <!--'"-->. Этот префикс необходим для правильного синтаксического анализа вложенных "обработчиков событий" в сочетании с неисчерпаемыми кавычками: <a id="><input onclick="<div onmousemove=evil()>">.

Эти RegExps динамически создаются с использованием внутренней функции cr/cri (C reate R eplace [ I nline]), Эти функции принимают список аргументов, а также создают и выполняют расширенную замену RE. Чтобы убедиться, что объекты HTML не нарушают RegExp (refresh in <meta http-equiv=refresh> могут быть записаны различными способами), динамически созданный RegExps частично построен функцией ae ( A ny E).
Фактические замены выполняются функцией by (замените на). В этой реализации by добавляет data- перед всеми согласованными атрибутами.

  • Все <script>//<[CDATA[ .. //]]></script> вхождения чередуются. Этот шаг необходим, потому что секции CDATA допускают строки </script> внутри кода. После того, как эта замена была выполнена, безопасно перейти к следующей замене:
  • Остальные теги <script>...</script> удаляются.
  • Тег <meta http-equiv=refresh .. > удален
  • Все прослушиватели событий и внешние указатели/атрибуты (href, src, url()) имеют префикс data-, как описано ранее.

  • Создается объект IFrame. IFrames менее склонны к утечке памяти (вопреки htmlfile ActiveXObject). IFrame становится невидимым и добавляется к документу, так что к DOM можно получить доступ. document.write() используются для записи HTML в IFrame. document.open() и document.close() используются для удаления предыдущего содержимого документа, так что сгенерированный документ является точной копией данной строки html.

  • Если указана функция обратного вызова, функция будет вызываться с двумя аргументами. Первый аргумент - ссылка на сгенерированный объект document. Второй аргумент - это функция, которая разрушает сгенерированное дерево DOM при вызове. Эта функция должна вызываться, когда вам больше не нужно дерево.
    Если функция обратного вызова не указана, функция возвращает объект, состоящий из двух свойств (doc и destroy), которые ведут себя как и ранее упомянутые аргументы.

Дополнительные примечания

  • Установка свойства designMode на "On" приведет к остановке выполнения кадром сценариев (не поддерживается в Chrome). Если вам нужно сохранить теги <script> по определенной причине, вы можете использовать iframe.designMode = "On" вместо функции удаления script.
  • Я не смог найти надежный источник для htmlfile activeXObject. Согласно этот источник, htmlfile медленнее IFrames и более восприимчив к утечкам памяти.

  • Все затронутые атрибуты (href, src,...) имеют префикс data-. Пример получения/изменения этих атрибутов показан для data-href:
    elem.getAttribute("data-href") и elem.setAttribute("data-href", "...")
    elem.dataset.href и elem.dataset.href = "...".
  • Внешние ресурсы отключены. В результате страница может выглядеть совершенно по-иному:
    <link rel="stylesheet" href="main.css" /> Нет внешних стилей
    <script>document.body.bgColor="red";</script> Нет стилей в стиле
    <img src="128x128.png" /> Нет изображений: размер элемента может быть совершенно другим.

Примеры

sanitiseHTML(html)
Вставьте этот букмарклет в строку местоположения. Он предложит вариант для ввода текстового поля, отображающий измененную HTML-строку.

javascript:void(function(){var s=document.createElement("script");s.src="http://rob.lekensteyn.nl/html-sanitizer.js";document.body.appendChild(s)})();

Примеры кода - string2dom(html):

string2dom("<html><head><title>Test</title></head></html>", function(doc, destroy){
    alert(doc.title); /* Alert: "Test" */
    destroy();
});

var test = string2dom("<div id='secret'></div>");
alert(test.doc.getElementById("secret").tagName); /* Alert: "DIV" */
test.destroy();

Заметные ссылки

Ответ 2

Не уверен, почему вы возитесь с documentFragments, вы можете просто установить текст HTML как innerHTML нового элемента div. Затем вы можете использовать этот элемент div для getElementsByTagName и т.д., Не добавляя div в DOM:

var htmlText= '<html><head><title>Test</title></head><body><div id="test_ele1">this is test_ele1 content</div><div id="test_ele2">this is test_ele content2</div></body></html>';

var d = document.createElement('div');
d.innerHTML = htmlText;

console.log(d.getElementsByTagName('div'));

Если вы действительно замужем за идеей documentFragment, вы можете использовать этот код, но вам все равно придется обернуть его в div, чтобы получить функции DOM, которые вы после:

function makeDocumentFragment(htmlText) {
    var range = document.createRange();
    var frag = range.createContextualFragment(htmlText);
    var d = document.createElement('div');
    d.appendChild(frag);
    return d;
}

Ответ 3

Предполагая, что HTML также является правильным XML, вы можете использовать loadXML()

Ответ 4

DocumentFragment не поддерживает getElementsByTagName -, который поддерживается только Document.

Возможно, вам понадобится использовать библиотеку, например jsdom, которая обеспечивает реализацию DOM и через которую вы можете искать с помощью getElementsByTagName и других DOM API. И вы можете настроить его на выполнение сценариев. Да, он "тяжелый", и я не знаю, работает ли он в IE 7.

Ответ 5

Я не уверен, поддерживает ли IE document.implementation.createHTMLDocument, но если это так, используйте этот алгоритм (адаптированный из моего расширения HTML DOMParser), Обратите внимание, что DOCTYPE не будет сохранен.:

var
      doc = document.implementation.createHTMLDocument("")
    , doc_elt = doc.documentElement
    , first_elt
;
doc_elt.innerHTML = your_html_here;
first_elt = doc_elt.firstElementChild;
if ( // are we dealing with an entire document or a fragment?
       doc_elt.childElementCount === 1
    && first_elt.tagName.toLowerCase() === "html"
) {
    doc.replaceChild(first_elt, doc_elt);
}

// doc is an HTML document
// you can now reference stuff like doc.title, etc.

Ответ 6

Просто блуждал по этой странице, я немного опоздал, чтобы быть полезным:), но следующее должно помочь любому, у кого есть аналогичная проблема в будущем... однако IE7/8 действительно следует игнорировать, и есть намного лучше методы, поддерживаемые более современными браузерами.

Следующее работает почти через все, что я тестировал - только две стороны:

  • Я добавил специальные функции getElementById и getElementsByName к элементу корневого div, поэтому они не будут казаться ожидаемыми далее по дереву (если только код не будет изменен для удовлетворения этого).

  • Признак doctype будет проигнорирован - однако я не думаю, что это будет иметь большое значение, поскольку мой опыт заключается в том, что doctype не будет влиять на то, как структурируется dom, как это делается (что, очевидно, не будет с этим метод).

В принципе система полагается на то, что <tag> и <namespace:tag> обрабатываются по-разному с помощью useragents. Как было установлено, некоторые специальные теги не могут существовать в элементе div, поэтому они удаляются. Элементы, пропущенные именами, могут быть размещены в любом месте (если не указано иное DTD). Хотя эти теги пространства имен фактически не будут вести себя как настоящие теги, поскольку мы действительно используем их только для их структурной позиции в документе, это действительно не вызывает проблемы.

разметка и код выглядят следующим образом:

<!DOCTYPE html>
<html>
<head>
<script>

  /// function for parsing HTML source to a dom structure
  /// Tested in Mac OSX, Win 7, Win XP with FF, IE 7/8/9, 
  /// Chrome, Safari & Opera.
  function parseHTML(src){

    /// create a random div, this will be our root
    var div = document.createElement('div'),
        /// specificy our namespace prefix
        ns = 'faux:',
        /// state which tags we will treat as "special"
        stn = ['html','head','body','title'];
        /// the reg exp for replacing the special tags
        re = new RegExp('<(/?)('+stn.join('|')+')([^>]*)?>','gi'),
        /// remember the getElementsByTagName function before we override it
        gtn = div.getElementsByTagName;

    /// a quick function to namespace certain tag names
    var nspace = function(tn){
      if ( stn.indexOf ) {
        return stn.indexOf(tn) != -1 ? ns + tn : tn;
      }
      else {
        return ('|'+stn.join('|')+'|').indexOf(tn) != -1 ? ns + tn : tn;
      }
    };

    /// search and replace our source so that special tags are namespaced
    /// &nbsp; required for IE7/8 to render tags before first text found
    /// <faux:check /> tag added so we can test how namespaces work
    src = '&nbsp;<'+ns+'check />' + src.replace(re,'<$1'+ns+'$2$3>');
    /// inject to the div
    div.innerHTML = src;
    /// quick test to see how we support namespaces in TagName searches
    if ( !div.getElementsByTagName(ns+'check').length ) {
      ns = '';
    }

    /// create our replacement getByName and getById functions
    var createGetElementByAttr = function(attr, collect){
      var func = function(a,w){
        var i,c,e,f,l,o; w = w||[];
        if ( this.nodeType == 1 ) {
          if ( this.getAttribute(attr) == a ) {
            if ( collect ) {
              w.push(this);
            }
            else {
              return this;
            }
          }
        }
        else {
          return false;
        }
        if ( (c = this.childNodes) && (l = c.length) ) {
          for( i=0; i<l; i++ ){
            if( (e = c[i]) && (e.nodeType == 1) ) {
              if ( (f = func.call( e, a, w )) && !collect ) {
                return f;
              }
            }
          }
        }
        return (w.length?w:false);
      }
      return func;
    }

    /// apply these replacement functions to the div container, obviously 
    /// you could add these to prototypes for browsers the support element 
    /// constructors. For other browsers you could step each element and 
    /// apply the functions through-out the node tree... however this would  
    /// be quite messy, far better just to always call from the root node - 
    /// or use div.getElementsByTagName.call( localElement, 'tag' );
    div.getElementsByTagName = function(t){return gtn.call(this,nspace(t));}
    div.getElementsByName    = createGetElementByAttr('name', true);
    div.getElementById       = createGetElementByAttr('id', false);

    /// return the final element
    return div;
  }

  window.onload = function(){

    /// parse the HTML source into a node tree
    var dom = parseHTML( document.getElementById('source').innerHTML );

    /// test some look ups :)
    var a = dom.getElementsByTagName('head'),
        b = dom.getElementsByTagName('title'),
        c = dom.getElementsByTagName('script'),
        d = dom.getElementById('body');

    /// alert the result
    alert(a[0].innerHTML);
    alert(b[0].innerHTML);
    alert(c[0].innerHTML);
    alert(d.innerHTML);

  }
</script>
</head>
<body>
  <xmp id="source">
    <!DOCTYPE html>
    <html>
    <head>
      <!-- Comment //-->
      <meta charset="utf-8">
      <meta name="robots" content="index, follow">
      <title>An example</title>
      <link href="test.css" />
      <script>alert('of parsing..');</script>
    </head>
    <body id="body">
      <b>in a similar way to createDocumentFragment</b>
    </body>
    </html>
  </xmp>
</body>
</html>