'use strict'; const Assert = require('@hapi/hoek/lib/assert'); const Clone = require('@hapi/hoek/lib/clone'); const EscapeHtml = require('@hapi/hoek/lib/escapeHtml'); const Formula = require('@sideway/formula'); const Common = require('./common'); const Errors = require('./errors'); const Ref = require('./ref'); const internals = { symbol: Symbol('template'), opens: new Array(1000).join('\u0000'), closes: new Array(1000).join('\u0001'), dateFormat: { date: Date.prototype.toDateString, iso: Date.prototype.toISOString, string: Date.prototype.toString, time: Date.prototype.toTimeString, utc: Date.prototype.toUTCString } }; module.exports = exports = internals.Template = class { constructor(source, options) { Assert(typeof source === 'string', 'Template source must be a string'); Assert(!source.includes('\u0000') && !source.includes('\u0001'), 'Template source cannot contain reserved control characters'); this.source = source; this.rendered = source; this._template = null; this._settings = Clone(options); this._parse(); } _parse() { // 'text {raw} {{ref}} \\{{ignore}} {{ignore\\}} {{ignore {{ignore}' if (!this.source.includes('{')) { return; } // Encode escaped \\{{{{{ const encoded = internals.encode(this.source); // Split on first { in each set const parts = internals.split(encoded); // Process parts let refs = false; const processed = []; const head = parts.shift(); if (head) { processed.push(head); } for (const part of parts) { const raw = part[0] !== '{'; const ender = raw ? '}' : '}}'; const end = part.indexOf(ender); if (end === -1 || // Ignore non-matching closing part[1] === '{') { // Ignore more than two { processed.push(`{${internals.decode(part)}`); continue; } let variable = part.slice(raw ? 0 : 1, end); const wrapped = variable[0] === ':'; if (wrapped) { variable = variable.slice(1); } const dynamic = this._ref(internals.decode(variable), { raw, wrapped }); processed.push(dynamic); if (typeof dynamic !== 'string') { refs = true; } const rest = part.slice(end + ender.length); if (rest) { processed.push(internals.decode(rest)); } } if (!refs) { this.rendered = processed.join(''); return; } this._template = processed; } static date(date, prefs) { return internals.dateFormat[prefs.dateFormat].call(date); } describe(options = {}) { if (!this._settings && options.compact) { return this.source; } const desc = { template: this.source }; if (this._settings) { desc.options = this._settings; } return desc; } static build(desc) { return new internals.Template(desc.template, desc.options); } isDynamic() { return !!this._template; } static isTemplate(template) { return template ? !!template[Common.symbols.template] : false; } refs() { if (!this._template) { return; } const refs = []; for (const part of this._template) { if (typeof part !== 'string') { refs.push(...part.refs); } } return refs; } resolve(value, state, prefs, local) { if (this._template && this._template.length === 1) { return this._part(this._template[0], /* context -> [*/ value, state, prefs, local, {} /*] */); } return this.render(value, state, prefs, local); } _part(part, ...args) { if (part.ref) { return part.ref.resolve(...args); } return part.formula.evaluate(args); } render(value, state, prefs, local, options = {}) { if (!this.isDynamic()) { return this.rendered; } const parts = []; for (const part of this._template) { if (typeof part === 'string') { parts.push(part); } else { const rendered = this._part(part, /* context -> [*/ value, state, prefs, local, options /*] */); const string = internals.stringify(rendered, value, state, prefs, local, options); if (string !== undefined) { const result = part.raw || (options.errors && options.errors.escapeHtml) === false ? string : EscapeHtml(string); parts.push(internals.wrap(result, part.wrapped && prefs.errors.wrap.label)); } } } return parts.join(''); } _ref(content, { raw, wrapped }) { const refs = []; const reference = (variable) => { const ref = Ref.create(variable, this._settings); refs.push(ref); return (context) => { const resolved = ref.resolve(...context); return resolved !== undefined ? resolved : null; }; }; try { var formula = new Formula.Parser(content, { reference, functions: internals.functions, constants: internals.constants }); } catch (err) { err.message = `Invalid template variable "${content}" fails due to: ${err.message}`; throw err; } if (formula.single) { if (formula.single.type === 'reference') { const ref = refs[0]; return { ref, raw, refs, wrapped: wrapped || ref.type === 'local' && ref.key === 'label' }; } return internals.stringify(formula.single.value); } return { formula, raw, refs }; } toString() { return this.source; } }; internals.Template.prototype[Common.symbols.template] = true; internals.Template.prototype.isImmutable = true; // Prevents Hoek from deep cloning schema objects internals.encode = function (string) { return string .replace(/\\(\{+)/g, ($0, $1) => { return internals.opens.slice(0, $1.length); }) .replace(/\\(\}+)/g, ($0, $1) => { return internals.closes.slice(0, $1.length); }); }; internals.decode = function (string) { return string .replace(/\u0000/g, '{') .replace(/\u0001/g, '}'); }; internals.split = function (string) { const parts = []; let current = ''; for (let i = 0; i < string.length; ++i) { const char = string[i]; if (char === '{') { let next = ''; while (i + 1 < string.length && string[i + 1] === '{') { next += '{'; ++i; } parts.push(current); current = next; } else { current += char; } } parts.push(current); return parts; }; internals.wrap = function (value, ends) { if (!ends) { return value; } if (ends.length === 1) { return `${ends}${value}${ends}`; } return `${ends[0]}${value}${ends[1]}`; }; internals.stringify = function (value, original, state, prefs, local, options = {}) { const type = typeof value; const wrap = prefs && prefs.errors && prefs.errors.wrap || {}; let skipWrap = false; if (Ref.isRef(value) && value.render) { skipWrap = value.in; value = value.resolve(original, state, prefs, local, { in: value.in, ...options }); } if (value === null) { return 'null'; } if (type === 'string') { return internals.wrap(value, options.arrayItems && wrap.string); } if (type === 'number' || type === 'function' || type === 'symbol') { return value.toString(); } if (type !== 'object') { return JSON.stringify(value); } if (value instanceof Date) { return internals.Template.date(value, prefs); } if (value instanceof Map) { const pairs = []; for (const [key, sym] of value.entries()) { pairs.push(`${key.toString()} -> ${sym.toString()}`); } value = pairs; } if (!Array.isArray(value)) { return value.toString(); } const values = []; for (const item of value) { values.push(internals.stringify(item, original, state, prefs, local, { arrayItems: true, ...options })); } return internals.wrap(values.join(', '), !skipWrap && wrap.array); }; internals.constants = { true: true, false: false, null: null, second: 1000, minute: 60 * 1000, hour: 60 * 60 * 1000, day: 24 * 60 * 60 * 1000 }; internals.functions = { if(condition, then, otherwise) { return condition ? then : otherwise; }, length(item) { if (typeof item === 'string') { return item.length; } if (!item || typeof item !== 'object') { return null; } if (Array.isArray(item)) { return item.length; } return Object.keys(item).length; }, msg(code) { const [value, state, prefs, local, options] = this; const messages = options.messages; if (!messages) { return ''; } const template = Errors.template(value, messages[0], code, state, prefs) || Errors.template(value, messages[1], code, state, prefs); if (!template) { return ''; } return template.render(value, state, prefs, local, options); }, number(value) { if (typeof value === 'number') { return value; } if (typeof value === 'string') { return parseFloat(value); } if (typeof value === 'boolean') { return value ? 1 : 0; } if (value instanceof Date) { return value.getTime(); } return null; } };