shawnholman/Aych

View on GitHub
src/core/TemplateParser.ts

Summary

Maintainability
A
1 hr
Test Coverage
import {SimpleObject} from "../interfaces";
import {Piper} from "./Piper";

// TODO: Support literals instead of always using tempalte.
// TODO: Support multiple pipes
export class TemplateParser {
    private static readonly tags = {
        start: '{{',
        end: '}}',
        pipe: '|',
    };

    private static readonly bufferedTags = {
        start: TemplateParser.tags.start + '\\s*',
        end: '\\s*' + TemplateParser.tags.end,
        pipe: '\\s*\\' + TemplateParser.tags.pipe + '\\s*',
    };

    private static readonly FULL_IDENTIFIER =
        '(([\\w-_][\\w\\d-_]*(\\[-?\\d+\\])?\\??)(\\.[\\w-_][\\w\\d-_]*(\\[-?\\d+\\])?\\??)*)';

    private static readonly FILTER =
        '(' + (TemplateParser.bufferedTags.pipe) + '(([\\w-_<>=!][\\w\\d-_<>=!]*)(\\((([\\w\\d ]+)(,\\s*[\\w\\d ]+)*)?\\))?))?';

    private static readonly TEMPLATE_TAG_STRING =
        TemplateParser.bufferedTags.start + TemplateParser.FULL_IDENTIFIER + TemplateParser.FILTER + TemplateParser.bufferedTags.end;

    private static readonly TEMPLATE_TAG = new RegExp(TemplateParser.TEMPLATE_TAG_STRING, 'g');

    private static readonly EXACT_TEMPLATE_TAG = new RegExp('^' + TemplateParser.TEMPLATE_TAG_STRING + '$');

    /** Indexes used to identify key locations in the resulting RegExp match */
    private static readonly templateIndex = {
        // Matches the name of the object key we are looking for.
        key: 1,
        // Matches the pipe function name
        pipeFuncName: 8,
        // Matches the list of pipe params
        pipeParams: 10,
    }

    /**
     * Templates a string with given data.
     * @param toTemplate The string to template.
     * @param templates The data to use.
     */
    public static template(toTemplate: string, templates: SimpleObject): string {
        if (!TemplateParser.probablyHasTemplates(toTemplate)) {
            return toTemplate;
        }

        return toTemplate.replace(TemplateParser.TEMPLATE_TAG, (...groups) => {
            const key = groups[TemplateParser.templateIndex.key];
            const pipeFunctionName = groups[TemplateParser.templateIndex.pipeFuncName];
            const pipeParameters = groups[TemplateParser.templateIndex.pipeParams];
            const value = TemplateParser.getValueFromObject(templates, key);

            return Piper.pipe(value, pipeFunctionName, pipeParameters);
        });
    }

    /**
     * Evaluates a given expression to true or false.
     * @param toTemplate The string to template.
     * @param templates The data to use.
     */
    public static evaluate(toTemplate: string, templates: SimpleObject): boolean {
        toTemplate = toTemplate.trim();
        if (!toTemplate.match(TemplateParser.EXACT_TEMPLATE_TAG)) {
            return false;
        }
        const result = TemplateParser.template(toTemplate, templates);

        return result === 'true' || result === '1';
    }

    /**
     * A quick check to see if a string has templates. This check does not
     * validate that the potential template is usable.
     * @param string The string to check if it has templates.
     */
    private static probablyHasTemplates(toTemplate: string): boolean {
        return toTemplate.includes(TemplateParser.tags.start) && toTemplate.includes(TemplateParser.tags.end);
    }

    /**
     * Gets the value of a key from an object.
     * @param object The object to search.
     * @param key A string representing the key of the value in the object that you are looking for. These follow
     * standard javascript syntax. For example:
     *     getFromObject({ array: [1, 2, 3, 4] }, 'array[2]') ===> 2
     *     getFromObject({ array: [{}, { key: 'value' }]}, 'array[1].key') ===> 'value'
     */
    private static getValueFromObject(object: SimpleObject, key: string): string {
        const keys = key.split(".").map(this.parseKey);
        let track: any = object;
        let prevKey: string | undefined = undefined;

        for (let key of keys) {
            // Key does not exist on track
            if (!Object.prototype.hasOwnProperty.call(track, key.value)) {
                if (key.isOptional) { // Does not matter if optional
                    return '';
                }
                if (prevKey === undefined) {
                    throw new Error(key.value + " is undefined.");
                }
                throw new Error(key.value + ` is not a property of ${prevKey}.`);
            }

            if (key.isArray) {
                const index = key.array!.index;
                const child = track[key.value];

                if (!Array.isArray(child)) {
                    throw new Error(key.value + ' is not an array.');
                }

                if (index < 0 || index >= child.length) {
                    if (key.isOptional) {
                        return '';
                    }
                    throw new Error(`Index out of bounds: ${key.value}[${index}].`);
                }
                track = child[index];
                prevKey = key.value;
                continue;
            }

            track = track[key.value];
            prevKey = key.value;
        }

        if (track === null || track === undefined) {
            return '';
        }

        return track.toString();
    }

    /**
     * Converts a key into a token for template parsing
     * @param key Key to convert
     * @return
     * @private
     */
    private static parseKey(key: string) {
        key = key.trim();
        let isOptional = false;

        if (key.endsWith("?")) {
            isOptional = true;
            // remove the optional sign
            key = key.slice(0, -1);
        }

        // Checks to see if the key is an array. It should match: key[1] for example
        // the match would return ['key[1]', 'key', '1']
        const keyIsArray = key.match(/^([\w-_][\w\d-_]*)\[([-]?\d+)]$/);
        let index = -1;
        if (keyIsArray) {
            key = keyIsArray[1];
            index = parseInt(keyIsArray[2]);
        }

        return {
            value: key,
            isOptional: isOptional,
            isArray: !!keyIsArray,
            array: keyIsArray ? {
                index: index
            } : null,
        }
    }
}