464 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			464 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
'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;
 | 
						|
 | 
						|
        if (options) {
 | 
						|
            const { functions, ...opts } = options;
 | 
						|
            this._settings = Object.keys(opts).length ? Clone(opts) : undefined;
 | 
						|
            this._functions = functions;
 | 
						|
            if (this._functions) {
 | 
						|
                Assert(Object.keys(this._functions).every((key) => typeof key === 'string'), 'Functions keys must be strings');
 | 
						|
                Assert(Object.values(this._functions).every((key) => typeof key === 'function'), 'Functions values must be functions');
 | 
						|
            }
 | 
						|
        }
 | 
						|
        else {
 | 
						|
            this._settings = undefined;
 | 
						|
            this._functions = undefined;
 | 
						|
        }
 | 
						|
 | 
						|
        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;
 | 
						|
        }
 | 
						|
 | 
						|
        if (this._functions) {
 | 
						|
            desc.functions = this._functions;
 | 
						|
        }
 | 
						|
 | 
						|
        return desc;
 | 
						|
    }
 | 
						|
 | 
						|
    static build(desc) {
 | 
						|
 | 
						|
        return new internals.Template(desc.template, desc.options || desc.functions ? { ...desc.options, functions: desc.functions } : undefined);
 | 
						|
    }
 | 
						|
 | 
						|
    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 {
 | 
						|
            const functions = this._functions ? { ...internals.functions, ...this._functions } : internals.functions;
 | 
						|
            var formula = new Formula.Parser(content, { reference, 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;
 | 
						|
    }
 | 
						|
};
 |