Created
November 14, 2013 21:48
-
-
Save andreypopp/7474901 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Copyright 2013 Facebook, Inc. | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
* | |
* @providesModule mutateHTMLNodeWithMarkup | |
* @typechecks static-only | |
*/ | |
/*jslint evil: true */ | |
'use strict'; | |
var createNodesFromMarkup = require('createNodesFromMarkup'); | |
var createArrayFrom = require('createArrayFrom'); | |
var emptyFunction = require('emptyFunction'); | |
var filterAttributes = require('filterAttributes'); | |
var invariant = require('invariant'); | |
/** | |
* Sync DOM attributes from one DOM node to another. If fromNode is null then | |
* just remove all attributes from node. | |
* | |
* @param {DOMElement|null} fromNode | |
* @param {DOMElement} toNode | |
*/ | |
function syncAttributes(fromNode, toNode) { | |
if (fromNode !== null) { | |
// Add all attributes present in fromNode | |
var attributesToSet = filterAttributes( | |
fromNode, | |
function(attr) { | |
return toNode.getAttributeNS(attr.namespaceURI, attr.name) !== attr.value; | |
} | |
); | |
attributesToSet.forEach(function(attr) { | |
toNode.setAttributeNS(attr.namespaceURI, attr.name, attr.value); | |
}); | |
} | |
// Remove all attributes not present in fromNode | |
var attributesToRemove = filterAttributes( | |
toNode, | |
function(attr) { | |
// Remove all attributes if fromNode is null or if it does not have | |
// the desired attribute. | |
return !( | |
fromNode && | |
fromNode.hasAttributeNS(attr.namespaceURI, attr.name) | |
); | |
} | |
); | |
attributesToRemove.forEach(function(attr) { | |
toNode.removeAttributeNS(attr.namespaceURI, attr.name); | |
}); | |
} | |
function indexOfLink(node, child) { | |
for (var i = 0, len = node.childNodes.length; i < len; i++) { | |
var n = node.childNodes[i]; | |
if (n.tagName === child.tagName && | |
n.rel === child.rel && | |
n.href === child.href && | |
n.target === child.target && | |
n.type === child.type) | |
return i; | |
} | |
return -1; | |
} | |
/** | |
* This function updates <head> element with special care for <link> elements so | |
* they are not moved with inside DOM tree with no reason so no flicks happen | |
* during stylesheet reappending. | |
* | |
* @param {DOMElement} node with tagName == 'head' | |
* @param {string} markup markup string including <head>. | |
*/ | |
function mutateHEADNodeWithMarkup(node, markup) { | |
var doc = node.ownerDocument; | |
var wrap = doc.createElement('html'); | |
wrap.innerHTML = markup; | |
var head = wrap.childNodes[0]; | |
syncAttributes(head, node); | |
// build a list of new child nodes for <head> element, if we encounter <link> | |
// element then check if we should reuse already existent element so we are | |
// not triggering another request | |
var childNodes = []; | |
for (var i = 0, len = head.childNodes.length; i < len; i++) { | |
var child = head.childNodes[i]; | |
if (child.tagName === 'LINK') { | |
var idx = indexOfLink(node, child); | |
if (idx > -1) { | |
syncAttributes(child, node.childNodes[idx]); | |
childNodes.push(node.childNodes[idx]); | |
continue; | |
} | |
} | |
childNodes.push(child); | |
} | |
var oldChildNodes = createArrayFrom(node.childNodes); | |
for (var i = 0, len = Math.max(childNodes.length, oldChildNodes.length); i < len; i++) { | |
var oldChild = oldChildNodes[i]; | |
var newChild = childNodes[i]; | |
if (oldChild && childNodes.indexOf(oldChild) === -1) { | |
node.removeChild(oldChild); | |
} | |
if (newChild && newChild.parentNode !== node) { | |
node.appendChild(newChild); | |
} | |
} | |
} | |
/** | |
* You can't set the innerHTML of a document. Unless you have | |
* this function. | |
* | |
* @param {DOMElement} node with tagName == 'html' | |
* @param {string} markup markup string including <html>. | |
*/ | |
function mutateHTMLNodeWithMarkup(node, markup) { | |
invariant( | |
node.tagName.toLowerCase() === 'html', | |
'mutateHTMLNodeWithMarkup(): node must have tagName of "html", got %s', | |
node.tagName | |
); | |
markup = markup.trim(); | |
invariant( | |
markup.toLowerCase().indexOf('<html') === 0, | |
'mutateHTMLNodeWithMarkup(): markup must start with <html' | |
); | |
var doc = node.ownerDocument; | |
var attributeHolder; | |
invariant( | |
doc !== null, | |
'documentElement should be in DOM' | |
); | |
invariant( | |
doc.head !== null, | |
'document.head should not null' | |
); | |
invariant( | |
doc.body !== null, | |
'document.body should not null' | |
); | |
var htmlOpenTagEnd = markup.indexOf('>') + 1; | |
var htmlCloseTagStart = markup.lastIndexOf('<'); | |
var htmlOpenTag = markup.substring(0, htmlOpenTagEnd); | |
attributeHolder = createNodesFromMarkup( | |
htmlOpenTag.replace('html ', 'span ') + '</span>' | |
)[0]; | |
syncAttributes(attributeHolder, node); | |
markup = markup.substring(htmlOpenTagEnd, htmlCloseTagStart); | |
var headOpenTagStart = markup.indexOf('<head'); | |
var headCloseTagEnd = markup.indexOf('</head>') + 7; | |
mutateHEADNodeWithMarkup(doc.head, markup.substring(headOpenTagStart, headCloseTagEnd)); | |
markup = markup.substring(headCloseTagEnd); | |
var bodyOpenTagStart = markup.indexOf('<body'); | |
var bodyOpenTagEnd = markup.indexOf('>') + 1; | |
var bodyCloseTagStart = markup.indexOf('</body>') + 1; | |
var bodyOpenTag = markup.substring(bodyOpenTagStart, bodyOpenTagEnd); | |
attributeHolder = createNodesFromMarkup( | |
bodyOpenTag.replace('body', 'span ') + '</span>' | |
)[0]; | |
syncAttributes(attributeHolder, doc.body); | |
doc.body.innerHTML = markup.substring(bodyOpenTagEnd, bodyCloseTagStart - 1); | |
} | |
module.exports = mutateHTMLNodeWithMarkup; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment