otiluke
Version:
Deploy JavaScript code transformers written in JavaScript themselves on node and browsers
138 lines (129 loc) • 4.28 kB
JavaScript
const Fs = require("fs");
const Path = require("path");
const Stream = require("stream");
const Browserify = require("browserify");
const Htmlparser2 = require("htmlparser2");
const basedir = Path.parse(__dirname).root;
const encode = (string) => string.replace(/[&<>"]/g, onmatch1);
const escape = (string) => string.replace(/<\/script>/g, onmatch2);
const onmatch1 = (character) => {
switch (character) {
case "&": return "&"
case "<": return "<";
case ">": return ">";
case "\"": return """
}
throw new Error("Illegal match: "+character);
}
const onmatch2 = (string) => "<\\"+string.substring(2);
const ishandler = (string) => string.startsWith("on");
module.exports = (vpath, gvar, argmpfx) => {
let setup = `<script>alert("Otiluke >> bundling not yet performed, retry in a sec...");</script>`;
const readable = new Stream.Readable();
readable.push(
`if (!global.${gvar}) {
const Virus = require(${JSON.stringify(Path.resolve(vpath))});
const argm = {};
const geval = eval;
const url = new URL(location.href);
for (let key of url.searchParams.keys()) {
if (key.startsWith(${JSON.stringify(argmpfx)})) {
const array = url.searchParams.getAll(key);
argm[key.substring(${argmpfx.length})] = array.length === 1 ? array[0] : array;
}
}
let buffer = [];
global.${gvar} = (script, source) => { buffer.push([script, source]) };
Virus(argm, (error, transform) => {
if (error)
throw error;
global.${gvar} = (script, source) => { geval(transform(script, source)) };
for (let index = 0; index < buffer.length; index++)
global.${gvar}(buffer[index][0], buffer[index][1]);
buffer = null;
});
}`
);
readable.push(null);
Browserify(readable, {basedir}).bundle((error, bundle) => {
if (error) {
setup =
`<script>
alert(${JSON.stringify("Browserify bundling error: "+error.message)});
const error = new Error(${JSON.stringify(error.message)});
error.stack = ${JSON.stringify(error.stack)};
throw error;
</script>`
} else {
setup = `<script>${escape(bundle.toString("utf8"))}</script>`;
}
});
let counter;
let script;
const prelude = [];
const output = [];
const parser = new Htmlparser2.Parser({
onopentag: (name, attributes) => {
if (name === "head") {
output.push(setup);
}
output.push("<", encode(name));
if (!("id" in attributes) && (Object.keys(attributes).some(ishandler) || name === "script"))
attributes.id = "__otiluke"+(++counter)+"__";
for (let key in attributes) {
if (ishandler(key)) {
prelude.push(
gvar,
"(",
JSON.stringify("document.getElementById("+JSON.stringify(attributes.id)+")["+JSON.stringify(key)+"] = function (event) {"+attributes[key]+"};"),
", ",
JSON.stringify(attributes.id+" "+key),
");");
} else {
output.push(" ", encode(key), "=\"", encode(attributes[key]), "\"");
}
}
output.push(">");
script = name === "script" ? "" : null;
},
ontext: (text) => {
if (typeof script === "string") {
script += text;
} else {
output.push(encode(text));
}
},
onclosetag: (name) => {
if (typeof script === "string") {
output.push(escape(prelude.join("")+gvar+"("+JSON.stringify(script)+", document.currentScript.id);"));
prelude.length = 0;
script = null;
}
if (name === "body" && prelude.length) {
output.push("<script>", escape(prelude.join("")), "</script>");
prelude.length = 0;
}
output.push("</", encode(name), ">");
},
onprocessinginstruction: (name, data) => {
output.push("<", encode(data), ">");
}
}, {
decodeEntities: true
});
const javascript = (script, source) => `${gvar}(${JSON.stringify(script)}, ${JSON.stringify(source)});`;
const html = (page, source) => {
output.length = 0;
prelude.length = 0;
counter = 0;
parser.parseComplete(page);
return output.join("");
};
return (mime) => {
if (mime && mime.includes("html"))
return html;
if (mime && mime.includes("javascript"))
return javascript;
return null;
};
};