Skip to content

Instantly share code, notes, and snippets.

@andreypopp
Created November 14, 2013 21:48
Show Gist options
  • Save andreypopp/7474901 to your computer and use it in GitHub Desktop.
Save andreypopp/7474901 to your computer and use it in GitHub Desktop.
/**
* 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