Skip to content

Instantly share code, notes, and snippets.

@dead-claudia
Created June 11, 2015 12:52
Show Gist options
  • Save dead-claudia/92a40db30a521413b684 to your computer and use it in GitHub Desktop.
Save dead-claudia/92a40db30a521413b684 to your computer and use it in GitHub Desktop.
Run other languages in the browser as first class citizens (uses MutationObserver with a CSS-based fallback)
/**
* This allows for compile-to-JS languages to run as first-class citizens
* in a browser. Very useful for development and smaller apps. It depends
* on MutationObserver, with a fallback to the deprecated DOM
*
* Usage:
*
* ```js
* installLanguage('text/coffee', CoffeeScript.compile, null, true);
* installLanguage('text/ls', require('livescript').run);
* ```
*/
window.installLanguage = (function (document) {
'use strict';
var currentScript = document.currentScript;
var parentNode = currentScript.parentNode;
var generated = {};
var currentId = 0;
var addListener = parentNode.addEventListener ? function (event, node, callback) {
node.addEventListener(event, callback, false);
} : function (event, node, callback) {
node.attachEvent(event, callback);
};
var removeListener = parentNode.removeEventListener ? function (event, node, callback) {
node.removeEventListener(event, callback, false);
} : function (event, node, callback) {
node.detachEvent(event, callback);
};
function insertNode(node) {
parentNode.insertBefore(node, currentScript);
}
function insert(node) {
var id = currentId++;
generated[id] = node;
var listener = function () {
delete generated[id];
removeListener('load', node, listener);
listener = null; // to prevent memory leaks
};
addListener('load', node, listener);
insertNode(node);
}
function forEach(xs, f) {
for (var i = 0; i < xs.length; i++) {
f(xs[i], i);
}
}
var observe = typeof MutationObserver !== 'undefined' ? function (mime, run, opts) {
var g = generated;
new MutationObserver(function (updates) {
forEach(updates, function (update) {
forEach(update.addedNodes, function (node) {
if (node.nodeType === 'SCRIPT' &&
node.type === mime &&
g.indexOf(node) === -1) {
run(node.text, opts);
}
});
});
}).observe(document, {
childList: true,
subtree: true
});
} : (function () {
// http://www.happycode.info/create-css-classes-with-javascript/
// Edited to DRY some of it, and to iterate in reverse instead of repeated
// assignment in loops
function createCSSSelectorRules(rules, selector) {
for (var i = 0, len = rules.length; i < len; i++) {
var current = rules[i];
if (current.selectorText &&
current.selectorText.toUpperCase() == selector.toUpperCase()) {
current.style.cssText = style;
return true;
}
}
}
function rule(selector, style) {
var styleSheets = document.styleSheets;
if (!styleSheets) {
return;
}
var len = styleSheets.length;
var styleSheet, mediaType;
selector = selector.toUpperCase();
// Reverse iteration simplifies this algorithm tremendously, since the last enabled
// stylesheet is what we're getting.
for (var i = len; i >= 0; i--) {
var current = styleSheets[i];
if (!current.disabled) {
var media = current.media;
mediaType = typeof media;
if (mediaType === 'string' && media === '' || media.indexOf('screen') !== -1) {
styleSheet = current;
} else if (mediaType === 'object') {
var mediaText = media.mediaText;
if (mediaText === '' || mediaText.indexOf('screen') !== -1) {
styleSheet = current;
}
}
if (typeof styleSheet === 'undefined') {
var elem = document.createElement('style');
elem.type = 'text/css';
insertNode(elem);
for (var i = len; i >= 0; i--) {
var current = styleSheets[i];
if (!current.disabled) {
styleSheet = current;
break;
}
}
mediaType = typeof styleSheet.media;
break;
}
}
}
if (mediaType === 'string') {
if (!createCSSSelectorRules(styleSheet.rules, selector)) {
styleSheet.addRule(selector, style);
}
} else if (mediaType === 'object') {
var rules = styleSheet.cssRules;
if (!rules || !createCSSSelectorRules(rules, selector)) {
styleSheet.insertRule(selector + '{' + style + '}', rules ? rules.length : 0);
}
}
}
var prefixes = ['', '-moz-', '-webkit-', '-ms-', '-o-'];
var list = new Array(10);
for (var i = 0, j = 5, len = prefixes.length; i < len; i++, j++) {
var prefix = prefixes[i];
rule('@' + prefix + 'keyframes nodeInserted', 'from{outline-color:#fff}to{outline-color:#000}');
list[i] = prefix + 'animation-duration:0.01s';
list[j] = prefix + 'animation-name:0.01s';
}
var source = list.join(';');
return function (mime, run, opts) {
function listener(event) {
if (event.animationName === 'nodeInserted') {
var node = event.target;
if (g.indexOf(node) === -1) {
run(node.text, opts);
}
}
}
addListener('animationstart', document, event);
addListener('MSAnimationStart', document, event);
addListener('webkitAnimationStart', document, event);
rule('script[type="' + mime.replace(/"/g, '\\"') + '"]', source);
};
})();
return function (mime, run, opts, compiles) {
if (opts != null) {
var oldRun = run;
run = function (code) {
oldRun(code);
};
}
if (compiles) {
var compiler = run;
run = function (code, opts) {
var elem = document.createElement('script');
elem.type = 'text/javascript';
elem.text = compiler(code, opts);
insert(elem);
};
}
observe(mime, run, opts);
};
})(document);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment