Skip to content

Instantly share code, notes, and snippets.

@neilj
Created October 18, 2013 05:26
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save neilj/7036874 to your computer and use it in GitHub Desktop.
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.
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