240 lines
6.7 KiB
JavaScript
240 lines
6.7 KiB
JavaScript
"use strict"
|
|
|
|
// doT.js
|
|
// 2011-2014, Laura Doktorova, https://github.com/olado/doT
|
|
// Licensed under the MIT license.
|
|
|
|
const doT = {
|
|
templateSettings: {
|
|
argName: "it",
|
|
encoders: {},
|
|
selfContained: false,
|
|
strip: true,
|
|
internalPrefix: "_val",
|
|
encodersPrefix: "_enc",
|
|
delimiters: {
|
|
start: "{{",
|
|
end: "}}",
|
|
},
|
|
},
|
|
template,
|
|
compile,
|
|
setDelimiters,
|
|
}
|
|
|
|
module.exports = doT
|
|
|
|
// depends on selfContained mode
|
|
const encoderType = {
|
|
false: "function",
|
|
true: "string",
|
|
}
|
|
|
|
const defaultSyntax = {
|
|
evaluate: /\{\{([\s\S]+?(\}?)+)\}\}/g,
|
|
interpolate: /\{\{=([\s\S]+?)\}\}/g,
|
|
typeInterpolate: /\{\{%([nsb])=([\s\S]+?)\}\}/g,
|
|
encode: /\{\{([a-z_$]+[\w$]*)?!([\s\S]+?)\}\}/g,
|
|
use: /\{\{#([\s\S]+?)\}\}/g,
|
|
useParams: /(^|[^\w$])def(?:\.|\[[\'\"])([\w$\.]+)(?:[\'\"]\])?\s*\:\s*([\w$]+(?:\.[\w$]+|\[[^\]]+\])*|\"[^\"]+\"|\'[^\']+\'|\{[^\}]+\})/g,
|
|
define: /\{\{##\s*([\w\.$]+)\s*(\:|=)([\s\S]+?)#\}\}/g,
|
|
defineParams: /^\s*([\w$]+):([\s\S]+)/,
|
|
conditional: /\{\{\?(\?)?\s*([\s\S]*?)\s*\}\}/g,
|
|
iterate: /\{\{~\s*(?:\}\}|([\s\S]+?)\s*\:\s*([\w$]+)\s*(?:\:\s*([\w$]+))?\s*\}\})/g,
|
|
}
|
|
|
|
let currentSyntax = {...defaultSyntax}
|
|
|
|
const TYPES = {
|
|
n: "number",
|
|
s: "string",
|
|
b: "boolean",
|
|
}
|
|
|
|
function resolveDefs(c, syn, block, def) {
|
|
return (typeof block === "string" ? block : block.toString())
|
|
.replace(syn.define, (_, code, assign, value) => {
|
|
if (code.indexOf("def.") === 0) {
|
|
code = code.substring(4)
|
|
}
|
|
if (!(code in def)) {
|
|
if (assign === ":") {
|
|
value.replace(syn.defineParams, (_, param, v) => {
|
|
def[code] = {arg: param, text: v}
|
|
})
|
|
if (!(code in def)) def[code] = value
|
|
} else {
|
|
new Function("def", `def['${code}']=${value}`)(def)
|
|
}
|
|
}
|
|
return ""
|
|
})
|
|
.replace(syn.use, (_, code) => {
|
|
code = code.replace(syn.useParams, (_, s, d, param) => {
|
|
if (def[d] && def[d].arg && param) {
|
|
const rw = unescape((d + ":" + param).replace(/'|\\/g, "_"))
|
|
def.__exp = def.__exp || {}
|
|
def.__exp[rw] = def[d].text.replace(
|
|
new RegExp(`(^|[^\\w$])${def[d].arg}([^\\w$])`, "g"),
|
|
`$1${param}$2`
|
|
)
|
|
return s + `def.__exp['${rw}']`
|
|
}
|
|
})
|
|
const v = new Function("def", "return " + code)(def)
|
|
return v ? resolveDefs(c, syn, v, def) : v
|
|
})
|
|
}
|
|
|
|
function unescape(code) {
|
|
return code.replace(/\\('|\\)/g, "$1").replace(/[\r\t\n]/g, " ")
|
|
}
|
|
|
|
function template(tmpl, c, def) {
|
|
const ds = c && c.delimiters
|
|
const syn = ds && !sameDelimiters(ds) ? getSyntax(ds) : currentSyntax
|
|
c = c ? {...doT.templateSettings, ...c} : doT.templateSettings
|
|
let sid = 0
|
|
let str = resolveDefs(c, syn, tmpl, def || {})
|
|
const needEncoders = {}
|
|
|
|
str = (
|
|
"let out='" +
|
|
(c.strip
|
|
? str
|
|
.trim()
|
|
.replace(/[\t ]+(\r|\n)/g, "\n") // remove trailing spaces
|
|
.replace(/(\r|\n)[\t ]+/g, " ") // leading spaces reduced to " "
|
|
.replace(/\r|\n|\t|\/\*[\s\S]*?\*\//g, "") // remove breaks, tabs and JS comments
|
|
: str
|
|
)
|
|
.replace(/'|\\/g, "\\$&")
|
|
.replace(syn.interpolate, (_, code) => `'+(${unescape(code)})+'`)
|
|
.replace(syn.typeInterpolate, (_, typ, code) => {
|
|
sid++
|
|
const val = c.internalPrefix + sid
|
|
const error = `throw new Error("expected ${TYPES[typ]}, got "+ (typeof ${val}))`
|
|
return `';const ${val}=(${unescape(code)});if(typeof ${val}!=="${
|
|
TYPES[typ]
|
|
}") ${error};out+=${val}+'`
|
|
})
|
|
.replace(syn.encode, (_, enc = "", code) => {
|
|
needEncoders[enc] = true
|
|
code = unescape(code)
|
|
const e = c.selfContained ? enc : enc ? "." + enc : '[""]'
|
|
return `'+${c.encodersPrefix}${e}(${code})+'`
|
|
})
|
|
.replace(syn.conditional, (_, elseCase, code) => {
|
|
if (code) {
|
|
code = unescape(code)
|
|
return elseCase ? `';}else if(${code}){out+='` : `';if(${code}){out+='`
|
|
}
|
|
return elseCase ? "';}else{out+='" : "';}out+='"
|
|
})
|
|
.replace(syn.iterate, (_, arr, vName, iName) => {
|
|
if (!arr) return "';} } out+='"
|
|
sid++
|
|
const defI = iName ? `let ${iName}=-1;` : ""
|
|
const incI = iName ? `${iName}++;` : ""
|
|
const val = c.internalPrefix + sid
|
|
return `';const ${val}=${unescape(
|
|
arr
|
|
)};if(${val}){${defI}for (const ${vName} of ${val}){${incI}out+='`
|
|
})
|
|
.replace(syn.evaluate, (_, code) => `';${unescape(code)}out+='`) +
|
|
"';return out;"
|
|
)
|
|
.replace(/\n/g, "\\n")
|
|
.replace(/\t/g, "\\t")
|
|
.replace(/\r/g, "\\r")
|
|
.replace(/(\s|;|\}|^|\{)out\+='';/g, "$1")
|
|
.replace(/\+''/g, "")
|
|
|
|
const args = Array.isArray(c.argName) ? properties(c.argName) : c.argName
|
|
|
|
if (Object.keys(needEncoders).length === 0) {
|
|
return try_(() => new Function(args, str))
|
|
}
|
|
checkEncoders(c, needEncoders)
|
|
str = `return function(${args}){${str}};`
|
|
return try_(() =>
|
|
c.selfContained
|
|
? new Function((str = addEncoders(c, needEncoders) + str))()
|
|
: new Function(c.encodersPrefix, str)(c.encoders)
|
|
)
|
|
|
|
function try_(f) {
|
|
try {
|
|
return f()
|
|
} catch (e) {
|
|
console.log("Could not create a template function: " + str)
|
|
throw e
|
|
}
|
|
}
|
|
}
|
|
|
|
function compile(tmpl, def) {
|
|
return template(tmpl, null, def)
|
|
}
|
|
|
|
function sameDelimiters({start, end}) {
|
|
const d = doT.templateSettings.delimiters
|
|
return d.start === start && d.end === end
|
|
}
|
|
|
|
function setDelimiters(delimiters) {
|
|
if (sameDelimiters(delimiters)) {
|
|
console.log("delimiters did not change")
|
|
return
|
|
}
|
|
currentSyntax = getSyntax(delimiters)
|
|
doT.templateSettings.delimiters = delimiters
|
|
}
|
|
|
|
function getSyntax({start, end}) {
|
|
start = escape(start)
|
|
end = escape(end)
|
|
const syntax = {}
|
|
for (const syn in defaultSyntax) {
|
|
const s = defaultSyntax[syn]
|
|
.toString()
|
|
.replace(/\\\{\\\{/g, start)
|
|
.replace(/\\\}\\\}/g, end)
|
|
syntax[syn] = strToRegExp(s)
|
|
}
|
|
return syntax
|
|
}
|
|
|
|
const escapeCharacters = /([{}[\]()<>\\\/^$\-.+*?!=|&:])/g
|
|
|
|
function escape(str) {
|
|
return str.replace(escapeCharacters, "\\$1")
|
|
}
|
|
|
|
const regexpPattern = /^\/(.*)\/([\w]*)$/
|
|
|
|
function strToRegExp(str) {
|
|
const [, rx, flags] = str.match(regexpPattern)
|
|
return new RegExp(rx, flags)
|
|
}
|
|
|
|
function properties(args) {
|
|
return args.reduce((s, a, i) => s + (i ? "," : "") + a, "{") + "}"
|
|
}
|
|
|
|
function checkEncoders(c, encoders) {
|
|
const typ = encoderType[c.selfContained]
|
|
for (const enc in encoders) {
|
|
const e = c.encoders[enc]
|
|
if (!e) throw new Error(`unknown encoder "${enc}"`)
|
|
if (typeof e !== typ)
|
|
throw new Error(`selfContained ${c.selfContained}: encoder type must be "${typ}"`)
|
|
}
|
|
}
|
|
|
|
function addEncoders(c, encoders) {
|
|
let s = ""
|
|
for (const enc in encoders) s += `const ${c.encodersPrefix}${enc}=${c.encoders[enc]};`
|
|
return s
|
|
}
|