Created
October 18, 2013 05:26
-
-
Save neilj/7036874 to your computer and use it in GitHub Desktop.
Rendering HTML with styles in a way that doesn't conflict with other styles on the page, without using an iframe for each HTML fragment.
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
var styleFrame = document.createElement( 'iframe' ), | |
stylesWaiting = [], | |
styleFrameIsReady = false; | |
styleFrame.setAttribute( 'style', | |
'visibility:hidden;position:absolute;top:0;left:0;width:1px;height:1px;' ); | |
styleFrame.addEventListener( 'load', function () { | |
var doc = styleFrame.contentDocument, | |
html, i, l; | |
// Check document has actually loaded. | |
if ( !doc ) { return; } | |
// Make sure we're in standards mode. | |
if ( doc.compatMode !== 'CSS1Compat' ) { | |
doc.open(); | |
doc.write( | |
'<!DOCTYPE html><html><head></head><body></body></html>' ); | |
doc.close(); | |
} | |
html = doc.documentElement; | |
if ( !styleFrameIsReady && html && html.firstChild ) { | |
styleFrameIsReady = true; | |
if ( l = stylesWaiting.length ) { | |
for ( i = 0; i < l; i += 1 ) { | |
applyStyles( stylesWaiting[i][0], stylesWaiting[i][1] ); | |
} | |
} | |
stylesWaiting = null; | |
} | |
}, false ); | |
document.body.appendChild( styleFrame ); | |
var camelCase = function ( string ) { | |
return string.replace( /\-([a-z])/g, function ( _, letter ) { | |
return letter.toUpperCase(); | |
}); | |
}; | |
var translate = { | |
'float': 'cssFloat', | |
'margin-left-value': 'marginLeft', | |
'margin-left-ltr-source': '', | |
'margin-left-rtl-source': '', | |
'margin-right-value': 'marginRight', | |
'margin-right-ltr-source': '', | |
'margin-right-rtl-source': '', | |
'padding-right-value': 'paddingRight', | |
'padding-right-ltr-source': '', | |
'padding-right-rtl-source': '', | |
'padding-left-value': 'paddingLeft', | |
'padding-left-ltr-source': '', | |
'padding-left-rtl-source': '' | |
}; | |
var STYLE_RULE = 1; // CSSRule.STYLE_RULE | |
var MEDIA_RULE = 4; // CSSRule.MEDIA_RULE | |
// Very basic, but close to compliant @media rule parser | |
// for the subset we care about. Good enough for now. | |
var testMedia = function ( media ) { | |
if ( /^only /.test( media ) ) { | |
media = media.slice( 5 ); | |
} | |
var parts = media.split( ' and ' ), | |
l = parts.length, | |
part, query; | |
while ( l-- ) { | |
part = parts[l]; | |
if ( part === 'all' || part === 'screen' ) { | |
continue; | |
} | |
if ( query = /^\(m(in|ax)\-(.*?):\s*(\d+)px\s*\)/.exec( part ) ) { | |
var type = query[2], | |
actualValue = | |
type === 'device-width' ? screen.width : | |
type === 'device-height' ? screen.height : | |
type === 'width' ? document.body.offsetWidth : | |
type === 'height' ? document.body.offsetHeight : | |
undefined, | |
requiredValue = +query[3]; | |
if ( query[1] === 'in' ? | |
actualValue >= requiredValue : | |
actualValue <= requiredValue ) { | |
continue; | |
} | |
} | |
return false; | |
} | |
return true; | |
}; | |
var applyStyleSheet = function ( root, stylesheet ) { | |
// Apply rules | |
var rules = stylesheet.cssRules, | |
media = stylesheet.media, | |
ruleLength = media ? media.length : 0, | |
rule, ruleStyle, selector, | |
els, elsLength, name, value, style, | |
i, l; | |
for ( i = 0; i < ruleLength; i += 1 ) { | |
if ( testMedia( media[i] || media.mediaText || '' ) ) { | |
ruleLength = 0; | |
} | |
} | |
// If there was a match, or no media rules, this will be 0: | |
if ( ruleLength ) { | |
return; | |
} | |
// Iterate backwards through rules and don't apply style if one already | |
// exists. This approximates CSS precedence: | |
// 1. Rules don't override explicit style attributes on elements | |
// 2. Later rules override earlier rules. | |
// However, it fails to handle selector precedence or !important rules. | |
// So far, this doesn't seem to be much of a real-world issue. | |
l = rules.length; | |
while ( l-- ) { | |
rule = rules[l]; | |
if ( rule.type === STYLE_RULE ) { | |
ruleStyle = rule.style; | |
ruleLength = ruleStyle && ruleStyle.length; | |
selector = rule.selectorText; | |
if ( !ruleLength || !selector ) { continue; } | |
try { | |
els = selector === 'body' ? | |
[ root ] : root.querySelectorAll( rule.selectorText ); | |
} catch ( error ) { | |
continue; | |
} | |
elsLength = els.length; | |
while ( ruleLength-- ) { | |
name = ruleStyle[ ruleLength ]; | |
name = translate[ name ] || camelCase( name ); | |
if ( !name ) { continue; } | |
value = ruleStyle[ name ]; | |
for ( i = 0; i < elsLength; i += 1 ) { | |
style = els[i].style; | |
if ( !style[ name ] ) { | |
style[ name ] = value; | |
} | |
} | |
} | |
} else if ( rule.type === MEDIA_RULE ) { | |
applyStyleSheet( root, rule ); | |
} | |
} | |
}; | |
var applyStyles = function ( root, styles ) { | |
var doc = styleFrame.contentDocument, | |
head, stylesheet; | |
if ( !doc || !styleFrameIsReady ) { | |
if ( !styleFrameIsReady ) { | |
stylesWaiting.push([ root, styles ]); | |
} | |
return; | |
} | |
// Create stylesheet | |
head = doc.documentElement.firstChild; | |
stylesheet = doc.createElement( 'style' ); | |
stylesheet.type = 'text/css'; | |
stylesheet.appendChild( doc.createTextNode( styles ) ); | |
head.appendChild( stylesheet ); | |
applyStyleSheet( root, doc.styleSheets[0] ); | |
// Remove stylesheet | |
stylesheet.parentNode.removeChild( stylesheet ); | |
}; | |
var removeIdsAndClasses = function ( root ) { | |
var id = root.id, | |
children, child, i, l; | |
if ( id ) { | |
root.removeAttribute( 'id' ); | |
} | |
if ( root.className ) { | |
root.removeAttribute( 'class' ); | |
} | |
if ( children = root.childNodes ) { | |
for ( i = 0, l = children.length; i < l; i += 1 ) { | |
child = children[i]; | |
if ( child.nodeType === 1 ) { | |
removeIdsAndClasses( child ); | |
} | |
} | |
} | |
}; | |
var renderHTML = function ( html, styles ) { | |
var div = document.createElement( 'div' ); | |
div.innerHTML = html; | |
if ( styles ) { | |
applyStyles( div, styles ); | |
} | |
removeIdsAndClasses( div ); | |
return div; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment