<commit_before>'use strict';

const fs = require('fs');
const path = require('path');
const url = require('url');
const util = require('util');

const fetch = require('node-fetch');
const yaml = require('js-yaml');

const jptr = require('reftools/lib/jptr.js').jptr;
const recurse = require('reftools/lib/recurse.js').recurse;
const clone = require('reftools/lib/clone.js').clone;
const isRef = require('reftools/lib/isref.js').isRef;
const common = require('oas-kit-common');

function unique(arr) {
    return [... new Set(arr)];
}

function readFileAsync(filename, encoding) {
    return new Promise(function (resolve, reject) {
        fs.readFile(filename, encoding, function (err, data) {
            if (err)
                reject(err);
            else
                resolve(data);
        });
    });
}

function resolveAllInternal(obj, context, src, parentPath, base, options) {

    let attachPoint = options.externalRefs[src+parentPath].paths[0];

    let baseUrl = url.parse(base);
    let seen = {}; // seen is indexed by the $ref value and contains path replacements
    let changes = 1;
    while (changes) {
        changes = 0;
        recurse(obj, {identityDetection:true}, function (obj, key, state) {
            if (isRef(obj, key)) {
                if (obj[key].startsWith('#')) {
                    if (!seen[obj[key]] && !obj.$fixed) {
                        let target = clone(jptr(context, obj[key]));
                        if (options.verbose>1) console.log((target === false ? common.colour.red : common.colour.green)+'Internal resolution', obj[key], common.colour.normal);
                        /*
                            ResolutionCase:A is where there is a local reference in an externally
                            referenced document, and we have not seen it before. The reference
                            is replaced by a copy of the data pointed to, which may be outside this fragment
                            but within the context of the external document
                        */
                        if (target === false) {
                            state.parent[state.pkey] = {}; /* case:A(2) where the resolution fails */
                        }
                        else {
                            changes++;
                            state.parent[state.pkey] = target;
                            seen[obj[key]] = state.path.replace('/%24ref','');
                        }
                    }
                    else {
                        if (!obj.$fixed) {
                            let newRef = (attachPoint+'/'+seen[obj[key]]).split('/#/').join('/');
                            state.parent[state.pkey] = { $ref: newRef, 'x-miro': obj[key], $fixed: true };
                            if (options.verbose>1) console.log('Replacing with',newRef);
                            changes++;
                        }
                        /*
                            ResolutionCase:B is where there is a local reference in an externally
                            referenced document, and we have seen this reference before and resolved it.
                            We create a new object containing the (immutable) $ref string
                        */
                    }
                }
                else if (baseUrl.protocol) {
                    let newRef = url.resolve(base,obj[key]).toString();
                    if (options.verbose>1) console.log(common.colour.yellow+'Rewriting external url ref',obj[key],'as',newRef,common.colour.normal);
                    obj['x-miro'] = obj[key];
                    obj[key] = newRef;
                }
                else if (!obj['x-miro']) {
                    let newRef = url.resolve(base,obj[key]).toString();
                    if (options.verbose>1) console.log(common.colour.yellow+'Rewriting external ref',obj[key],'as',newRef,common.colour.normal);
                    obj['x-miro'] = obj[key]; // we use x-miro as a flag so we don't do this > once
                    obj[key] = newRef;
                }
            }
        });
    }

    recurse(obj,{},function(obj,key,state){
        if (isRef(obj, key)) {
            if (obj.$fixed) delete obj.$fixed;
        }
    });

    if (options.verbose>1) console.log('Finished internal resolution');
    return obj;
}

function filterData(data, options) {
    if (!options.filters || !options.filters.length) return data;
    for (let filter of options.filters) {
        data = filter(data, options);
    }
    return data;
}

function resolveExternal(root, pointer, options, callback) {
    var u = url.parse(options.source);
    var base = options.source.split('\\').join('/').split('/');
    let doc = base.pop(); // drop the actual filename
    if (!doc) base.pop(); // in case it ended with a /
    let fragment = '';
    let fnComponents = pointer.split('#');
    if (fnComponents.length > 1) {
        fragment = '#' + fnComponents[1];
        pointer = fnComponents[0];
    }
    base = base.join('/');

    let u2 = url.parse(pointer);
    let effectiveProtocol = (u2.protocol ? u2.protocol : (u.protocol ? u.protocol : 'file:'));

    let target = url.resolve(base ? base + '/' : '', pointer)

    if (options.cache[target]) {
        if (options.verbose) console.log('CACHED', target, fragment);
        /*
            resolutionSource:A this is where we have cached the externally-referenced document from a
            file, http or custom handler
        */
        let context = clone(options.cache[target]);
        let data = context;
        if (fragment) {
            data = jptr(data, fragment);
            if (data === false) data = {}; // case:A(2) where the resolution fails
        }
        data = resolveAllInternal(data, context, pointer, fragment, target, options);
        data = filterData(data, options);
        callback(clone(data), target, options);
        return Promise.resolve(data);
    }

    if (options.verbose) console.log('GET', target, fragment);

    if (options.handlers && options.handlers[effectiveProtocol]) {
        return options.handlers[effectiveProtocol](base, pointer, fragment, options)
            .then(function (data) {
                data = filterData(data, options);
                options.cache[target] = data;
                callback(data, target, options);
                return data;
            })
            .catch(function(ex){
                if (options.verbose) console.warn(ex);
            });
    }
    else if (effectiveProtocol.startsWith('http')) {
        return fetch(target, { agent: options.agent })
            .then(function (res) {
                if (res.status !== 200) throw new Error(`Received status code ${res.status}`);
                return res.text();
            })
            .then(function (data) {
                try {
                    let context = yaml.safeLoad(data, { json: true });
                    data = context;
                    options.cache[target] = clone(data);
                    /* resolutionSource:B, from the network, data is fresh, but we clone it into the cache */
                    if (fragment) {
                        data = jptr(data, fragment);
                        if (data === false) data = {}; /* case:B(2) where the resolution fails */
                    }
                    data = resolveAllInternal(data, context, pointer, fragment, target, options);
                    data = filterData(data, options);
                }
                catch (ex) {
                    if (options.verbose) console.warn(ex);
                }
                callback(data, target, options);
                return data;
            })
            .catch(function (err) {
                if (options.verbose) console.warn(err);
                options.cache[target] = {};
                if (options.promise && options.fatal) options.promise.reject(err);
            });
    }
    else {
        return readFileAsync(target, options.encoding || 'utf8')
            .then(function (data) {
                try {
                    let context = yaml.safeLoad(data, { json: true });
                    data = context;
                    /*
                        resolutionSource:C from a file, data is fresh but we clone it into the cache
                    */
                    options.cache[target] = clone(data);
                    if (fragment) {
                        data = jptr(data, fragment);
                        if (data === false) data = {}; /* case:C(2) where the resolution fails */
                    }
                    data = resolveAllInternal(data, context, pointer, fragment, target, options);
                    data = filterData(data, options);
                }
                catch (ex) {
                    if (options.verbose) console.warn(ex);
                }
                callback(data, target, options);
                return data;
            })
            .catch(function(err){
                if (options.verbose) console.warn(err);
                options.cache[target] = {};
                if (options.promise && options.fatal) options.promise.reject(err);
            });
    }
}

function scanExternalRefs(options) {
    return new Promise(function (res, rej) {
        let refs = options.externalRefs;

        if ((options.resolver.depth>0) && (options.source === options.resolver.base)) {
            // we only need to do any of this when called directly on pass #1
            return res(refs);
        }

        recurse(options.openapi, {identityDetection: true}, function (obj, key, state) {
            if (obj[key] && isRef(obj[key],'$ref')) {
                let $ref = obj[key].$ref;
                if (!$ref.startsWith('#')) {
                    if (!refs[$ref]) {
                        refs[$ref] = { resolved: false, paths: [], description: obj[key].description };
                    }
                    if (refs[$ref].resolved) {
                        if (options.rewriteRefs) {
                            // we've already seen it
                            let newRef = refs[$ref].resolvedAt;
                            if (options.verbose>1) console.log('Rewriting ref', $ref, newRef);
                            obj[key]['x-miro'] = $ref;
                            obj[key].$ref = newRef; // resolutionCase:C (new string)
                        }
                        else {
                            obj[key] = clone(refs[$ref].data); // resolutionCase:D (cloned:yes)
                        }
                    }
                    else {
                        refs[$ref].paths.push(state.path);
                    }
                }
            }
        });

        res(refs);
    });
}

function findExternalRefs(options) {
    return new Promise(function (res, rej) {

        scanExternalRefs(options)
            .then(function (refs) {
                for (let ref in refs) {

                    if (!refs[ref].resolved) {
                        let depth = options.resolver.depth;
                        if (depth>0) depth++;
                        options.resolver.actions[depth].push(function () {
                            return resolveExternal(options.openapi, ref, options, function (data, source, options) {
                                if (!refs[ref].resolved) {
                                    let external = {};
                                    external.context = refs[ref];
                                    external.$ref = ref;
                                    external.original = clone(data);
                                    external.updated = data;
                                    external.source = source;
                                    options.externals.push(external);
                                    refs[ref].resolved = true;
                                }

                                let localOptions = Object.assign({}, options, { source: '',
                                    resolver: {actions: options.resolver.actions,
                                    depth: options.resolver.actions.length-1, base: options.resolver.base } });
                                if (options.patch && refs[ref].description && !data.description &&
                                    (typeof data === 'object')) {
                                    data.description = refs[ref].description;
                                }
                                refs[ref].data = data;
                                let pointers = unique(refs[ref].paths).sort(function(a,b){
                                    if (a.length < b.length) return -1;
                                    if (a.length > b.length) return +1;
                                    return 0;
                                });
                                for (let ptr of pointers) {
                                    // shared x-ms-examples $refs confuse the fixupRefs heuristic in index.js
                                    if (refs[ref].resolvedAt && (ptr !== refs[ref].resolvedAt) && (ptr.indexOf('x-ms-examples/')<0)) {
                                        if (options.verbose>1) console.log('Creating pointer to data at', ptr);
                                        jptr(options.openapi, ptr, { $ref: refs[ref].resolvedAt, 'x-miro': ref }); // resolutionCase:E (new object)
                                    }
                                    else {
                                        if (refs[ref].resolvedAt) {
                                            if (options.verbose>1) console.log('Avoiding circular reference');
                                        }
                                        else {
                                            refs[ref].resolvedAt = ptr;
                                            if (options.verbose>1) console.log('Creating initial clone of data at', ptr);
                                        }
                                        let cdata = clone(data);
                                        jptr(options.openapi, ptr, cdata); // resolutionCase:F (cloned:yes)
                                    }
                                }
                                if (options.resolver.actions[localOptions.resolver.depth].length === 0) {
                                    //options.resolver.actions[localOptions.resolver.depth].push(function () { return scanExternalRefs(localOptions) });
                                    options.resolver.actions[localOptions.resolver.depth].push(function () { return findExternalRefs(localOptions) }); // findExternalRefs calls scanExternalRefs
                                }
                            });
                        });
                    }
                }
            })
            .catch(function(ex){
                if (options.verbose) console.warn(ex);
            });

        let result = {options:options};
        result.actions = options.resolver.actions[options.resolver.depth];
        res(result);
    });
}

const serial = funcs =>
    funcs.reduce((promise, func) =>
        promise.then(result => func().then(Array.prototype.concat.bind(result))), Promise.resolve([]));

function loopReferences(options, res, rej) {
    options.resolver.actions.push([]);
    findExternalRefs(options)
        .then(function (data) {
            serial(data.actions)
                .then(function () {
                    if (options.resolver.depth>=options.resolver.actions.length) {
                        console.warn('Ran off the end of resolver actions');
                        return res(true);
                    } else {
                        options.resolver.depth++;
                        if (options.resolver.actions[options.resolver.depth].length) {
                            setTimeout(function () {
                                loopReferences(data.options, res, rej);
                            }, 0);
                        }
                        else {
                            if (options.verbose>1) console.log(common.colour.yellow+'Finished resolution!',common.colour.normal);
                            res(options);
                        }
                    }
                })
                .catch(function (ex) {
                    if (options.verbose) console.warn(ex);
                });
        })
        .catch(function(ex){
            if (options.verbose) console.warn(ex);
        });
}

function setupOptions(options) {
    if (!options.cache) options.cache = {};

    options.cache[options.source] = { '$ref': '#/' };

    if (!options.externals) options.externals = [];
    if (!options.externalRefs) options.externalRefs = [];
    options.rewriteRefs = true;
    options.resolver = {};
    options.resolver.depth = 0;
    options.resolver.base = options.source;
    options.resolver.actions = [[]];
}

/** compatibility function for swagger2openapi */
function optionalResolve(options) {
    setupOptions(options);
    return new Promise(function (res, rej) {
        if (options.resolve)
            loopReferences(options, res, rej)
        else
            res(options);
    });
}

function resolve(openapi,source,options) {
    if (!options) options = {};
    options.openapi = openapi;
    options.source = source;
    options.resolve = true;
    setupOptions(options);
    return new Promise(function (res, rej) {
        loopReferences(options, res, rej)
    });
}

module.exports = {
    optionalResolve: optionalResolve,
    resolve: resolve
};

<commit_msg>resolver; missing resources are now fatal<commit_after>'use strict';

const fs = require('fs');
const path = require('path');
const url = require('url');
const util = require('util');

const fetch = require('node-fetch');
const yaml = require('js-yaml');

const jptr = require('reftools/lib/jptr.js').jptr;
const recurse = require('reftools/lib/recurse.js').recurse;
const clone = require('reftools/lib/clone.js').clone;
const isRef = require('reftools/lib/isref.js').isRef;
const common = require('oas-kit-common');

function unique(arr) {
    return [... new Set(arr)];
}

function readFileAsync(filename, encoding) {
    return new Promise(function (resolve, reject) {
        fs.readFile(filename, encoding, function (err, data) {
            if (err)
                reject(err);
            else
                resolve(data);
        });
    });
}

function resolveAllInternal(obj, context, src, parentPath, base, options) {

    let attachPoint = options.externalRefs[src+parentPath].paths[0];

    let baseUrl = url.parse(base);
    let seen = {}; // seen is indexed by the $ref value and contains path replacements
    let changes = 1;
    while (changes) {
        changes = 0;
        recurse(obj, {identityDetection:true}, function (obj, key, state) {
            if (isRef(obj, key)) {
                if (obj[key].startsWith('#')) {
                    if (!seen[obj[key]] && !obj.$fixed) {
                        let target = clone(jptr(context, obj[key]));
                        if (options.verbose>1) console.log((target === false ? common.colour.red : common.colour.green)+'Internal resolution', obj[key], common.colour.normal);
                        /*
                            ResolutionCase:A is where there is a local reference in an externally
                            referenced document, and we have not seen it before. The reference
                            is replaced by a copy of the data pointed to, which may be outside this fragment
                            but within the context of the external document
                        */
                        if (target === false) {
                            state.parent[state.pkey] = {}; /* case:A(2) where the resolution fails */
                        }
                        else {
                            changes++;
                            state.parent[state.pkey] = target;
                            seen[obj[key]] = state.path.replace('/%24ref','');
                        }
                    }
                    else {
                        if (!obj.$fixed) {
                            let newRef = (attachPoint+'/'+seen[obj[key]]).split('/#/').join('/');
                            state.parent[state.pkey] = { $ref: newRef, 'x-miro': obj[key], $fixed: true };
                            if (options.verbose>1) console.log('Replacing with',newRef);
                            changes++;
                        }
                        /*
                            ResolutionCase:B is where there is a local reference in an externally
                            referenced document, and we have seen this reference before and resolved it.
                            We create a new object containing the (immutable) $ref string
                        */
                    }
                }
                else if (baseUrl.protocol) {
                    let newRef = url.resolve(base,obj[key]).toString();
                    if (options.verbose>1) console.log(common.colour.yellow+'Rewriting external url ref',obj[key],'as',newRef,common.colour.normal);
                    obj['x-miro'] = obj[key];
                    obj[key] = newRef;
                }
                else if (!obj['x-miro']) {
                    let newRef = url.resolve(base,obj[key]).toString();
                    if (options.verbose>1) console.log(common.colour.yellow+'Rewriting external ref',obj[key],'as',newRef,common.colour.normal);
                    obj['x-miro'] = obj[key]; // we use x-miro as a flag so we don't do this > once
                    obj[key] = newRef;
                }
            }
        });
    }

    recurse(obj,{},function(obj,key,state){
        if (isRef(obj, key)) {
            if (obj.$fixed) delete obj.$fixed;
        }
    });

    if (options.verbose>1) console.log('Finished internal resolution');
    return obj;
}

function filterData(data, options) {
    if (!options.filters || !options.filters.length) return data;
    for (let filter of options.filters) {
        data = filter(data, options);
    }
    return data;
}

function resolveExternal(root, pointer, options, callback) {
    var u = url.parse(options.source);
    var base = options.source.split('\\').join('/').split('/');
    let doc = base.pop(); // drop the actual filename
    if (!doc) base.pop(); // in case it ended with a /
    let fragment = '';
    let fnComponents = pointer.split('#');
    if (fnComponents.length > 1) {
        fragment = '#' + fnComponents[1];
        pointer = fnComponents[0];
    }
    base = base.join('/');

    let u2 = url.parse(pointer);
    let effectiveProtocol = (u2.protocol ? u2.protocol : (u.protocol ? u.protocol : 'file:'));

    let target = url.resolve(base ? base + '/' : '', pointer)

    if (options.cache[target]) {
        if (options.verbose) console.log('CACHED', target, fragment);
        /*
            resolutionSource:A this is where we have cached the externally-referenced document from a
            file, http or custom handler
        */
        let context = clone(options.cache[target]);
        let data = context;
        if (fragment) {
            data = jptr(data, fragment);
            if (data === false) data = {}; // case:A(2) where the resolution fails
        }
        data = resolveAllInternal(data, context, pointer, fragment, target, options);
        data = filterData(data, options);
        callback(clone(data), target, options);
        return Promise.resolve(data);
    }

    if (options.verbose) console.log('GET', target, fragment);

    if (options.handlers && options.handlers[effectiveProtocol]) {
        return options.handlers[effectiveProtocol](base, pointer, fragment, options)
            .then(function (data) {
                data = filterData(data, options);
                options.cache[target] = data;
                callback(data, target, options);
                return data;
            })
            .catch(function(ex){
                if (options.verbose) console.warn(ex);
                throw(ex);
            });
    }
    else if (effectiveProtocol.startsWith('http')) {
        return fetch(target, { agent: options.agent })
            .then(function (res) {
                if (res.status !== 200) throw new Error(`Received status code ${res.status}`);
                return res.text();
            })
            .then(function (data) {
                try {
                    let context = yaml.safeLoad(data, { json: true });
                    data = context;
                    options.cache[target] = clone(data);
                    /* resolutionSource:B, from the network, data is fresh, but we clone it into the cache */
                    if (fragment) {
                        data = jptr(data, fragment);
                        if (data === false) data = {}; /* case:B(2) where the resolution fails */
                    }
                    data = resolveAllInternal(data, context, pointer, fragment, target, options);
                    data = filterData(data, options);
                }
                catch (ex) {
                    if (options.verbose) console.warn(ex);
                }
                callback(data, target, options);
                return data;
            })
            .catch(function (err) {
                if (options.verbose) console.warn(err);
                options.cache[target] = {};
                if (options.promise && options.fatal) options.promise.reject(err)
                else throw(err);
            });
    }
    else {
        return readFileAsync(target, options.encoding || 'utf8')
            .then(function (data) {
                try {
                    let context = yaml.safeLoad(data, { json: true });
                    data = context;
                    /*
                        resolutionSource:C from a file, data is fresh but we clone it into the cache
                    */
                    options.cache[target] = clone(data);
                    if (fragment) {
                        data = jptr(data, fragment);
                        if (data === false) data = {}; /* case:C(2) where the resolution fails */
                    }
                    data = resolveAllInternal(data, context, pointer, fragment, target, options);
                    data = filterData(data, options);
                }
                catch (ex) {
                    if (options.verbose) console.warn(ex);
                }
                callback(data, target, options);
                return data;
            })
            .catch(function(err){
                if (options.verbose) console.warn(err);
                if (options.promise && options.fatal) options.promise.reject(err)
                else throw(err);
            });
    }
}

function scanExternalRefs(options) {
    return new Promise(function (res, rej) {
        let refs = options.externalRefs;

        if ((options.resolver.depth>0) && (options.source === options.resolver.base)) {
            // we only need to do any of this when called directly on pass #1
            return res(refs);
        }

        recurse(options.openapi, {identityDetection: true}, function (obj, key, state) {
            if (obj[key] && isRef(obj[key],'$ref')) {
                let $ref = obj[key].$ref;
                if (!$ref.startsWith('#')) {
                    if (!refs[$ref]) {
                        refs[$ref] = { resolved: false, paths: [], description: obj[key].description };
                    }
                    if (refs[$ref].resolved) {
                        if (options.rewriteRefs) {
                            // we've already seen it
                            let newRef = refs[$ref].resolvedAt;
                            if (options.verbose>1) console.log('Rewriting ref', $ref, newRef);
                            obj[key]['x-miro'] = $ref;
                            obj[key].$ref = newRef; // resolutionCase:C (new string)
                        }
                        else {
                            obj[key] = clone(refs[$ref].data); // resolutionCase:D (cloned:yes)
                        }
                    }
                    else {
                        refs[$ref].paths.push(state.path);
                    }
                }
            }
        });

        res(refs);
    });
}

function findExternalRefs(options) {
    return new Promise(function (res, rej) {

        scanExternalRefs(options)
            .then(function (refs) {
                for (let ref in refs) {

                    if (!refs[ref].resolved) {
                        let depth = options.resolver.depth;
                        if (depth>0) depth++;
                        options.resolver.actions[depth].push(function () {
                            return resolveExternal(options.openapi, ref, options, function (data, source, options) {
                                if (!refs[ref].resolved) {
                                    let external = {};
                                    external.context = refs[ref];
                                    external.$ref = ref;
                                    external.original = clone(data);
                                    external.updated = data;
                                    external.source = source;
                                    options.externals.push(external);
                                    refs[ref].resolved = true;
                                }

                                let localOptions = Object.assign({}, options, { source: '',
                                    resolver: {actions: options.resolver.actions,
                                    depth: options.resolver.actions.length-1, base: options.resolver.base } });
                                if (options.patch && refs[ref].description && !data.description &&
                                    (typeof data === 'object')) {
                                    data.description = refs[ref].description;
                                }
                                refs[ref].data = data;
                                let pointers = unique(refs[ref].paths).sort(function(a,b){
                                    if (a.length < b.length) return -1;
                                    if (a.length > b.length) return +1;
                                    return 0;
                                });
                                for (let ptr of pointers) {
                                    // shared x-ms-examples $refs confuse the fixupRefs heuristic in index.js
                                    if (refs[ref].resolvedAt && (ptr !== refs[ref].resolvedAt) && (ptr.indexOf('x-ms-examples/')<0)) {
                                        if (options.verbose>1) console.log('Creating pointer to data at', ptr);
                                        jptr(options.openapi, ptr, { $ref: refs[ref].resolvedAt, 'x-miro': ref }); // resolutionCase:E (new object)
                                    }
                                    else {
                                        if (refs[ref].resolvedAt) {
                                            if (options.verbose>1) console.log('Avoiding circular reference');
                                        }
                                        else {
                                            refs[ref].resolvedAt = ptr;
                                            if (options.verbose>1) console.log('Creating initial clone of data at', ptr);
                                        }
                                        let cdata = clone(data);
                                        jptr(options.openapi, ptr, cdata); // resolutionCase:F (cloned:yes)
                                    }
                                }
                                if (options.resolver.actions[localOptions.resolver.depth].length === 0) {
                                    //options.resolver.actions[localOptions.resolver.depth].push(function () { return scanExternalRefs(localOptions) });
                                    options.resolver.actions[localOptions.resolver.depth].push(function () { return findExternalRefs(localOptions) }); // findExternalRefs calls scanExternalRefs
                                }
                            });
                        });
                    }
                }
            })
            .catch(function(ex){
                if (options.verbose) console.warn(ex);
                rej(ex);
            });

        let result = {options:options};
        result.actions = options.resolver.actions[options.resolver.depth];
        res(result);
    });
}

const serial = funcs =>
    funcs.reduce((promise, func) =>
        promise.then(result => func().then(Array.prototype.concat.bind(result))), Promise.resolve([]));

function loopReferences(options, res, rej) {
    options.resolver.actions.push([]);
    findExternalRefs(options)
        .then(function (data) {
            serial(data.actions)
                .then(function () {
                    if (options.resolver.depth>=options.resolver.actions.length) {
                        console.warn('Ran off the end of resolver actions');
                        return res(true);
                    } else {
                        options.resolver.depth++;
                        if (options.resolver.actions[options.resolver.depth].length) {
                            setTimeout(function () {
                                loopReferences(data.options, res, rej);
                            }, 0);
                        }
                        else {
                            if (options.verbose>1) console.log(common.colour.yellow+'Finished resolution!',common.colour.normal);
                            res(options);
                        }
                    }
                })
                .catch(function (ex) {
                    if (options.verbose) console.warn(ex);
                    rej(ex);
                });
        })
        .catch(function(ex){
            if (options.verbose) console.warn(ex);
            rej(ex);
        });
}

function setupOptions(options) {
    if (!options.cache) options.cache = {};

    options.cache[options.source] = { '$ref': '#/' };

    if (!options.externals) options.externals = [];
    if (!options.externalRefs) options.externalRefs = [];
    options.rewriteRefs = true;
    options.resolver = {};
    options.resolver.depth = 0;
    options.resolver.base = options.source;
    options.resolver.actions = [[]];
}

/** compatibility function for swagger2openapi */
function optionalResolve(options) {
    setupOptions(options);
    return new Promise(function (res, rej) {
        if (options.resolve)
            loopReferences(options, res, rej)
        else
            res(options);
    });
}

function resolve(openapi,source,options) {
    if (!options) options = {};
    options.openapi = openapi;
    options.source = source;
    options.resolve = true;
    setupOptions(options);
    return new Promise(function (res, rej) {
        loopReferences(options, res, rej)
    });
}

module.exports = {
    optionalResolve: optionalResolve,
    resolve: resolve
};

<|endoftext|>


<commit_before>import Basket from './svgs/basket';
import Bin from './svgs/bin';
import Business from './svgs/business';
import CSV from './svgs/csv';
import Chevron from './svgs/chevron';
import Completed from './svgs/completed';
import Dribble from './svgs/dribble';
import Edit from './svgs/edit';
import Email from './svgs/email';
import ExternalLink from './svgs/external-link';
import Github from './svgs/github';
import Help from './svgs/help';
import Individual from './svgs/individual';
import Information from './svgs/information';
import Key from './svgs/key';
import Location from './svgs/location';
import Maintenance from './svgs/maintenance';
import Message from './svgs/message';
import Minus from './svgs/minus';
import Mobile from './svgs/mobile';
import New from './svgs/new';
import PDF from './svgs/pdf';
import Paperclip from './svgs/paperclip';
import Payment from './svgs/payment';
import Phone from './svgs/phone';
import Plus from './svgs/plus';
import Print from './svgs/print';
import Processing from './svgs/processing';
import Progress from './svgs/progress';
import Refresh from './svgs/refresh';
import SortDown from './svgs/sort-down';
import SortUp from './svgs/sort-up';
import Submitted from './svgs/submitted';
import Sync from './svgs/sync';
import Twitter from './svgs/twitter';
import Warning from './svgs/warning';
import WhiteTick from './svgs/white-tick';

export default {
  basket: Basket,
  bin: Bin,
  business: Business,
  csv: CSV,
  chevron: Chevron,
  completed: Completed,
  dribble: Dribble,
  edit: Edit,
  email: Email,
  'external-link': ExternalLink,
  github: Github,
  help: Help,
  individual: Individual,
  information: Information,
  key: Key,
  location: Location,
  maintenance: Maintenance,
  message: Message,
  minus: Minus,
  mobile: Mobile,
  new: New,
  pdf: PDF,
  paperclip: Paperclip,
  payment: Payment,
  phone: Phone,
  plus: Plus,
  print: Print,
  processing: Processing,
  progress: Progress,
  refresh: Refresh,
  'sort-down': SortDown,
  'sort-up': SortUp,
  submitted: Submitted,
  sync: Sync,
  twitter: Twitter,
  warning: Warning,
  'white-tick': WhiteTick
};
<commit_msg>SBF-384: add import for draft icon<commit_after>import Basket from './svgs/basket';
import Bin from './svgs/bin';
import Business from './svgs/business';
import CSV from './svgs/csv';
import Chevron from './svgs/chevron';
import Completed from './svgs/completed';
import Draft from './svgs/draft';
import Dribble from './svgs/dribble';
import Edit from './svgs/edit';
import Email from './svgs/email';
import ExternalLink from './svgs/external-link';
import Github from './svgs/github';
import Help from './svgs/help';
import Individual from './svgs/individual';
import Information from './svgs/information';
import Key from './svgs/key';
import Location from './svgs/location';
import Maintenance from './svgs/maintenance';
import Message from './svgs/message';
import Minus from './svgs/minus';
import Mobile from './svgs/mobile';
import New from './svgs/new';
import PDF from './svgs/pdf';
import Paperclip from './svgs/paperclip';
import Payment from './svgs/payment';
import Phone from './svgs/phone';
import Plus from './svgs/plus';
import Print from './svgs/print';
import Processing from './svgs/processing';
import Progress from './svgs/progress';
import Refresh from './svgs/refresh';
import SortDown from './svgs/sort-down';
import SortUp from './svgs/sort-up';
import Submitted from './svgs/submitted';
import Sync from './svgs/sync';
import Twitter from './svgs/twitter';
import Warning from './svgs/warning';
import WhiteTick from './svgs/white-tick';

export default {
  basket: Basket,
  bin: Bin,
  business: Business,
  csv: CSV,
  chevron: Chevron,
  completed: Completed,
  dribble: Dribble,
  edit: Edit,
  email: Email,
  'external-link': ExternalLink,
  github: Github,
  help: Help,
  individual: Individual,
  information: Information,
  key: Key,
  location: Location,
  maintenance: Maintenance,
  message: Message,
  minus: Minus,
  mobile: Mobile,
  new: New,
  pdf: PDF,
  paperclip: Paperclip,
  payment: Payment,
  phone: Phone,
  plus: Plus,
  print: Print,
  processing: Processing,
  progress: Progress,
  refresh: Refresh,
  'sort-down': SortDown,
  'sort-up': SortUp,
  submitted: Submitted,
  sync: Sync,
  twitter: Twitter,
  warning: Warning,
  'white-tick': WhiteTick
};
<|endoftext|>


<commit_before>import React from 'react'
import { Link } from 'react-static'
import { Avatar, Flex, Box, Heading, Lead } from 'rebass'
import CTA from './CTA'
import { shuffle, range, sample } from 'lodash'
import { mx, mm } from '../theme'

const Root = Flex.extend.attrs({
  justify: 'center',
  align: 'center',
  pt: 3,
  px: [0, 3]
})`
  position: relative;
  min-height: 32rem;
  max-width: 100vw;
  height: 100vh;
  overflow-y: hidden;
`

const Fill = Flex.extend.attrs({ justify: 'space-around', wrap: true })`
  position: absolute;
  top: 0;
  z-index: -1;
`

const Bubble = Avatar.extend`
  margin: 0.75em;
  &:nth-child(odd) {
    margin-left: 1.5em;
    margin-top: 0;
  }
  &:nth-child(even) {
    margin-right: 1em;
  }
  &:nth-child(6n) {
    width: 3em;
    height: 3em;
    margin-bottom: 1em;
  }
  &:nth-child(8n) {
    width: 2.5em;
    height: 2.5em;
    margin-left: 1em;
  }
  &:nth-child(5n) {
    margin-top: 2em;
  }
  &:nth-child(13n) {
    width: 1.5em;
    height: 1.5em;
  }
  &:nth-child(17n) {
    margin-top: 3em;
  }
  ${mm[1]} {
    max-width: 72px;
    max-height: 72px;
    margin: 0.25em;
  }
`

const Cloud = Box.extend`
  margin: auto;
  border-radius: 4rem;
  box-shadow: 0 0 2rem 4rem rgba(252, 252, 252, 0.95);
  background-color: rgba(252, 252, 252, 0.95);
  text-align: center;
  position: relative;
  z-index: 2;
  @media (min-height: 24em) {
    box-sizing: content-box;
    border-radius: 8rem;
    box-shadow: 0 0 4rem 4rem rgba(252, 252, 252, 0.95);
    max-width: 36rem;
    padding: 1rem;
    top: -2rem;
  }
`

const Title = Heading.extend.attrs({
  is: 'h1',
  f: [5, 6, 7],
  mx: 'auto',
  my: 0,
  color: 'primary'
})`
  line-height: 1;
  max-width: 32rem;
`
const Subtitle = Lead.extend.attrs({
  f: [3, 4],
  mt: 3,
  mb: 4,
  mx: 'auto',
  color: 'grey'
})`line-height: 1.5;`

const Bubbles = ({ children }) => (
  <Root>
    <Fill>
      {shuffle([...range(92), ...range(92)]).map((i, n) => (
        <Bubble
          src={`/avatars/${i + 1}.jpg`}
          size={sample([48, 56, 64, 72, 84, 96])}
          key={`a-${n}`}
        />
      ))}
    </Fill>
    <Cloud>
      <Title>
        By the students,

        for the students.
      </Title>
      <Subtitle>
        Hack Clubs are high school clubs where students learn to code and build
        amazing things together.
      </Subtitle>
      <Flex justify="center" wrap m={-2}>
        <CTA href="/donate" bg="white" color="primary">
          Donate
        </CTA>
        <CTA to="/start">Start a Club</CTA>
      </Flex>
    </Cloud>
  </Root>
)

export default Bubbles
<commit_msg>Make description more descriptive<commit_after>import React from 'react'
import { Link } from 'react-static'
import { Avatar, Flex, Box, Heading, Lead } from 'rebass'
import CTA from './CTA'
import { shuffle, range, sample } from 'lodash'
import { mx, mm } from '../theme'

const Root = Flex.extend.attrs({
  justify: 'center',
  align: 'center',
  pt: 3,
  px: [0, 3]
})`
  position: relative;
  min-height: 32rem;
  max-width: 100vw;
  height: 100vh;
  overflow-y: hidden;
`

const Fill = Flex.extend.attrs({ justify: 'space-around', wrap: true })`
  position: absolute;
  top: 0;
  z-index: -1;
`

const Bubble = Avatar.extend`
  margin: 0.75em;
  &:nth-child(odd) {
    margin-left: 1.5em;
    margin-top: 0;
  }
  &:nth-child(even) {
    margin-right: 1em;
  }
  &:nth-child(6n) {
    width: 3em;
    height: 3em;
    margin-bottom: 1em;
  }
  &:nth-child(8n) {
    width: 2.5em;
    height: 2.5em;
    margin-left: 1em;
  }
  &:nth-child(5n) {
    margin-top: 2em;
  }
  &:nth-child(13n) {
    width: 1.5em;
    height: 1.5em;
  }
  &:nth-child(17n) {
    margin-top: 3em;
  }
  ${mm[1]} {
    max-width: 72px;
    max-height: 72px;
    margin: 0.25em;
  }
`

const Cloud = Box.extend`
  margin: auto;
  border-radius: 4rem;
  box-shadow: 0 0 2rem 4rem rgba(252, 252, 252, 0.95);
  background-color: rgba(252, 252, 252, 0.95);
  text-align: center;
  position: relative;
  z-index: 2;
  @media (min-height: 24em) {
    box-sizing: content-box;
    border-radius: 8rem;
    box-shadow: 0 0 4rem 4rem rgba(252, 252, 252, 0.95);
    max-width: 36rem;
    padding: 1rem;
    top: -2rem;
  }
`

const Title = Heading.extend.attrs({
  is: 'h1',
  f: [5, 6, 7],
  mx: 'auto',
  my: 0,
  color: 'primary'
})`
  line-height: 1;
  max-width: 32rem;
`
const Subtitle = Lead.extend.attrs({
  f: [3, 4],
  mt: 3,
  mb: 4,
  mx: 'auto',
  color: 'grey'
})`line-height: 1.5;`

const Bubbles = ({ children }) => (
  <Root>
    <Fill>
      {shuffle([...range(92), ...range(92)]).map((i, n) => (
        <Bubble
          src={`/avatars/${i + 1}.jpg`}
          size={sample([48, 56, 64, 72, 84, 96])}
          key={`a-${n}`}
        />
      ))}
    </Fill>
    <Cloud>
      <Title>
        By the students,

        for the students.
      </Title>
      <Subtitle>
        Hack Club is a nonprofit network of free student-led coding clubs where
        members learn to code through building things.
      </Subtitle>
      <Flex justify="center" wrap m={-2}>
        <CTA href="/donate" bg="white" color="primary">
          Donate
        </CTA>
        <CTA to="/start">Start a Club</CTA>
      </Flex>
    </Cloud>
  </Root>
)

export default Bubbles
<|endoftext|>


<commit_before>import React from 'react';
import { Form, Select, Icon } from 'antd';
import { layout, getRules } from 'olymp-ui';

const StateInput = ({
  item,
  field,
  label,
  layout,
  initialValue,
  rules,
  placeholder,
  form,
  ...rest
}) => (
  <Form.Item key={field} label={label} {...layout}>
    {form.getFieldDecorator(field, {
      initialValue: item ? item[field] : undefined,
      rules: getRules(rules, label),
    })(
      <Select style={{ width: '100%' }} {...rest}>
        <Select.Option value="DRAFT">
          <Icon type="inbox" /> Ablage
          </Select.Option>
        <Select.Option value="PUBLISHED">
          <Icon type="check" />{' '}
            Veröffentlicht
          </Select.Option>
        <Select.Option value="REMOVED">
          <Icon type="delete" /> Gelöscht
          </Select.Option>
      </Select>
      )}
  </Form.Item>
  );
StateInput.defaultProps = { layout };
export default StateInput;
<commit_msg>make state required<commit_after>import React from 'react';
import { Form, Select, Icon } from 'antd';
import { layout, getRules } from 'olymp-ui';

const StateInput = ({
  item,
  field,
  label,
  layout,
  initialValue,
  rules = ['required'],
  placeholder,
  form,
  ...rest
}) => (
  <Form.Item key={field} label={label} {...layout}>
    {form.getFieldDecorator(field, {
      initialValue: item ? item[field] : undefined,
      rules: getRules(rules, label),
    })(
      <Select style={{ width: '100%' }} {...rest}>
        <Select.Option value="DRAFT">

            <Icon type="inbox" />
          {' '}
          Ablage
        </Select.Option>
        <Select.Option value="PUBLISHED">

            <Icon type="check" />
          {' '}
          Veröffentlicht
        </Select.Option>
        <Select.Option value="REMOVED">

            <Icon type="delete" />
          {' '}
          Gelöscht
        </Select.Option>
      </Select>,
    )}
  </Form.Item>
);
StateInput.defaultProps = { layout };
export default StateInput;
<|endoftext|>


<commit_before>// Dependencies
import React, {Component, PropTypes} from 'react';
import ReactDOM from 'react-dom';
import moment from 'moment';
import Base from '../../../behaviours/component-base';
import InputText from '../text';
import DatePicker from 'react-date-picker';
import compose from 'lodash/function/compose';
import isArray from 'lodash/lang/isArray';
import uniqueId from 'lodash/utility/uniqueId';
import closest from 'closest';

const isISOString = value => moment.utc(value, moment.ISO_8601).isValid();

const propTypes = {
    drops: PropTypes.oneOf(['up', 'down']).isRequired,
    error: PropTypes.string,
    locale: PropTypes.string.isRequired,
    name: PropTypes.string.isRequired,
    onChange: PropTypes.func.isRequired,
    beforeValueGetter: PropTypes.func.isRequired,
    placeholder: PropTypes.string,
    showDropdowns: PropTypes.bool.isRequired,
    validate: PropTypes.func,
    value: (props, propName, componentName) => {
        const prop = props[propName];
        if (prop && !isISOString(prop)) {
            throw new Error(`The date ${prop} provided to the component ${componentName} is not an ISO date. Please provide a valid date string.`);
        }
    }
};

const defaultProps = {
    drops: 'down',
    locale: 'en',
    format: 'MM/DD/YYYY',
    beforeValueGetter: value => value,
    /**
    * Default onChange prop, that will log an error.
    */
    onChange() {
        console.error('You did not give an onChange method to an input date, please check your code.');
    },
    showDropdowns: true,
    validate: isISOString
};

@Base
class InputDate extends Component {
    constructor(props) {
        super(props);
        const {value} = props;
        const state = {
            dropDownDate: isISOString(value) ? moment.utc(value, moment.ISO_8601) : moment.utc(),
            inputDate: this._formatDate(value),
            displayPicker: false
        };
        this.state = state;
        this._inputDateId = uniqueId('input-date-');
    }

    componentWillMount() {
        // moment.locale(this.props.locale);
        document.addEventListener('click', this._onDocumentClick);
    }

    componentDidMount() {
        const {drops, showDropdowns} = this.props;
        const {inputDate: startDate} = this.state;
    }

    componentWillReceiveProps({value}) {
        this.setState({
            dropDownDate: isISOString(value) ? moment.utc(value, moment.ISO_8601) : moment(),
            inputDate: this._formatDate(value)
        });
    }

    componentWillUnmount() {
        document.removeEventListener('click', this._onDocumentClick);
    }

    _isInputFormatCorrect = value => this._parseInputDate(value).isValid();

    _parseInputDate = inputDate => {
        const {format} = this.props;
        return moment.utc(inputDate, format);
    };

    _formatDate = isoDate => {
        let {format} = this.props;
        if (isISOString(isoDate)) {
            if (isArray(format)) {
                format = format[0];
            }
            return moment.utc(isoDate, moment.ISO_8601).format(format);
        } else {
            return isoDate;
        }
    };

    _onInputChange = (inputDate, fromBlur) => {
        if (this._isInputFormatCorrect(inputDate)) {
            const dropDownDate = this._parseInputDate(inputDate);
            this.setState({dropDownDate, inputDate});
        } else {
            this.setState({inputDate});
        }
        if(fromBlur !== true) {
            this.props.onChange(inputDate);
        }
    };

    _onInputBlur = () => {
        const {inputDate} = this.state;
        this._onInputChange(inputDate, true);
    };

    _onDropDownChange = (text, date) => {
        if (date._isValid) {
            this.setState({displayPicker: false}, () => {
                const correctedDate = moment.utc(date).add(moment(date).utcOffset(), 'minutes').toISOString(); // Add UTC offset to get right ISO string
                this.props.onChange(correctedDate);
                this._onInputChange(this._formatDate(correctedDate), true);
            });
        }
    };

    _onInputFocus = () => {
        this.setState({displayPicker: true});
    };

    _onDocumentClick = ({target}) => {
        const targetClassAttr = target.getAttribute('class');
        const isTriggeredFromPicker = targetClassAttr ? targetClassAttr.includes('dp-cell') : false; //this is the only way to check the target comes from picker cause at this stage, month and year div are unmounted by React.
        if(!isTriggeredFromPicker) {
            //if target was not triggered inside the date picker, we check it was not triggered by the input
            if (closest(target, `[data-id='${this._inputDateId}']`, true) === undefined) {
                this.setState({displayPicker: false}, () => this._onInputBlur());
            }
        }
    };

    _handleKeyDown = ({key}) => {
        if (key === 'Tab' || key === 'Enter') {
            this.setState({displayPicker: false}, () => this._onInputBlur());
        }
    };

    getValue = () => {
        const {inputDate} = this.state;
        const rawValue = this._isInputFormatCorrect(inputDate) ? this._parseInputDate(inputDate).toISOString() : null;
        return this.props.beforeValueGetter(rawValue);
    };

    validate = () => {
        const {inputDate} = this.state;
        const {isRequired} = this.props;
        if ('' === inputDate || !inputDate) {
            return ({
                isValid: !isRequired,
                message: 'field.required'
            });
        } else {
            return ({
                isValid: this._isInputFormatCorrect(inputDate),
                message: this.i18n('input.date.invalid', {date: inputDate})
            });
        }
    };

    render() {
        const {error, locale, name, placeholder, disabled} = this.props;
        const {dropDownDate, inputDate, displayPicker} = this.state;
        const {_onInputBlur, _onInputChange, _onInputFocus, _onDropDownChange, _onPickerCloserClick, _handleKeyDown} = this;
        const inputProps = { disabled };
        return (

                <InputText error={error} name={name} onChange={_onInputChange} onKeyDown={_handleKeyDown} onFocus={_onInputFocus} placeholder={placeholder} ref='input' value={inputDate} {...inputProps} />
                {displayPicker &&

                        <DatePicker
                            date={dropDownDate}
                            hideFooter
                            locale={locale}
                            onChange={_onDropDownChange}
                            ref='picker'
                            />

                }

        );
    }
}

InputDate.propTypes = propTypes;
InputDate.defaultProps = defaultProps;
InputDate.displayName = 'InputDate';

export default InputDate;
<commit_msg>Missing utc<commit_after>// Dependencies
import React, {Component, PropTypes} from 'react';
import ReactDOM from 'react-dom';
import moment from 'moment';
import Base from '../../../behaviours/component-base';
import InputText from '../text';
import DatePicker from 'react-date-picker';
import compose from 'lodash/function/compose';
import isArray from 'lodash/lang/isArray';
import uniqueId from 'lodash/utility/uniqueId';
import closest from 'closest';

const isISOString = value => moment.utc(value, moment.ISO_8601).isValid();

const propTypes = {
    drops: PropTypes.oneOf(['up', 'down']).isRequired,
    error: PropTypes.string,
    locale: PropTypes.string.isRequired,
    name: PropTypes.string.isRequired,
    onChange: PropTypes.func.isRequired,
    beforeValueGetter: PropTypes.func.isRequired,
    placeholder: PropTypes.string,
    showDropdowns: PropTypes.bool.isRequired,
    validate: PropTypes.func,
    value: (props, propName, componentName) => {
        const prop = props[propName];
        if (prop && !isISOString(prop)) {
            throw new Error(`The date ${prop} provided to the component ${componentName} is not an ISO date. Please provide a valid date string.`);
        }
    }
};

const defaultProps = {
    drops: 'down',
    locale: 'en',
    format: 'MM/DD/YYYY',
    beforeValueGetter: value => value,
    /**
    * Default onChange prop, that will log an error.
    */
    onChange() {
        console.error('You did not give an onChange method to an input date, please check your code.');
    },
    showDropdowns: true,
    validate: isISOString
};

@Base
class InputDate extends Component {
    constructor(props) {
        super(props);
        const {value} = props;
        const state = {
            dropDownDate: isISOString(value) ? moment.utc(value, moment.ISO_8601) : moment.utc(),
            inputDate: this._formatDate(value),
            displayPicker: false
        };
        this.state = state;
        this._inputDateId = uniqueId('input-date-');
    }

    componentWillMount() {
        // moment.locale(this.props.locale);
        document.addEventListener('click', this._onDocumentClick);
    }

    componentDidMount() {
        const {drops, showDropdowns} = this.props;
        const {inputDate: startDate} = this.state;
    }

    componentWillReceiveProps({value}) {
        this.setState({
            dropDownDate: isISOString(value) ? moment.utc(value, moment.ISO_8601) : moment.utc(),
            inputDate: this._formatDate(value)
        });
    }

    componentWillUnmount() {
        document.removeEventListener('click', this._onDocumentClick);
    }

    _isInputFormatCorrect = value => this._parseInputDate(value).isValid();

    _parseInputDate = inputDate => {
        const {format} = this.props;
        return moment.utc(inputDate, format);
    };

    _formatDate = isoDate => {
        let {format} = this.props;
        if (isISOString(isoDate)) {
            if (isArray(format)) {
                format = format[0];
            }
            return moment.utc(isoDate, moment.ISO_8601).format(format);
        } else {
            return isoDate;
        }
    };

    _onInputChange = (inputDate, fromBlur) => {
        if (this._isInputFormatCorrect(inputDate)) {
            const dropDownDate = this._parseInputDate(inputDate);
            this.setState({dropDownDate, inputDate});
        } else {
            this.setState({inputDate});
        }
        if(fromBlur !== true) {
            this.props.onChange(inputDate);
        }
    };

    _onInputBlur = () => {
        const {inputDate} = this.state;
        this._onInputChange(inputDate, true);
    };

    _onDropDownChange = (text, date) => {
        if (date._isValid) {
            this.setState({displayPicker: false}, () => {
                const correctedDate = moment.utc(date).add(moment(date).utcOffset(), 'minutes').toISOString(); // Add UTC offset to get right ISO string
                this.props.onChange(correctedDate);
                this._onInputChange(this._formatDate(correctedDate), true);
            });
        }
    };

    _onInputFocus = () => {
        this.setState({displayPicker: true});
    };

    _onDocumentClick = ({target}) => {
        const targetClassAttr = target.getAttribute('class');
        const isTriggeredFromPicker = targetClassAttr ? targetClassAttr.includes('dp-cell') : false; //this is the only way to check the target comes from picker cause at this stage, month and year div are unmounted by React.
        if(!isTriggeredFromPicker) {
            //if target was not triggered inside the date picker, we check it was not triggered by the input
            if (closest(target, `[data-id='${this._inputDateId}']`, true) === undefined) {
                this.setState({displayPicker: false}, () => this._onInputBlur());
            }
        }
    };

    _handleKeyDown = ({key}) => {
        if (key === 'Tab' || key === 'Enter') {
            this.setState({displayPicker: false}, () => this._onInputBlur());
        }
    };

    getValue = () => {
        const {inputDate} = this.state;
        const rawValue = this._isInputFormatCorrect(inputDate) ? this._parseInputDate(inputDate).toISOString() : null;
        return this.props.beforeValueGetter(rawValue);
    };

    validate = () => {
        const {inputDate} = this.state;
        const {isRequired} = this.props;
        if ('' === inputDate || !inputDate) {
            return ({
                isValid: !isRequired,
                message: 'field.required'
            });
        } else {
            return ({
                isValid: this._isInputFormatCorrect(inputDate),
                message: this.i18n('input.date.invalid', {date: inputDate})
            });
        }
    };

    render() {
        const {error, locale, name, placeholder, disabled} = this.props;
        const {dropDownDate, inputDate, displayPicker} = this.state;
        const {_onInputBlur, _onInputChange, _onInputFocus, _onDropDownChange, _onPickerCloserClick, _handleKeyDown} = this;
        const inputProps = { disabled };
        return (

                <InputText error={error} name={name} onChange={_onInputChange} onKeyDown={_handleKeyDown} onFocus={_onInputFocus} placeholder={placeholder} ref='input' value={inputDate} {...inputProps} />
                {displayPicker &&

                        <DatePicker
                            date={dropDownDate}
                            hideFooter
                            locale={locale}
                            onChange={_onDropDownChange}
                            ref='picker'
                            />

                }

        );
    }
}

InputDate.propTypes = propTypes;
InputDate.defaultProps = defaultProps;
InputDate.displayName = 'InputDate';

export default InputDate;
<|endoftext|>


<commit_before>import * as calculate from '../utils/calculate'
import * as getFromCSV from './scriptUtils/getFromCSV'
import R from 'ramda'
import { writeToDB, clearCollection } from '../service/dbService'
import readCSV from '../service/readCSV'

const getProperties = (ev) => ({
  year: getFromCSV.getYear(ev),
  term: getFromCSV.getTerm(ev),
  course: getFromCSV.getCourse(ev),
  section: getFromCSV.getSection(ev),
  courseName: getFromCSV.getCourseName(ev),
  courseLevel: getFromCSV.getCourseLevel(ev),
  dept: getFromCSV.getDept(ev),
  instructorName: getFromCSV.getInstructorName(ev),
  PUID: getFromCSV.getPUID(ev),
  gender: getFromCSV.getGender(ev)
})

// filter out any rows in the csv with all 0 UMI ratings
const filterAll0s = (csv) => csv.filter(ev => {
  if (getFromCSV.getUMI1(ev) === 0 && getFromCSV.getUMI2(ev) === 0 && getFromCSV.getUMI3(ev) === 0 && getFromCSV.getUMI4(ev) === 0 && getFromCSV.getUMI5(ev) === 0 && getFromCSV.getUMI6(ev) === 0) return false
  else return true
})

const createCourseObj = (csv) => {
  const filteredCSV = filterAll0s(csv)
  return filteredCSV.reduce((acc, ev) => {
    const { year, term, course, section, courseName, courseLevel, dept, instructorName, PUID, gender } = getProperties(ev)

    const uniqSectionInTerm = (x) => (x.year === year &&
      x.course === course &&
      x.term === term &&
      x.section === section &&
      x.instructorName === instructorName)

    if (acc.some(x => uniqSectionInTerm(x))) {
      const index = acc.findIndex(x => uniqSectionInTerm(x))
      for (let i = 1; i <= 6; i++) {
        let getUMI
        let UMI = 'UMI' + i
        switch (i) {
          case 1:
            getUMI = getFromCSV.getUMI1(ev)
            break
          case 2:
            getUMI = getFromCSV.getUMI2(ev)
            break
          case 3:
            getUMI = getFromCSV.getUMI3(ev)
            break
          case 4:
            getUMI = getFromCSV.getUMI4(ev)
            break
          case 5:
            getUMI = getFromCSV.getUMI5(ev)
            break
          case 6:
            getUMI = getFromCSV.getUMI6(ev)
            break
        }
        if (typeof (acc[index][UMI].count[getUMI]) === 'undefined') {
          acc[index][UMI].count = { ...acc[index][UMI].count, [getUMI]: 1 }
        } else acc[index][UMI].count[getUMI] = acc[index][UMI].count[getUMI] + 1
      }
      acc[index].gender[gender] = acc[index].gender[gender] + 1
      return acc
    } else {
      acc.push({
        year,
        term,
        course,
        section,
        courseName,
        courseLevel,
        dept,
        instructorName,
        PUID,
        gender: {
          Female: (gender === 'Female') ? 1 : 0,
          Male: (gender === 'Male') ? 1 : 0
        },
        UMI1: {
          count: {
            [String(getFromCSV.getUMI1(ev))]: 1
          }
        },
        UMI2: {
          count: {
            [String(getFromCSV.getUMI2(ev))]: 1
          }
        },
        UMI3: {
          count: {
            [String(getFromCSV.getUMI3(ev))]: 1
          }
        },
        UMI4: {
          count: {
            [String(getFromCSV.getUMI4(ev))]: 1
          }
        },
        UMI5: {
          count: {
            [String(getFromCSV.getUMI5(ev))]: 1
          }
        },
        UMI6: {
          count: {
            [String(getFromCSV.getUMI6(ev))]: 1
          }
        }
      })
      return acc
    }
  }, [])
}

const removeIncorrectCounts = (courseObj) => {
  for (let i = 1; i <= 6; i++) {
    let UMI = 'UMI' + i
    const countObj = courseObj[UMI].count
    const newObj = R.pick(['1', '2', '3', '4', '5'], countObj)
    courseObj[UMI].count = newObj
  }
  return courseObj
}

const insertDispersionIndex = (courseObj) => {
  for (let i = 1; i <= 6; i++) {
    let UMI = 'UMI' + i
    courseObj[UMI].dispersionIndex = calculate.dispersionIndex(courseObj[UMI].count)
  }
  return courseObj
}

const insertAvg = (courseObj) => {
  for (let i = 1; i <= 6; i++) {
    let UMI = 'UMI' + i
    courseObj[UMI].average = calculate.umiAvg(courseObj[UMI].count)
  }
  return courseObj
}

const insertPercentFav = (courseObj) => {
  for (let i = 1; i <= 6; i++) {
    let UMI = 'UMI' + i
    courseObj[UMI].percentFavourable = calculate.percentFavourable(courseObj[UMI].count)
  }
  return courseObj
}

const insertPercentileRanking = (courseObjs) => {
  let sortedByUMI
  for (let i = 1; i <= 6; i++) {
    let UMI = 'UMI' + i
    sortedByUMI = R.sort((a, b) => a[UMI].average - b[UMI].average, courseObjs)
    sortedByUMI.map((course) => {
      const filteredByTerm = sortedByUMI.filter(x => x.year === course.year && x.term === course.term)
      const filteredByDept = filteredByTerm.filter(x => x.dept === course.dept)
      course[UMI].percentileRankingByFaculty = calculate.percentileRankingOfCourse(course, UMI, filteredByTerm)
      course[UMI].percentileRankingByDept = calculate.percentileRankingOfCourse(course, UMI, filteredByDept)
    })
  }
  return sortedByUMI
}

// errorChecks
const errorCheck = (courseObjs) => {
  // check to make sure enrolments are all there:
  const courseObjMissingEnrolment = courseObjs.filter(course => !course.hasOwnProperty('enrolment'))
  if (courseObjMissingEnrolment.length !== 0) {
    console.log(courseObjMissingEnrolment)
    throw new Error('Some courses are missing enrolment data')
  }

  const courseObjWithFalsyValues = courseObjs.filter(course => {
    if (!course.year && !course.term && !course.section && !course.courseName && !course.courseLevel && !course.dept && !course.instructorName && !course.PUID) return true
    else return false
  })
  if (courseObjWithFalsyValues.length !== 0) {
    console.log(courseObjWithFalsyValues)
    throw new Error('Some courses have null values')
  }

  const courseObjWithFalsyUMIValues = []
  for (let i = 1; i <= 6; i++) {
    let UMI = 'UMI' + i
    courseObjWithFalsyUMIValues.push(courseObjs.filter(course => {
      if (!course[UMI].dispersionIndex && course[UMI].dispersionIndex !== 0) return true
      if (!course[UMI].average && course[UMI].average !== 0) return true
      if (!course[UMI].percentFavourable && course[UMI].percentFavourable !== 0) return true
      if (!course[UMI].percentileRankingByFaculty && course[UMI].percentileRankingByFaculty !== 0) return true
      if (!course[UMI].percentileRankingByDept && course[UMI].percentileRankingByDept !== 0) return true
      else return false
    }))
  }
  if (R.flatten(courseObjWithFalsyUMIValues).length !== 0) {
    // console.log(JSON.stringify(courseObjWithFalsyUMIValues, null, 2))
    // throw new Error('Some courses have UMI values that are invalid (dispersionIndex, average, etc)')
  }
  return true
}

readCSV('../scripts/source/rawDataAll.csv', (csv) => {
  let courseObjs = createCourseObj(csv)

  courseObjs.map(courseObj => R.pipe(
    x => removeIncorrectCounts(x),
    x => insertDispersionIndex(x),
    x => insertAvg(x),
    x => insertPercentFav(x)
  )(courseObj))

  courseObjs = insertPercentileRanking(courseObjs)

  // this adds in the enrolment data from another CSV
  readCSV('../scripts/source/course_eval_enrollments-2009-2017SA.csv', (csv) => {
    csv.map(enrolmentCourse => {
      const { enrolmentCourseName, enrolmentCourseID, enrolmentSection, enrolmentYear, enrolmentTerm, enrolment } =
        {
          enrolmentCourseName: enrolmentCourse.crsname,
          enrolmentCourseID: getFromCSV.getEnrolmentCourseNumber(enrolmentCourse.crsnum),
          enrolmentSection: getFromCSV.getEnrolmentSection(enrolmentCourse.section),
          enrolmentYear: getFromCSV.getEnrolmentYear(enrolmentCourse.period),
          enrolmentTerm: getFromCSV.getEnrolmentTerm(enrolmentCourse.period),
          enrolment: enrolmentCourse.no_enrolled
        }

      courseObjs.map(course => {
        const { courseName, courseID, section, year, term } =
          {
            courseName: course.courseName,
            courseID: course.course,
            section: course.section,
            year: course.year,
            term: course.term
          }
        if (courseName === enrolmentCourseName && courseID === enrolmentCourseID && section === enrolmentSection && year === enrolmentYear && term === enrolmentTerm) {
          // add in enrolment and response rate into course
          course.enrolment = enrolment
          const responses = course.gender.Female + course.gender.Male
          const responseRate = calculate.toTwoDecimal(responses / enrolment)
          course.responseRate = responseRate
          course.meetsMin = calculate.meetsMinimum(enrolment, responseRate)
        }
      })
    })

    if (errorCheck(courseObjs)) {
      console.log(JSON.stringify(courseObjs, null, 2))
      clearCollection('aggregatedData')
      writeToDB(courseObjs, 'aggregatedData')
    }
  })
})

export {
  createCourseObj,
  insertDispersionIndex,
  insertAvg,
  insertPercentFav,
  insertPercentileRanking,
  removeIncorrectCounts
}
<commit_msg>refactored createAggregatedDataCollection<commit_after>import * as calculate from '../utils/calculate'
import * as getFromCSV from './scriptUtils/getFromCSV'
import R from 'ramda'
import { writeToDB, clearCollection } from '../service/dbService'
import readCSV from '../service/readCSV'

const getProperties = (ev) => ({
  year: getFromCSV.getYear(ev),
  term: getFromCSV.getTerm(ev),
  course: getFromCSV.getCourse(ev),
  section: getFromCSV.getSection(ev),
  courseName: getFromCSV.getCourseName(ev),
  courseLevel: getFromCSV.getCourseLevel(ev),
  dept: getFromCSV.getDept(ev),
  instructorName: getFromCSV.getInstructorName(ev),
  PUID: getFromCSV.getPUID(ev),
  gender: getFromCSV.getGender(ev)
})

// filter out any rows in the csv with all 0 UMI ratings
const filterAll0s = (csv) => csv.filter(ev => {
  if (getFromCSV.getUMI1(ev) === 0 && getFromCSV.getUMI2(ev) === 0 && getFromCSV.getUMI3(ev) === 0 && getFromCSV.getUMI4(ev) === 0 && getFromCSV.getUMI5(ev) === 0 && getFromCSV.getUMI6(ev) === 0) return false
  else return true
})

const createCourseObj = (csv) => {
  const filteredCSV = filterAll0s(csv)
  return filteredCSV.reduce((acc, ev) => {
    const { year, term, course, section, courseName, courseLevel, dept, instructorName, PUID, gender } = getProperties(ev)

    const uniqSectionInTerm = (x) => (x.year === year &&
      x.course === course &&
      x.term === term &&
      x.section === section &&
      x.instructorName === instructorName)

    if (acc.some(x => uniqSectionInTerm(x))) {
      const index = acc.findIndex(x => uniqSectionInTerm(x))
      for (let i = 1; i <= 6; i++) {
        let getUMI
        let UMI = 'UMI' + i
        if (i === 1) getUMI = getFromCSV.getUMI1(ev)
        if (i === 2) getUMI = getFromCSV.getUMI2(ev)
        if (i === 3) getUMI = getFromCSV.getUMI3(ev)
        if (i === 4) getUMI = getFromCSV.getUMI4(ev)
        if (i === 5) getUMI = getFromCSV.getUMI5(ev)
        if (i === 6) getUMI = getFromCSV.getUMI6(ev)
        if (typeof (acc[index][UMI].count[getUMI]) === 'undefined') {
          acc[index][UMI].count = { ...acc[index][UMI].count, [getUMI]: 1 }
        } else acc[index][UMI].count[getUMI] = acc[index][UMI].count[getUMI] + 1
      }
      acc[index].gender[gender] = acc[index].gender[gender] + 1
      return acc
    } else {
      acc.push({
        year,
        term,
        course,
        section,
        courseName,
        courseLevel,
        dept,
        instructorName,
        PUID,
        gender: {
          Female: (gender === 'Female') ? 1 : 0,
          Male: (gender === 'Male') ? 1 : 0
        },
        UMI1: {
          count: {
            [String(getFromCSV.getUMI1(ev))]: 1
          }
        },
        UMI2: {
          count: {
            [String(getFromCSV.getUMI2(ev))]: 1
          }
        },
        UMI3: {
          count: {
            [String(getFromCSV.getUMI3(ev))]: 1
          }
        },
        UMI4: {
          count: {
            [String(getFromCSV.getUMI4(ev))]: 1
          }
        },
        UMI5: {
          count: {
            [String(getFromCSV.getUMI5(ev))]: 1
          }
        },
        UMI6: {
          count: {
            [String(getFromCSV.getUMI6(ev))]: 1
          }
        }
      })
      return acc
    }
  }, [])
}

const removeIncorrectCounts = (courseObj) => {
  for (let i = 1; i <= 6; i++) {
    let UMI = 'UMI' + i
    const countObj = courseObj[UMI].count
    const newObj = R.pick(['1', '2', '3', '4', '5'], countObj)
    courseObj[UMI].count = newObj
  }
  return courseObj
}

const insertDispersionIndex = (courseObj) => {
  for (let i = 1; i <= 6; i++) {
    let UMI = 'UMI' + i
    courseObj[UMI].dispersionIndex = calculate.dispersionIndex(courseObj[UMI].count)
  }
  return courseObj
}

const insertAvg = (courseObj) => {
  for (let i = 1; i <= 6; i++) {
    let UMI = 'UMI' + i
    courseObj[UMI].average = calculate.umiAvg(courseObj[UMI].count)
  }
  return courseObj
}

const insertPercentFav = (courseObj) => {
  for (let i = 1; i <= 6; i++) {
    let UMI = 'UMI' + i
    courseObj[UMI].percentFavourable = calculate.percentFavourable(courseObj[UMI].count)
  }
  return courseObj
}

const insertPercentileRanking = (courseObjs) => {
  let sortedByUMI
  for (let i = 1; i <= 6; i++) {
    let UMI = 'UMI' + i
    sortedByUMI = R.sort((a, b) => a[UMI].average - b[UMI].average, courseObjs)
    sortedByUMI.map((course) => {
      const filteredByTerm = sortedByUMI.filter(x => x.year === course.year && x.term === course.term)
      const filteredByDept = filteredByTerm.filter(x => x.dept === course.dept)
      course[UMI].percentileRankingByFaculty = calculate.percentileRankingOfCourse(course, UMI, filteredByTerm)
      course[UMI].percentileRankingByDept = calculate.percentileRankingOfCourse(course, UMI, filteredByDept)
    })
  }
  return sortedByUMI
}

// errorChecks
const errorCheck = (courseObjs) => {
  // check to make sure enrolments are all there:
  const courseObjMissingEnrolment = courseObjs.filter(course => !course.hasOwnProperty('enrolment'))
  if (courseObjMissingEnrolment.length !== 0) {
    console.log(courseObjMissingEnrolment)
    throw new Error('Some courses are missing enrolment data')
  }

  const courseObjWithFalsyValues = courseObjs.filter(course => {
    if (!course.year && !course.term && !course.section && !course.courseName && !course.courseLevel && !course.dept && !course.instructorName && !course.PUID) return true
    else return false
  })
  if (courseObjWithFalsyValues.length !== 0) {
    console.log(courseObjWithFalsyValues)
    throw new Error('Some courses have null values')
  }

  const courseObjWithFalsyUMIValues = []
  for (let i = 1; i <= 6; i++) {
    let UMI = 'UMI' + i
    courseObjWithFalsyUMIValues.push(courseObjs.filter(course => {
      if (!course[UMI].dispersionIndex && course[UMI].dispersionIndex !== 0) return true
      if (!course[UMI].average && course[UMI].average !== 0) return true
      if (!course[UMI].percentFavourable && course[UMI].percentFavourable !== 0) return true
      if (!course[UMI].percentileRankingByFaculty && course[UMI].percentileRankingByFaculty !== 0) return true
      if (!course[UMI].percentileRankingByDept && course[UMI].percentileRankingByDept !== 0) return true
      else return false
    }))
  }
  if (R.flatten(courseObjWithFalsyUMIValues).length !== 0) {
    // console.log(JSON.stringify(courseObjWithFalsyUMIValues, null, 2))
    // throw new Error('Some courses have UMI values that are invalid (dispersionIndex, average, etc)')
  }
  return true
}

readCSV('../scripts/source/rawDataAll.csv', (csv) => {
  let courseObjs = createCourseObj(csv)

  courseObjs.map(courseObj => R.pipe(
    x => removeIncorrectCounts(x),
    x => insertDispersionIndex(x),
    x => insertAvg(x),
    x => insertPercentFav(x)
  )(courseObj))

  courseObjs = insertPercentileRanking(courseObjs)

  // this adds in the enrolment data from another CSV
  readCSV('../scripts/source/course_eval_enrollments-2009-2017SA.csv', (csv) => {
    csv.map(enrolmentCourse => {
      const { enrolmentCourseName, enrolmentCourseID, enrolmentSection, enrolmentYear, enrolmentTerm, enrolment } =
        {
          enrolmentCourseName: enrolmentCourse.crsname,
          enrolmentCourseID: getFromCSV.getEnrolmentCourseNumber(enrolmentCourse.crsnum),
          enrolmentSection: getFromCSV.getEnrolmentSection(enrolmentCourse.section),
          enrolmentYear: getFromCSV.getEnrolmentYear(enrolmentCourse.period),
          enrolmentTerm: getFromCSV.getEnrolmentTerm(enrolmentCourse.period),
          enrolment: enrolmentCourse.no_enrolled
        }

      courseObjs.map(course => {
        const { courseName, courseID, section, year, term } =
          {
            courseName: course.courseName,
            courseID: course.course,
            section: course.section,
            year: course.year,
            term: course.term
          }
        if (courseName === enrolmentCourseName && courseID === enrolmentCourseID && section === enrolmentSection && year === enrolmentYear && term === enrolmentTerm) {
          // add in enrolment and response rate into course
          course.enrolment = enrolment
          const responses = course.gender.Female + course.gender.Male
          const responseRate = calculate.toTwoDecimal(responses / enrolment)
          course.responseRate = responseRate
          course.meetsMin = calculate.meetsMinimum(enrolment, responseRate)
        }
      })
    })

    if (errorCheck(courseObjs)) {
      console.log(JSON.stringify(courseObjs, null, 2))
      clearCollection('aggregatedData')
      writeToDB(courseObjs, 'aggregatedData')
    }
  })
})

export {
  createCourseObj,
  insertDispersionIndex,
  insertAvg,
  insertPercentFav,
  insertPercentileRanking,
  removeIncorrectCounts
}
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';

import { Spinner as zIndex } from 'zIndexes';

const Spinner = ({ percent }) => (

    <div style={{
      backgroundColor: 'teal',
      position: 'fixed',
      left: 0, top: 0, height: 3,
      width: `${(percent === -1 || percent === 100) ? 0 : percent}%`,
      opacity: (0 < percent && percent <= 100) ? 1 : 0,
      transition: 'all 1s'
    }} />

);

Spinner.propTypes = {
  percent: PropTypes.number
};
Spinner.defaultProps = {
  percent: 0
};

export default Spinner;
<commit_msg>Changed spinner color<commit_after>import React from 'react';
import PropTypes from 'prop-types';

import { Spinner as zIndex } from 'zIndexes';

const Spinner = ({ percent }) => (

    <div style={{
      backgroundColor: 'orange',
      position: 'fixed',
      left: 0, top: 0, height: 3,
      width: `${(percent === -1 || percent === 100) ? 0 : percent}%`,
      opacity: (0 < percent && percent <= 100) ? 1 : 0,
      transition: 'all 1s'
    }} />

);

Spinner.propTypes = {
  percent: PropTypes.number
};
Spinner.defaultProps = {
  percent: 0
};

export default Spinner;
<|endoftext|>


<commit_before>import { FS } from 'grind-support'
import './Template'

export class Compiler {
	engine = null
	directives = { }
	sectionStack = [ ]

	constructor(engine) {
		this.engine = engine
	}

	async compile(template) {
		let contents = (await FS.readFile(template)).toString().trim()
		let extendsLayout = null

		const expressions = [ ]
		let match = null

		// Loops through and finds all directives
		while((match = contents.match(/@([a-z0-9_]+)(\s*\()?/))) {
			if(match.index > 0) {
				expressions.push({
					type: 'string',
					contents: contents.substring(0, match.index)
				})

				contents = contents.substring(match.index)
			}

			let args = null

			if(match[2]) {
				let openCount = -1
				let index = 0
				let startIndex = 0
				let lastIndex = 0

				while(openCount !== 0 && (index = Template.nextIndexOfParenthesis(contents, index)) >= 0) {
					const parenthesis = contents.substring(index, index + 1)

					if(parenthesis === ')') {
						openCount--
					} else if(openCount === -1) {
						startIndex = index
						openCount = 1
					} else {
						openCount++
					}

					lastIndex = index
					index++
				}

				args = contents.substring(startIndex + 1, lastIndex)
				contents = contents.substring(lastIndex + 1)
			} else {
				contents = contents.substring(match[0].length)
			}

			if(match[1] === 'extends') {
				extendsLayout = args
				continue
			}

			const result = this.compileDirective(match[1], args)

			if(!result.isNil) {
				expressions.push({
					type: 'code',
					contents: result
				})
			}
		}

		contents = contents.trim()

		if(contents.length > 0) {
			expressions.push({
				type: 'string',
				contents: contents
			})
		}

		let code = ''

		for(const { type, contents } of expressions) {
			if(type === 'code') {
				code += `${contents}\n`
			} else {
				code += `output += \`${Template.sanitizeHtml(contents)}\`;\n`
			}
		}

		code = `template = function(_, _sections = { }) {\nlet output = '';\n${Template.contextualize(code)}\n`

		if(extendsLayout !== null) {
			code += `return _.$compiler._extends(${extendsLayout}, _, _sections);\n}`
		} else {
			code += `return output;\n}`
		}

		return eval(code)
	}

	_extends(template, context, sections) {
		return this.compile(this.engine.resolve(template)).then(template => template(context, sections))
	}

	compileDirective(name, args) {
		if(name === 'directive') {
			// Avoid infinite loop
			return null
		}

		if(typeof this.directives[name] === 'function') {
			return this.directives[name](args)
		}

		return this[`compile${name[0].toUpperCase()}${name.substring(1)}`](args)
	}

	compileIf(condition) {
		return `if(${condition}) {`
	}

	compileElseif(condition) {
		return `} else if(${condition}) {`
	}

	compileElse() {
		return '} else {'
	}

	compileEndif() {
		return this.compileEnd()
	}

	compileFor(args) {
		return `for(${args}) {`
	}

	compileEndfor() {
		return this.compileEnd()
	}

	compileWhile(condition) {
		return `while(${condition}) {`
	}

	compileEndwhile() {
		return this.compileEnd()
	}

	compileEnd() {
		return '}'
	}

	compileSection(args) {
		if(args.indexOf(',') === -1) {
			this.sectionStack.push(args)
			return this._compileSection(args, `function() {\nlet output = '';`)
		}

		args = args.split(/,/)

		if(args.length !== 2) {
			throw new Error('Invalid section block')
		}

		return this._compileSection(args[0], `function() { return ${args[1]}; })`)
	}

	_compileSection(name, code) {
		return `(_sections[${name}] = (_sections[${name}] || [ ])).unshift(${code}\n`
	}

	/**
	 * Ends the current section and returns output
	 * @return {string} Output from the section
	 */
	compileEndsection() {
		this.sectionStack.pop()
		return 'return output;\n})'
	}

	/**
	 * Ends the current section and yields it for display
	 * @return {string} Output from the section
	 */
	compileShow() {
		const section = this.sectionStack[this.sectionStack.length - 1]
		return `${this.compileEndsection()};\n${this.compileYield(section)}`
	}

	/**
	 * Compiles the yield directive to output a section
	 *
	 * @param  {string} section Name of the section to yield
	 * @return {string}         Code to render the section
	 */
	compileYield(section) {
		return `output += _sections[${section}].length > 0 ? (_sections[${section}].pop())() : ''`
	}

	/**
	 * Renders content from the section section
	 * @return {string} Code to render the super section
	 */
	compileSuper() {
		// Due to how sections work, we can cheat by just calling yeild
		// which will pop off the next chunk of content in this section
		// and render it within ours
		return this.compileYield(this.sectionStack[this.sectionStack.length - 1])
	}

	/**
	 * Alias of compileSuper for compatibility with Blade
	 * @return {string} Code to render the super section
	 */
	compileParent() {
		return this.compileSuper()
	}

}
<commit_msg>Improved directive handling<commit_after>import { FS } from 'grind-support'
import './Template'

export class Compiler {
	engine = null
	directives = { }
	sectionStack = [ ]

	constructor(engine) {
		this.engine = engine
	}

	async compile(template) {
		let contents = (await FS.readFile(template)).toString().trim()
		let extendsLayout = null

		const expressions = [ ]
		let match = null

		// Loops through and finds all directives
		while((match = contents.match(/@([a-zA-Z0-9_]+)(\s*\()?/))) {
			if(match.index > 0) {
				expressions.push({
					type: 'string',
					contents: contents.substring(0, match.index)
				})

				contents = contents.substring(match.index)
			}

			let args = null

			if(match[2]) {
				let openCount = -1
				let index = 0
				let startIndex = 0
				let lastIndex = 0

				while(openCount !== 0 && (index = Template.nextIndexOfParenthesis(contents, index)) >= 0) {
					const parenthesis = contents.substring(index, index + 1)

					if(parenthesis === ')') {
						openCount--
					} else if(openCount === -1) {
						startIndex = index
						openCount = 1
					} else {
						openCount++
					}

					lastIndex = index
					index++
				}

				args = contents.substring(startIndex + 1, lastIndex)
				contents = contents.substring(lastIndex + 1)
			} else {
				contents = contents.substring(match[0].length)
			}

			if(match[1] === 'extends') {
				extendsLayout = args
				continue
			}

			const result = this.compileDirective(match[1].toLowerCase(), args)

			if(!result.isNil) {
				expressions.push({
					type: 'code',
					contents: result
				})
			}
		}

		contents = contents.trim()

		if(contents.length > 0) {
			expressions.push({
				type: 'string',
				contents: contents
			})
		}

		let code = ''

		for(const { type, contents } of expressions) {
			if(type === 'code') {
				code += `${contents}\n`
			} else {
				code += `output += \`${Template.sanitizeHtml(contents)}\`;\n`
			}
		}

		code = `template = function(_, _sections = { }) {\nlet output = '';\n${Template.contextualize(code)}\n`

		if(extendsLayout !== null) {
			code += `return _.$compiler._extends(${extendsLayout}, _, _sections);\n}`
		} else {
			code += `return output;\n}`
		}

		return eval(code)
	}

	_extends(template, context, sections) {
		return this.compile(this.engine.resolve(template)).then(template => template(context, sections))
	}

	compileDirective(name, args) {
		if(name === 'directive') {
			// Avoid infinite loop
			return null
		}

		if(typeof this.directives[name] === 'function') {
			return this.directives[name](args)
		}

		const method = `compile${name[0].toUpperCase()}${name.substring(1)}`

		if(typeof this[method] !== 'function') {
			throw new Error(`@${name} is not a valid Stone directive.`)
		}

		return this[method](args)
	}

	compileIf(condition) {
		return `if(${condition}) {`
	}

	compileElseif(condition) {
		return `} else if(${condition}) {`
	}

	compileElse() {
		return '} else {'
	}

	compileEndif() {
		return this.compileEnd()
	}

	compileFor(args) {
		return `for(${args}) {`
	}

	compileEndfor() {
		return this.compileEnd()
	}

	compileWhile(condition) {
		return `while(${condition}) {`
	}

	compileEndwhile() {
		return this.compileEnd()
	}

	compileEnd() {
		return '}'
	}

	compileSection(args) {
		if(args.indexOf(',') === -1) {
			this.sectionStack.push(args)
			return this._compileSection(args, `function() {\nlet output = '';`)
		}

		args = args.split(/,/)

		if(args.length !== 2) {
			throw new Error('Invalid section block')
		}

		return this._compileSection(args[0], `function() { return ${args[1]}; })`)
	}

	_compileSection(name, code) {
		return `(_sections[${name}] = (_sections[${name}] || [ ])).unshift(${code}\n`
	}

	/**
	 * Ends the current section and returns output
	 * @return {string} Output from the section
	 */
	compileEndsection() {
		this.sectionStack.pop()
		return 'return output;\n})'
	}

	/**
	 * Ends the current section and yields it for display
	 * @return {string} Output from the section
	 */
	compileShow() {
		const section = this.sectionStack[this.sectionStack.length - 1]
		return `${this.compileEndsection()};\n${this.compileYield(section)}`
	}

	/**
	 * Compiles the yield directive to output a section
	 *
	 * @param  {string} section Name of the section to yield
	 * @return {string}         Code to render the section
	 */
	compileYield(section) {
		return `output += _sections[${section}].length > 0 ? (_sections[${section}].pop())() : ''`
	}

	/**
	 * Renders content from the section section
	 * @return {string} Code to render the super section
	 */
	compileSuper() {
		// Due to how sections work, we can cheat by just calling yeild
		// which will pop off the next chunk of content in this section
		// and render it within ours
		return this.compileYield(this.sectionStack[this.sectionStack.length - 1])
	}

	/**
	 * Alias of compileSuper for compatibility with Blade
	 * @return {string} Code to render the super section
	 */
	compileParent() {
		return this.compileSuper()
	}

}
<|endoftext|>