import { isPrimitive, isNull, each } from "../safe"; import { MapOf } from "../interfaces"; type SubstFn = (name: string, format?: string) => string; type TemplateFn = (subst: SubstFn) => string; type ConvertFn = (value: any, format?: string) => string; const map = { "\\{": "&curlopen;", "\\}": "&curlclose;", "&": "&", "\\:": ":" }; const rev = { curlopen: "{", curlclose: "}", amp: "&", colon: ":" }; function espaceString(s: string) { if (!s) return s; return "'" + s.replace(/('|\\)/g, "\\$1").replace("\n", "\\n") + "'"; } function encode(s: string) { if (!s) return s; return s.replace(/\\{|\\}|&|\\:|\n/g, m => map[m] || m); } function decode(s: string) { if (!s) return s; return s.replace(/&(\w+);/g, (m, $1) => rev[$1] || m); } function subst(s: string) { const i = s.indexOf(":"); let name: string; let pattern: string; if (i >= 0) { name = s.substr(0, i); pattern = s.substr(i + 1); } else { name = s; } if (pattern) return [ espaceString(decode(name)), espaceString(decode(pattern))]; else return [espaceString(decode(name))]; } function _compile(str: string) { if (!str) return () => void 0; const chunks = encode(str).split("{"); let chunk: string; const code = ["var result=[];"]; for (let i = 0; i < chunks.length; i++) { chunk = chunks[i]; if (i === 0) { if (chunk) code.push("result.push(" + espaceString(decode(chunk)) + ");"); } else { const len = chunk.indexOf("}"); if (len < 0) throw new Error("Unbalanced substitution #" + i); code.push("result.push(subst(" + subst(chunk.substr(0, len)).join(",") + "));"); if (chunk.length > len + 1) code.push("result.push(" + espaceString(decode(chunk.substr(len + 1))) + ");"); } } code.push("return result.join('');"); // the code for this function is generated from the template // tslint:disable-next-line:function-constructor return new Function("subst", code.join("\n")) as TemplateFn; } const cache: MapOf = {}; export function compile(template: string) { let compiled = cache[template]; if (!compiled) { compiled = _compile(template); cache[template] = compiled; } return compiled; } function defaultConverter(value: any, pattern: string) { if (pattern && pattern.toLocaleLowerCase() === "json") { const seen = []; return JSON.stringify(value, (k, v) => { if (!isPrimitive(v)) { const id = seen.indexOf(v); if (id >= 0) return "@ref-" + id; else { seen.push(v); return v; } } else { return v; } }, 2); } else if (isNull(value)) { return ""; } else if (value instanceof Date) { return value.toISOString(); } else { return pattern ? value.toString(pattern) : value.toString(); } } export class Formatter { _converters: ConvertFn[]; constructor(converters?: ConvertFn[]) { this._converters = converters || []; this._converters.push(defaultConverter); } convert(value: any, pattern: string) { for (const c of this._converters) { const res = c(value, pattern); if (!isNull(res)) return res; } return ""; } format(msg: string, ...args: any[]) { const template = compile(msg); return template((name, pattern) => { const value = args[name]; return !isNull(value) ? this.convert(value, pattern) : ""; }); } compile(msg: string) { const template = compile(msg); return (...args: any[]) => { return template((name, pattern) => { const value = args[name]; return !isNull(value) ? this.convert(value, pattern) : ""; }); }; } } const _default = new Formatter(); export function format(msg: string, ...args: any[]) { return _default.format(msg, ...args); }