Skip to content

Instantly share code, notes, and snippets.

@lac5
Created March 18, 2020 18:06
Show Gist options
  • Save lac5/12da558d6a91da4322a1ea520e4a7e45 to your computer and use it in GitHub Desktop.
Save lac5/12da558d6a91da4322a1ea520e4a7e45 to your computer and use it in GitHub Desktop.
import { getOptions } from 'loader-utils';
import XRegExp from 'xregexp';
import toSource from 'tosource';
export default function ejsLoader(content) {
let options = getOptions(this);
let [source, ...imports] = compileEjs(content, options);
return (
`${imports.map(code =>
code + '\n;//\`/***/;'
).join('\n')}
export default ${source}`
);
}
const reservedWords = [
'abstract', 'arguments','await','boolean','break','byte','case','catch', 'char',
'class', 'const','continue','debugger','default','delete','do','double', 'else',
'enum', 'eval', 'export', 'extends', 'false','final', 'finally', 'float', 'for',
'function', 'goto', 'if', 'implements', 'import', 'in', 'instanceof', 'int',
'interface', 'let','long','native','new','null','package','private','protected',
'public', 'return', 'short', 'static','super', 'switch', 'synchronized', 'this',
'throw', 'throws', 'transient', 'true','try','typeof','var', 'void', 'volatile',
'while', 'with', 'yield',
];
/**
* @param {string} name
* @returns {string}
*/
export function normalize(name) {
if (!name) return '';
let cleanName = String(name)
.replace(/^\W+|\W+$/g, '')
.replace(/\W+[a-z]/ig, m =>
m[m.length - 1].toUpperCase())
.replace(/^(?=\d)|\W+/g, '_');
if (reservedWords.index(cleanName) > -1) {
cleanName = '_' + cleanName;
}
if (cleanName !== name) {
console.warn('"%s" changed to "%s"', name, cleanName);
}
return cleanName;
}
/**
* @param {string} content
* @param {{
* predefine: {
* [name: string]: any
* } = {},
* variables: {
* locals : string = 'locals',
* template : string = 'template',
* result : string = 'result',
* echo : string = 'echo',
* escape : string = 'escape',
* } = {},
* entities : RegExp = /[&<>\'"]/g,
* escape : RegExp = /<%-([\s\S]*?)(?:%>|$)/,
* evaluate : RegExp = /<%(?![-=])([\s\S]*?)(?:%>|$)/,
* interpolate: RegExp = /<%=([\s\S]*?)(?:%>|$)/,
* extract : RegExp = /<%\^([\s\S]*?)(?:%>|$)/,
* matching: (
* 'extract'|'escape'|'interpolate'|'evaluate'
* )[] = [
* 'extract','escape','interpolate','evaluate'
* ],
* async : boolean = false,
* minify? : (x: string) => string,
* }} [options={}]
* @returns {string[]}
*/
export function compileEjs(content, {
predefine = {},
variables = {},
entities = /[&<>\'"]/g,
escape = /<%-([\s\S]*?)(?:%>|$)/,
evaluate = /<%(?![-=])([\s\S]*?)(?:%>|$)/,
interpolate = /<%=([\s\S]*?)(?:%>|$)/,
extract = /<%\^([\s\S]*?)(?:%>|$)/,
matching ,
async ,
minify ,
} = {}) {
let template = '';
let extracts = [];
matching = matching
? Array.from(matching).map(normalize).filter(Boolean)
: ['extract','escape','interpolate','evaluate'];
const rx = XRegExp.build(
matching.map(v => `(?<${v}>{{${v}}})|`).join('') + '[\s\S]*?$',
{ escape, evaluate, interpolate, extract }
);
if (typeof minify !== 'function') {
minify = x => x;
}
variables = Object(variables);
variables.locals = normalize(variables.locals ) || 'locals' ;
variables.template = normalize(variables.template) || 'template';
variables.result = normalize(variables.result ) || 'result' ;
variables.echo = normalize(variables.echo ) || 'echo' ;
variables.escape = normalize(variables.escape ) || 'escape' ;
for (let i = 0, length = content.length; i < length;) {
let slice = content.slice(i);
let match = XRegExp.exec(slice, rx);
if (match[0]) {
let code = '';
for (let i = 1, length = match.length; i < length; i++) {
if (match[0] === match[i]) {
code = match[i + 1] || '';
break;
}
}
let string = minify(slice.slice(0, match.index));
if (string) {
template += `${variables.result} += ${JSON.stringify(String(string))};\n`;
}
if (code) {
if (match.extract) {
extracts.push(code);
} else {
template +=
match.escape ? `${variables.result} += escape(${code})\n;//\`/***/;\n` :
match.evaluate ? `${code}\n;//\`/***/;\n` :
match.interpolate ? `${variables.result} += (${code}) || ""\n;//\`/***/;\n` :
'\n;//\`/***/;\n';
}
}
i += match.index + match[0].length;
} else {
template += `${variables.result} += ${JSON.stringify(minify(slice))};\n`;
break;
}
}
return [
`${async ? 'async ' : ''}function ${variables.template}(${variables.locals}) {
"use strict";
/* predefine *****************/
${Object.entries(predefine).map(([name, value]) => {
name = normalize(name);
return (name ?
` var ${normalize(name)} = ${toSource(value)};` :
``);
}).join('\n')}
/* /predefine ****************/
var ${variables.result} = "";
var ${variables.echo} = function(input) {
${variables.result} += input;
};
var ${variables.escape} = function(input) {
return ("" + (input || "")).replace(${toSource(entities)}, function (m) {
return m.replace(/[\s\S]/g, function (c) {
return "&#" + c.charCodeAt(0) + ";";
});
});
};
/* template ******************/
${template}
/* /template *****************/
return result;
}` ,
...extracts,
];
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment