mirror of
https://github.com/LalleSX/4chan-XZ.git
synced 2025-10-07 07:22:37 +02:00
272 lines
7.9 KiB
JavaScript
272 lines
7.9 KiB
JavaScript
/* jshint evil: true */
|
|
var fs = require('fs')
|
|
var path = require('path')
|
|
var _template = require('lodash.template')
|
|
var esprima = require('esprima')
|
|
|
|
// disable ES6 delimiters
|
|
var _templateSettings = { interpolate: /<%=([\s\S]+?)%>/g }
|
|
|
|
// Functions used in templates.
|
|
var tools = {}
|
|
|
|
var read = (tools.read = filename =>
|
|
fs.readFileSync(filename, 'utf8').replace(/\r\n/g, '\n'))
|
|
var readJSON = (tools.readJSON = filename => JSON.parse(read(filename)))
|
|
tools.readBase64 = filename => fs.readFileSync(filename).toString('base64')
|
|
|
|
tools.readHTML = function (filename) {
|
|
var text = read(filename).replace(/^ +/gm, '').replace(/\r?\n/g, '')
|
|
text = _template(text, _templateSettings)(pkg) // package.json data only; no recursive imports
|
|
return tools.html(text)
|
|
}
|
|
|
|
tools.multiline = function (text) {
|
|
return text
|
|
.replace(/\n+/g, '\n')
|
|
.split(/^/m)
|
|
.map(JSON.stringify)
|
|
.join('+')
|
|
.replace(/"\+"/g, '\\\n')
|
|
}
|
|
|
|
// Convert JSONify-able object to Javascript expression.
|
|
var constExpression = data => JSON.stringify(data).replace(/`/g, '\\`')
|
|
|
|
function TextStream(text) {
|
|
this.text = text
|
|
}
|
|
|
|
TextStream.prototype.eat = function (regexp) {
|
|
var match = regexp.exec(this.text)
|
|
if (match && match.index === 0) {
|
|
this.text = this.text.slice(match[0].length)
|
|
}
|
|
return match
|
|
}
|
|
|
|
function parseHTMLTemplate(stream, context) {
|
|
var template = stream.text // text from beginning, for error messages
|
|
var expression = new HTMLExpression(context)
|
|
var match
|
|
|
|
try {
|
|
while (stream.text) {
|
|
// Literal HTML
|
|
if (
|
|
(match = stream.eat(
|
|
// characters not indicating start or end of placeholder, using backslash as escape
|
|
/^(?:[^\\{}]|\\.)+(?!{)/
|
|
))
|
|
) {
|
|
var unescaped = match[0].replace(/\\(.)/g, '$1')
|
|
expression.addLiteral(unescaped)
|
|
|
|
// Placeholder
|
|
} else if (
|
|
(match = stream.eat(
|
|
// symbol identifying placeholder type and first argument (enclosed by {})
|
|
// backtick not allowed in arguments as it can end embedded JS in Coffeescript
|
|
/^([^}]){([^}`]*)}/
|
|
))
|
|
) {
|
|
var type = match[1]
|
|
var args = [match[2]]
|
|
if (type === '?') {
|
|
// conditional expression can take up to two subtemplate arguments
|
|
for (var i = 0; i < 2 && stream.eat(/^{/); i++) {
|
|
var subtemplate = parseHTMLTemplate(stream, context)
|
|
args.push(subtemplate)
|
|
if (!stream.eat(/^}/)) {
|
|
throw new Error(
|
|
`Unexpected characters in subtemplate (${stream.text})`
|
|
)
|
|
}
|
|
}
|
|
}
|
|
expression.addPlaceholder(new Placeholder(type, args))
|
|
|
|
// No match: end of subtemplate (} next) or error
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
return expression.build()
|
|
} catch (err) {
|
|
throw new Error(`${err.message}: ${template}`)
|
|
}
|
|
}
|
|
|
|
function HTMLExpression(context) {
|
|
this.parts = []
|
|
this.startContext = this.endContext = context || ''
|
|
}
|
|
|
|
HTMLExpression.prototype.addLiteral = function (text) {
|
|
this.parts.push(constExpression(text))
|
|
this.endContext = this.endContext
|
|
.replace(/(=['"])[^'"<>]*/g, '$1') // remove values from quoted attributes (no '"<> allowed)
|
|
.replace(/(<\w+)( [\w-]+((?=[ >])|=''|=""))*/g, '$1') // remove attributes from tags
|
|
.replace(/^([^'"<>]+|<\/?\w+>)*/, '') // remove text (no '"<> allowed) and tags
|
|
}
|
|
|
|
HTMLExpression.prototype.addPlaceholder = function (placeholder) {
|
|
if (!placeholder.allowed(this.endContext)) {
|
|
throw new Error(
|
|
`Illegal insertion of placeholder (type ${placeholder.type}) into HTML template (at ${this.endContext})`
|
|
)
|
|
}
|
|
this.parts.push(placeholder.build())
|
|
}
|
|
|
|
HTMLExpression.prototype.build = function () {
|
|
if (this.startContext !== this.endContext) {
|
|
throw new Error(`HTML template is ill-formed (at ${this.endContext})`)
|
|
}
|
|
return this.parts.length === 0 ? '""' : this.parts.join(' + ')
|
|
}
|
|
|
|
function Placeholder(type, args) {
|
|
this.type = type
|
|
this.args = args
|
|
}
|
|
|
|
Placeholder.prototype.allowed = function (context) {
|
|
switch (this.type) {
|
|
case '$':
|
|
// escaped text allowed outside tags or in quoted attributes
|
|
return context === '' || /\=['"]$/.test(context)
|
|
case '&':
|
|
case '@':
|
|
// contents of one/many HTML element or template allowed outside tags only
|
|
return context === ''
|
|
case '?':
|
|
// conditionals allowed anywhere so long as their contents don't change context (checked by HTMLExpression.prototype.build)
|
|
return true
|
|
}
|
|
throw new Error(`Unrecognized placeholder type (${this.type})`)
|
|
}
|
|
|
|
Placeholder.prototype.build = function () {
|
|
// first argument is always JS expression; validate it so we don't accidentally break out of placeholders
|
|
var expr = this.args[0]
|
|
var ast
|
|
try {
|
|
ast = esprima.parse(expr)
|
|
} catch (err) {
|
|
throw new Error(`Invalid JavaScript in template (${expr})`)
|
|
}
|
|
if (
|
|
!(
|
|
ast.type === 'Program' &&
|
|
ast.body.length == 1 &&
|
|
ast.body[0].type === 'ExpressionStatement'
|
|
)
|
|
) {
|
|
throw new Error(`JavaScript in template is not an expression (${expr})`)
|
|
}
|
|
switch (this.type) {
|
|
case '$':
|
|
return `E(${expr})` // $ : escaped text
|
|
case '&':
|
|
return `(${expr}).innerHTML` // & : contents of HTML element or template (of form {innerHTML: "safeHTML"})
|
|
case '@':
|
|
return `E.cat(${expr})` // @ : contents of array of HTML elements or templates (see src/General/Globals.coffee for E.cat)
|
|
case '?':
|
|
return `((${expr}) ? ${this.args[1] || '""'} : ${this.args[2] || '""'})` // ? : conditional expression
|
|
}
|
|
throw new Error(`Unrecognized placeholder type (${this.type})`)
|
|
}
|
|
|
|
// HTML template generator with placeholders of forms ${}, &{}, @{}, and ?{}{}{} (see Placeholder.prototype.build)
|
|
// that checks safety of generated expressions at compile time.
|
|
tools.html = function (template) {
|
|
var stream = new TextStream(template)
|
|
var output = parseHTMLTemplate(stream)
|
|
if (stream.text) {
|
|
throw new Error(
|
|
`Unexpected characters in template (${stream.text}): ${template}`
|
|
)
|
|
}
|
|
return `{innerHTML: ${output}}`
|
|
}
|
|
|
|
function includesDir(templateName) {
|
|
var dir = path.dirname(templateName)
|
|
var subdir = path.basename(templateName).replace(/\.[^.]+$/, '')
|
|
if (fs.readdirSync(dir).indexOf(subdir) >= 0) {
|
|
return path.join(dir, subdir)
|
|
} else {
|
|
return dir
|
|
}
|
|
}
|
|
|
|
function resolvePath(includeName, templateName) {
|
|
var dir
|
|
if (includeName[0] === '/') {
|
|
dir = process.cwd()
|
|
} else {
|
|
dir = includesDir(templateName)
|
|
}
|
|
return path.join(dir, includeName)
|
|
}
|
|
|
|
function wrapTool(tool, templateName) {
|
|
return function (includeName) {
|
|
return tool(resolvePath(includeName, templateName))
|
|
}
|
|
}
|
|
|
|
function loadModules(templateName) {
|
|
var dir = includesDir(templateName)
|
|
var moduleNames = fs.readdirSync(dir).filter(f => /\.inc$/.test(f))
|
|
var modules = {}
|
|
for (var name of moduleNames) {
|
|
var code = read(path.join(dir, name))
|
|
modules[name.replace(/\.inc$/, '')] = new Function(code)()
|
|
}
|
|
return modules
|
|
}
|
|
|
|
// Import variables from package.json.
|
|
var pkg = readJSON('package.json')
|
|
|
|
function interpolate(text, data, filename) {
|
|
var context = {},
|
|
key
|
|
for (key in tools) {
|
|
context[key] = /^read/.test(key)
|
|
? wrapTool(tools[key], filename)
|
|
: tools[key]
|
|
}
|
|
for (key in pkg) {
|
|
context[key] = pkg[key]
|
|
}
|
|
if (data) {
|
|
for (key in data) {
|
|
context[key] = data[key]
|
|
}
|
|
}
|
|
context.files = fs.readdirSync(includesDir(filename))
|
|
context.require = loadModules(filename)
|
|
return _template(text, _templateSettings)(context)
|
|
}
|
|
|
|
module.exports = interpolate
|
|
|
|
if (require.main === module) {
|
|
;(function () {
|
|
// Take variables from command line.
|
|
var data = {}
|
|
for (var i = 4; i < process.argv.length; i++) {
|
|
var m = process.argv[i].match(/(.*?)=(.*)/)
|
|
data[m[1]] = m[2]
|
|
}
|
|
|
|
var text = read(process.argv[2])
|
|
text = interpolate(text, data, process.argv[2])
|
|
fs.writeFileSync(process.argv[3], text)
|
|
})()
|
|
}
|