Можно ли загрузить весь HTML-документ в фрагмент документа в Internet Explorer? - PullRequest
42 голосов
/ 19 сентября 2011

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

Некоторые ограничения, на которые я хотел бы обратить внимание:

  • Я неЯ не хочу использовать библиотеки (например, jQuery).Здесь слишком много наворотов для того, что мне нужно сделать.
  • Ни при каких обстоятельствах не следует выполнять сценарии с удаленной страницы (по соображениям безопасности).
  • API DOM, такие как 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> внутрифрагмент, все еще не повезло.

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

Ответы [ 7 ]

78 голосов
/ 24 сентября 2011

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

DocumentFragment не реализует методы DOM.Использование document.createElement в сочетании с innerHTML удаляет теги <head> и <body> (даже если созданный элемент является корневым элементом, <html>).Поэтому решение следует искать в другом месте.Я создал функцию кросс-браузер string-to-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 в <meta http-equiv=refresh> можно записать различными способами), динамически созданные RegExps частично создаются функцией ae ( A ny *).1054 * E ntity).Фактические замены выполняются функцией by (замените на ).В этой реализации by добавляет data- перед всеми сопоставленными атрибутами.

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

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

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

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

  • Установка для свойства designMode значения «Вкл.» Остановит выполнение сценариев фреймом (не поддерживается в Chrome).Если вам нужно сохранить теги <script> по определенной причине, вы можете использовать iframe.designMode = "On" вместо функции удаления сценариев.
  • Мне не удалось найти надежный источник для 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();

Известные ссылки

4 голосов
/ 19 сентября 2011

Не уверен, почему вы возитесь с 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;
}
2 голосов
/ 25 сентября 2011

Я не уверен, что 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.
1 голос
/ 19 сентября 2011

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

0 голосов
/ 25 января 2018

Чтобы использовать полные возможности HTML DOM без запуска запросов, без необходимости работать с несовместимостями:

var doc = document.cloneNode();
if (!doc.documentElement) {
    doc.appendChild(doc.createElement('html'));
    doc.documentElement.appendChild(doc.createElement('head'));
    doc.documentElement.appendChild(doc.createElement('body'));
}

Все готово!Документ является HTML-документ, но он не в сети.

0 голосов
/ 13 октября 2012

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

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

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

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

В основном, система опирается на тот факт, что <tag> и <namespace:tag> обрабатываются по-разному. Как было обнаружено, некоторые специальные теги не могут существовать в элементе 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>
0 голосов
/ 24 сентября 2011

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...