Skip to content

Commit

Permalink
Use createNodesFromMarkup
Browse files Browse the repository at this point in the history
Pulled out markup rendering logic for better reuse.
  • Loading branch information
yungsters authored and zpao committed Aug 1, 2013
1 parent dc06704 commit 808e625
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 96 deletions.
15 changes: 12 additions & 3 deletions src/core/__tests__/ReactMultiChild-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,7 @@ describe('ReactMultiChild', function() {
</div>,
container
);
expect(setInnerHTML).toHaveBeenCalled();
var callCountOnMount = setInnerHTML.callCount;

// Warm the cache used by `getMarkupWrap`.
React.renderComponent(
<div>
<p><span /><span /></p>
Expand All @@ -63,6 +61,17 @@ describe('ReactMultiChild', function() {
</div>,
container
);
expect(setInnerHTML).toHaveBeenCalled();
var callCountOnMount = setInnerHTML.callCount;

React.renderComponent(
<div>
<p><span /><span /><span /></p>
<p><span /><span /><span /></p>
<p><span /><span /><span /></p>
</div>,
container
);
expect(setInnerHTML.callCount).toBe(callCountOnMount + 1);
});
});
117 changes: 24 additions & 93 deletions src/dom/Danger.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,65 +23,13 @@

var ExecutionEnvironment = require('ExecutionEnvironment');

var createNodesFromMarkup = require('createNodesFromMarkup');
var emptyFunction = require('emptyFunction');
var getMarkupWrap = require('getMarkupWrap');
var invariant = require('invariant');

/**
* Dummy container used to render all markup.
*/
var dummyNode = ExecutionEnvironment.canUseDOM ?
document.createElement('div') :
null;

/**
* Some browsers cannot use `innerHTML` to render certain elements standalone,
* so we wrap them, render the wrapped nodes, then extract the desired node.
*/
var markupWrap = {
'option': [1, '<select multiple="true">', '</select>'],
'legend': [1, '<fieldset>', '</fieldset>'],
'area': [1, '<map>', '</map>'],
'param': [1, '<object>', '</object>'],
'thead': [1, '<table>', '</table>'],
'tr': [2, '<table><tbody>', '</tbody></table>'],
'col': [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'],
'td': [3, '<table><tbody><tr>', '</tr></tbody></table>']
};
markupWrap['optgroup'] = markupWrap['option'];
markupWrap['tbody'] = markupWrap['thead'];
markupWrap['tfoot'] = markupWrap['thead'];
markupWrap['colgroup'] = markupWrap['thead'];
markupWrap['caption'] = markupWrap['thead'];
markupWrap['th'] = markupWrap['td'];

/**
* In IE8, certain elements cannot render alone, so wrap all elements.
*/
var defaultWrap = [1, '?<div>', '</div>'];

/**
* Feature detection, remove wraps that are unnecessary for the current browser.
*/
if (dummyNode) {
for (var nodeName in markupWrap) {
if (!markupWrap.hasOwnProperty(nodeName)) {
continue;
}
dummyNode.innerHTML = '<' + nodeName + '></' + nodeName + '>';
if (dummyNode.firstChild) {
markupWrap[nodeName] = null;
}
}
dummyNode.innerHTML = '<link />';
if (dummyNode.firstChild) {
defaultWrap = null;
}
}

/**
* Extracts the `nodeName` from a string of markup. This does not require a
* regular expression match because we make assumptions about React-generated
* markup (i.e. there are no spaces surrounding the opening tag and there is at
* least an ID attribute).
* Extracts the `nodeName` from a string of markup.
*
* NOTE: Extracting the `nodeName` does not require a regular expression match
* because we make assumptions about React-generated markup (i.e. there are no
Expand All @@ -95,31 +43,6 @@ function getNodeName(markup) {
return markup.substring(1, markup.indexOf(' '));
}

/**
* Renders markup into nodes. The returned HTMLCollection is live and should be
* used immediately (or at least before the next invocation to `renderMarkup`).
*
* @param {string} markup Markup for one or more nodes with the same `nodeName`.
* @param {?string} nodeName Optional, the lowercase node name of the markup.
* @return {*} An HTMLCollection.
*/
function renderMarkup(markup, nodeName) {
nodeName = nodeName || getNodeName(markup);
var node = dummyNode;
var wrap = markupWrap[nodeName] || defaultWrap;
if (wrap) {
node.innerHTML = wrap[1] + markup + wrap[2];

var wrapDepth = wrap[0];
while (wrapDepth--) {
node = node.lastChild;
}
} else {
node.innerHTML = markup;
}
return node.childNodes;
}

var Danger = {

/**
Expand Down Expand Up @@ -147,31 +70,39 @@ var Danger = {
'dangerouslyRenderMarkup(...): Missing markup.'
);
nodeName = getNodeName(markupList[i]);
nodeName = markupWrap[nodeName] ? nodeName : '*';
nodeName = getMarkupWrap(nodeName) ? nodeName : '*';
markupByNodeName[nodeName] = markupByNodeName[nodeName] || [];
markupByNodeName[nodeName][i] = markupList[i];
}
var renderedMarkup = [];
var resultList = [];
for (nodeName in markupByNodeName) {
if (!markupByNodeName.hasOwnProperty(nodeName)) {
continue;
}
var markupListByNodeName = markupByNodeName[nodeName];
var markup = markupListByNodeName.join('');
// Render each group of markup.
var childNode = renderMarkup(markup, nodeName)[0];
// Render each group of markup with similar `nodeName`.
var renderNodes = createNodesFromMarkup(markup, emptyFunction);
// Restore the initial ordering.
for (var j = 0; j < markupListByNodeName.length; j++) {
var renderIndex = 0;
var resultIndex = 0;
while (resultIndex < markupListByNodeName.length) {
// `markupListByNodeName` may be a sparse array.
if (markupListByNodeName[j]) {
invariant(childNode, 'dangerouslyRenderMarkup(...): Missing node.');
renderedMarkup[j] = childNode;
childNode = childNode.nextSibling;
if (markupListByNodeName[resultIndex]) {
invariant(
resultList[resultIndex] = renderNodes[renderIndex],
'dangerouslyRenderMarkup(...): Missing node.'
);
renderIndex++;
}
resultIndex++;
}
invariant(!childNode, 'dangerouslyRenderMarkupO(...): Unexpected nodes.');
invariant(
renderIndex === renderNodes.length,
'dangerouslyRenderMarkupO(...): Unexpected nodes.'
);
}
return renderedMarkup;
return resultList;
},

/**
Expand All @@ -190,7 +121,7 @@ var Danger = {
'immediately.'
);
invariant(markup, 'dangerouslyReplaceNodeWithMarkup(...): Missing markup.');
var newChild = renderMarkup(markup)[0];
var newChild = createNodesFromMarkup(markup, emptyFunction)[0];
oldChild.parentNode.replaceChild(newChild, oldChild);
}

Expand Down
56 changes: 56 additions & 0 deletions src/vendor/core/createArrayFrom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* 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 createArrayFrom
* @typechecks
*/

var hasArrayNature = require('hasArrayNature');

/**
* Ensure that the argument is an array by wrapping it in an array if it is not.
* Creates a copy of the argument if it is already an array.
*
* This is mostly useful idiomatically:
*
* var createArrayFrom = require('createArrayFrom');
*
* function takesOneOrMoreThings(things) {
* things = createArrayFrom(things);
* ...
* }
*
* This allows you to treat `things' as an array, but accept scalars in the API.
*
* This is also good for converting certain pseudo-arrays, like `arguments` or
* HTMLCollections, into arrays.
*
* @param {*} obj
* @return {array}
*/
function createArrayFrom(obj) {
if (!hasArrayNature(obj)) {
return [obj];
}
if (obj.item) {
// IE does not support Array#slice on HTMLCollections
var l = obj.length, ret = new Array(l);
while (l--) { ret[l] = obj[l]; }
return ret;
}
return Array.prototype.slice.call(obj);
}

module.exports = createArrayFrom;
89 changes: 89 additions & 0 deletions src/vendor/core/createNodesFromMarkup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* 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 createNodesFromMarkup
* @typechecks
*/

/*jslint evil: true, sub: true */

var createArrayFrom = require('createArrayFrom');
var getMarkupWrap = require('getMarkupWrap');
var invariant = require('invariant');

/**
* Dummy container used to render all markup.
*/
var dummyNode = document.createElement('div');

/**
* Pattern used by `getNodeName`.
*/
var nodeNamePattern = /^\s*<(\w+)/;

/**
* Extracts the `nodeName` of the first element in a string of markup.
*
* @param {string} markup String of markup.
* @return {?string} Node name of the supplied markup.
*/
function getNodeName(markup) {
var nodeNameMatch = markup.match(nodeNamePattern);
return nodeNameMatch && nodeNameMatch[1].toLowerCase();
}

/**
* Creates an array containing the nodes rendered from the supplied markup. The
* optionally supplied `handleScript` function will be invoked once for each
* <script> element that is rendered. If no `handleScript` function is supplied,
* an exception is thrown if any <script> elements are rendered.
*
* @param {string} markup A string of valid HTML markup.
* @param {?function} handleScript Invoked once for each rendered <script>.
* @return {array<DOMElement|DOMTextNode>} An array of rendered nodes.
*/
function createNodesFromMarkup(markup, handleScript) {
var node = dummyNode;
var nodeName = getNodeName(markup);

var wrap = nodeName && getMarkupWrap(nodeName);
if (wrap) {
node.innerHTML = wrap[1] + markup + wrap[2];

var wrapDepth = wrap[0];
while (wrapDepth--) {
node = node.lastChild;
}
} else {
node.innerHTML = markup;
}

var scripts = node.getElementsByTagName('script');
if (scripts.length) {
invariant(
handleScript,
'createNodesFromMarkup(...): Unexpected <script> element rendered.'
);
createArrayFrom(scripts).forEach(handleScript);
}

var nodes = createArrayFrom(node.childNodes);
while (node.lastChild) {
node.removeChild(node.lastChild);
}
return nodes;
}

module.exports = createNodesFromMarkup;
Loading

0 comments on commit 808e625

Please sign in to comment.