<commit_before>'use strict';

const path = require('path');<% if ( codeCoverage ) { %>
const fs = require('fs');<% } %><% if ( bundlingTool === 'webpack' ) { %>
const nodeLibsBrowser = require('node-libs-browser');<% } %><% if ( bundlingTool === 'rollup' ) { %>
const { default: resolve } = require('@rollup/plugin-node-resolve');
const commonjs = require('@rollup/plugin-commonjs');
const nodeBuiltins = require('rollup-plugin-node-builtins');
const globals = require('rollup-plugin-node-globals');<% if ( transpile ) { %>
const { default: babel } = require('@rollup/plugin-babel');<% } %><% if ( codeCoverage ) { %>
const istanbul = require('rollup-plugin-istanbul');<% } %>
const rollupConfig = require('./rollup.config');<% } %><% if ( browserTestType === 'headless' ) { %>
const puppeteer = require('puppeteer');

process.env.CHROME_BIN = puppeteer.executablePath();<% } %>

let config;

const isCI = typeof process.env.CI !== 'undefined' && process.env.CI !== 'false';
const isPR = <% if ( ciService === 'travis' ) { %>typeof process.env.TRAVIS_PULL_REQUEST !== 'undefined' && process.env.TRAVIS_PULL_REQUEST !== 'false'<% } else { %>typeof process.env.GITHUB_HEAD_REF !== 'undefined' && process.env.GITHUB_HEAD_REF !== ''<% } %>;
const local = !isCI || (isCI && isPR);

const port = 0;

if ( local ) {
	config = {
		browsers: <% if ( browserTestType !== 'headless' ) { %>['Chrome']<% } else { %>['ChromeHeadless']<% } %>,
	};
} else {
	config = {<% if ( cloudBrowsers ) { %>
		hostname: 'bs-local.com',
		browserStack: {
			username: process.env.BROWSER_STACK_USERNAME,
			accessKey: process.env.BROWSER_STACK_ACCESS_KEY,
			startTunnel: true,
			project: '<%= moduleName %>',
			name: 'Automated (Karma)',
			build: 'Automated (Karma)'
		},<% } %>
		customLaunchers: {<% if ( cloudBrowsers ) { %><% for (browser of cloudBrowsersToTest) { %>
			'BS-<%= browser.shortName %>': <%- JSON.stringify(Object.assign({
				base: 'BrowserStack',
				project: moduleName,
				build: 'Automated (Karma)',
			}, browser.karma)) + ',' %><% } %><% } else { %>
			'Chrome-CI': {
				base: 'Chrome',
				flags: ['--no-sandbox']
			}<% } %>
		},
		browsers: <% if ( cloudBrowsers ) { %>[<%- cloudBrowsersToTest.map((browser) => `'BS-${browser.shortName}'`).join(', ') %>]<% } else { %><% if ( browserTestType !== 'headless' ) { %>[(!local ? 'Chrome-CI' : 'Chrome')]<% } else { %>['ChromeHeadless']<% } %><% } %>
	};
}

module.exports = function ( baseConfig ) {

	baseConfig.set(Object.assign({
		basePath: '',
		frameworks: ['mocha'<% if ( usesHtmlFixtures ) { %>, 'fixture'<% } %>],
		files: [
			'test/<% if ( manualTests || integrationTests ) { %>automated/<% } %>**/*.html',<% if ( bundlingTool === 'webpack' ) { %>
			'test/<% if ( manualTests || integrationTests ) { %>automated/<% } %>**/.webpack.js'<% } %><% if ( bundlingTool === 'rollup' ) { %>
			{ pattern: 'test/<% if ( manualTests || integrationTests ) { %>automated/<% } %><% if ( vanillaJsWidget ) { %>index<% } else { %>**/*<% } %>.<%= extension || 'js' %>', watched: false }<% } %>
		],
		exclude: [],
		preprocessors: {<% if ( usesHtmlFixtures ) { %>
			'test/<% if ( manualTests || integrationTests ) { %>automated/<% } %>**/*.html': ['html2js'],<% } %><% if ( bundlingTool === 'webpack' ) { %>
			'test/<% if ( manualTests || integrationTests ) { %>automated/<% } %>**/.webpack.js': ['webpack', 'sourcemap']<% } %><% if ( bundlingTool === 'rollup' ) { %>
			'test/<% if ( manualTests || integrationTests ) { %>automated/<% } %><% if ( vanillaJsWidget ) { %>index<% } else { %>**/*<% } %>.<%= extension || 'js' %>': ['rollup', 'sourcemap']<% } %>
		},
		reporters: ['mocha'<% if ( codeCoverage ) { %>, 'coverage'<% } %>],
		port: port,
		colors: true,
		logLevel: baseConfig.LOG_INFO,
		autoWatch: false,
		client: {
			captureConsole: true
		},
		browserConsoleLogOptions: {
			level: 'log',
			format: '%b %T: %m',
			terminal: true
		},<% if ( bundlingTool === 'webpack' ) { %>
		webpack: {
			mode: 'none',
			devtool: 'inline-source-map',
			resolve: {<% if ( transpile && typescript && typescriptMode === 'full' ) { %>
				extensions: ['...', '.ts'],<% } %>
				fallback: {
					assert: nodeLibsBrowser.assert
				}
			}<% if ( transpile || codeCoverage ) { %>,
			module: {
				rules: [<% if ( transpile ) { %>
					{
						test: /\.<%= extension || 'js' %>$/,
						exclude: /node_modules/,
						use: [{
							loader: 'babel-loader'
						}]
					}<% } %><% if ( transpile && codeCoverage ) { %>,<% } %><% if ( codeCoverage ) { %>
					{
						test: /\.<%= extension || 'js' %>$/,
						exclude: /(node_modules|test)/,
						enforce: 'post',
						use: [{
							loader: '@jsdevtools/coverage-istanbul-loader'<% if ( esModules ) { %>,
							options: {
								esModules: true
							}<% } %>
						}]
					}<% } %>
				]
			}<% } %>
		}<% } %><% if ( bundlingTool === 'rollup' ) { %>
		rollupPreprocessor: {
			plugins: [<% if ( codeCoverage ) { %>
				istanbul({
					exclude: ['test/<% if ( manualTests || integrationTests ) { %>automated/<% } %>**/*.<%= extension || 'js' %>', 'node_modules/**/*']
				}),<% } %>
				nodeBuiltins()<% if ( transpile ) { %>,
				babel({
					exclude: 'node_modules/**',
					babelHelpers: 'runtime'<% if ( typescript && typescriptMode === 'full' ) { %>,
					extensions: ['.js', '.ts']<% } %>
				})<% } %>,
				resolve({
					preferBuiltins: true
				}),
				commonjs(),
				babel({
					include: 'node_modules/{has-flag,supports-color}/**',
					babelHelpers: 'runtime',
					babelrc: false,
					configFile: path.resolve(__dirname, '.babelrc')
				}),
				globals(),
				...rollupConfig.plugins<% if ( transpile || esModules || typescript ) { %>.filter(({ name }) => ![<% if ( transpile ) { %>'babel'<% } %><% if ( esModules ) { %>, 'package-type'<% } %><% if ( typescript ) { %>, 'types'<% } %>].includes(name))<% } %><% if ( vanillaJsWidget || (transpile && typescript && typescriptMode === 'full') ) { %>,
				babel({
					exclude: 'node_modules/**',
					babelHelpers: 'runtime',
					extensions: ['.js'<% if (typescript && typescriptMode === 'full') { %>, '.ts'<% } %><% if (vanillaJsWidget) { %>, '.svelte'<% } %>]
				})<% if ( vanillaJsWidget ) { %>,
				babel({
					include: 'node_modules/svelte/shared.js',
					babelHelpers: 'runtime',
					babelrc: false,
					configFile: path.resolve(__dirname, '.babelrc')
				})<% } %><% } %>
			],
			output: {
				format: 'iife',
				name: '<%= camelCasedModuleName %>',
				sourcemap: baseConfig.autoWatch ? false : 'inline', // Source map support has weird behavior in watch mode
				intro: 'window.TYPED_ARRAY_SUPPORT = false;' // IE9
			}
		}<% } %>,<% if ( codeCoverage ) { %>
		coverageReporter: {
			dir: path.join(__dirname, 'coverage'),
			reporters: [{type: 'html'}, {type: 'text'}],
			check: {
				global: JSON.parse(fs.readFileSync(path.join(__dirname, '.nycrc'), 'utf8'))
			}
		},<% } %>
		singleRun: true,
		concurrency: Infinity
	}, config));

};
<commit_msg>Fix HTML fixture handling<commit_after>'use strict';

const path = require('path');<% if ( codeCoverage ) { %>
const fs = require('fs');<% } %><% if ( bundlingTool === 'webpack' ) { %>
const nodeLibsBrowser = require('node-libs-browser');<% } %><% if ( bundlingTool === 'rollup' ) { %>
const { default: resolve } = require('@rollup/plugin-node-resolve');
const commonjs = require('@rollup/plugin-commonjs');
const nodeBuiltins = require('rollup-plugin-node-builtins');
const globals = require('rollup-plugin-node-globals');<% if ( transpile ) { %>
const { default: babel } = require('@rollup/plugin-babel');<% } %><% if ( codeCoverage ) { %>
const istanbul = require('rollup-plugin-istanbul');<% } %>
const rollupConfig = require('./rollup.config');<% } %><% if ( browserTestType === 'headless' ) { %>
const puppeteer = require('puppeteer');

process.env.CHROME_BIN = puppeteer.executablePath();<% } %>

let config;

const isCI = typeof process.env.CI !== 'undefined' && process.env.CI !== 'false';
const isPR = <% if ( ciService === 'travis' ) { %>typeof process.env.TRAVIS_PULL_REQUEST !== 'undefined' && process.env.TRAVIS_PULL_REQUEST !== 'false'<% } else { %>typeof process.env.GITHUB_HEAD_REF !== 'undefined' && process.env.GITHUB_HEAD_REF !== ''<% } %>;
const local = !isCI || (isCI && isPR);

const port = 0;

if ( local ) {
	config = {
		browsers: <% if ( browserTestType !== 'headless' ) { %>['Chrome']<% } else { %>['ChromeHeadless']<% } %>,
	};
} else {
	config = {<% if ( cloudBrowsers ) { %>
		hostname: 'bs-local.com',
		browserStack: {
			username: process.env.BROWSER_STACK_USERNAME,
			accessKey: process.env.BROWSER_STACK_ACCESS_KEY,
			startTunnel: true,
			project: '<%= moduleName %>',
			name: 'Automated (Karma)',
			build: 'Automated (Karma)'
		},<% } %>
		customLaunchers: {<% if ( cloudBrowsers ) { %><% for (browser of cloudBrowsersToTest) { %>
			'BS-<%= browser.shortName %>': <%- JSON.stringify(Object.assign({
				base: 'BrowserStack',
				project: moduleName,
				build: 'Automated (Karma)',
			}, browser.karma)) + ',' %><% } %><% } else { %>
			'Chrome-CI': {
				base: 'Chrome',
				flags: ['--no-sandbox']
			}<% } %>
		},
		browsers: <% if ( cloudBrowsers ) { %>[<%- cloudBrowsersToTest.map((browser) => `'BS-${browser.shortName}'`).join(', ') %>]<% } else { %><% if ( browserTestType !== 'headless' ) { %>[(!local ? 'Chrome-CI' : 'Chrome')]<% } else { %>['ChromeHeadless']<% } %><% } %>
	};
}

module.exports = function ( baseConfig ) {

	baseConfig.set(Object.assign({
		basePath: '',
		frameworks: ['mocha'<% if ( usesHtmlFixtures ) { %>, 'fixture'<% } %>],
		files: [<% if ( usesHtmlFixtures ) { %>
			'test/<% if ( manualTests || integrationTests ) { %>automated/<% } %>**/*.html',<% } %><% if ( bundlingTool === 'webpack' ) { %>
			'test/<% if ( manualTests || integrationTests ) { %>automated/<% } %>**/.webpack.js'<% } %><% if ( bundlingTool === 'rollup' ) { %>
			{ pattern: 'test/<% if ( manualTests || integrationTests ) { %>automated/<% } %><% if ( vanillaJsWidget ) { %>index<% } else { %>**/*<% } %>.<%= extension || 'js' %>', watched: false }<% } %>
		],
		exclude: [],
		preprocessors: {<% if ( usesHtmlFixtures ) { %>
			'test/<% if ( manualTests || integrationTests ) { %>automated/<% } %>**/*.html': ['html2js'],<% } %><% if ( bundlingTool === 'webpack' ) { %>
			'test/<% if ( manualTests || integrationTests ) { %>automated/<% } %>**/.webpack.js': ['webpack', 'sourcemap']<% } %><% if ( bundlingTool === 'rollup' ) { %>
			'test/<% if ( manualTests || integrationTests ) { %>automated/<% } %><% if ( vanillaJsWidget ) { %>index<% } else { %>**/*<% } %>.<%= extension || 'js' %>': ['rollup', 'sourcemap']<% } %>
		},
		reporters: ['mocha'<% if ( codeCoverage ) { %>, 'coverage'<% } %>],
		port: port,
		colors: true,
		logLevel: baseConfig.LOG_INFO,
		autoWatch: false,
		client: {
			captureConsole: true
		},
		browserConsoleLogOptions: {
			level: 'log',
			format: '%b %T: %m',
			terminal: true
		},<% if ( bundlingTool === 'webpack' ) { %>
		webpack: {
			mode: 'none',
			devtool: 'inline-source-map',
			resolve: {<% if ( transpile && typescript && typescriptMode === 'full' ) { %>
				extensions: ['...', '.ts'],<% } %>
				fallback: {
					assert: nodeLibsBrowser.assert
				}
			}<% if ( transpile || codeCoverage ) { %>,
			module: {
				rules: [<% if ( transpile ) { %>
					{
						test: /\.<%= extension || 'js' %>$/,
						exclude: /node_modules/,
						use: [{
							loader: 'babel-loader'
						}]
					}<% } %><% if ( transpile && codeCoverage ) { %>,<% } %><% if ( codeCoverage ) { %>
					{
						test: /\.<%= extension || 'js' %>$/,
						exclude: /(node_modules|test)/,
						enforce: 'post',
						use: [{
							loader: '@jsdevtools/coverage-istanbul-loader'<% if ( esModules ) { %>,
							options: {
								esModules: true
							}<% } %>
						}]
					}<% } %>
				]
			}<% } %>
		}<% } %><% if ( bundlingTool === 'rollup' ) { %>
		rollupPreprocessor: {
			plugins: [<% if ( codeCoverage ) { %>
				istanbul({
					exclude: ['test/<% if ( manualTests || integrationTests ) { %>automated/<% } %>**/*.<%= extension || 'js' %>', 'node_modules/**/*']
				}),<% } %>
				nodeBuiltins()<% if ( transpile ) { %>,
				babel({
					exclude: 'node_modules/**',
					babelHelpers: 'runtime'<% if ( typescript && typescriptMode === 'full' ) { %>,
					extensions: ['.js', '.ts']<% } %>
				})<% } %>,
				resolve({
					preferBuiltins: true
				}),
				commonjs(),
				babel({
					include: 'node_modules/{has-flag,supports-color}/**',
					babelHelpers: 'runtime',
					babelrc: false,
					configFile: path.resolve(__dirname, '.babelrc')
				}),
				globals(),
				...rollupConfig.plugins<% if ( transpile || esModules || typescript ) { %>.filter(({ name }) => ![<% if ( transpile ) { %>'babel'<% } %><% if ( esModules ) { %>, 'package-type'<% } %><% if ( typescript ) { %>, 'types'<% } %>].includes(name))<% } %><% if ( vanillaJsWidget || (transpile && typescript && typescriptMode === 'full') ) { %>,
				babel({
					exclude: 'node_modules/**',
					babelHelpers: 'runtime',
					extensions: ['.js'<% if (typescript && typescriptMode === 'full') { %>, '.ts'<% } %><% if (vanillaJsWidget) { %>, '.svelte'<% } %>]
				})<% if ( vanillaJsWidget ) { %>,
				babel({
					include: 'node_modules/svelte/shared.js',
					babelHelpers: 'runtime',
					babelrc: false,
					configFile: path.resolve(__dirname, '.babelrc')
				})<% } %><% } %>
			],
			output: {
				format: 'iife',
				name: '<%= camelCasedModuleName %>',
				sourcemap: baseConfig.autoWatch ? false : 'inline', // Source map support has weird behavior in watch mode
				intro: 'window.TYPED_ARRAY_SUPPORT = false;' // IE9
			}
		}<% } %>,<% if ( codeCoverage ) { %>
		coverageReporter: {
			dir: path.join(__dirname, 'coverage'),
			reporters: [{type: 'html'}, {type: 'text'}],
			check: {
				global: JSON.parse(fs.readFileSync(path.join(__dirname, '.nycrc'), 'utf8'))
			}
		},<% } %>
		singleRun: true,
		concurrency: Infinity
	}, config));

};
<|endoftext|>


<commit_before>'use strict';

/**
 * The editor for schema resources
 */
class SchemaEditor extends HashBrown.Entity.View.ResourceEditor.ResourceEditorBase {
    static get category() { return 'schemas'; }
    static get itemType() { return HashBrown.Entity.Resource.Schema.SchemaBase; }

    /**
     * Constructor
     */
    constructor(params) {
        super(params);

        this.template = require('template/resourceEditor/schemaEditor.js');
    }

    /**
     * Fetches the model
     */
    async fetch() {
        if(this.state.id) {
            this.model = await HashBrown.Service.SchemaService.getSchemaById(this.state.id);
            this.state.compiledSchema = await HashBrown.Service.SchemaService.getSchemaById(this.model.id, true);
        }
    }

    /**
     * Pre render
     */
    prerender() {
        this.state.title = this.model.name;
        this.state.icon = this.model.icon;

        if(this.model instanceof HashBrown.Entity.Resource.Schema.ContentSchema) {
            this.state.tab = this.state.tab || 'content';
            this.state.properties = {};
            this.state.parentTabs = {};
            this.state.tabOptions = {};

            // Build parent tabs and tab options
            for(let tabId in this.state.compiledSchema.tabs) {
                this.state.tabOptions[this.state.compiledSchema.tabs[tabId]] = tabId;

                if(this.model.tabs[tabId]) { continue; }

                this.state.parentTabs[tabId] = this.state.compiledSchema.tabs[tabId];
            }

            for(let tabId in this.model.tabs) {
                this.state.tabOptions[this.model.tabs[tabId]] = tabId;
            }

            if(!this.model.fields || !this.model.fields.properties) { return; }

            for(let key in this.model.fields.properties) {
                let definition = this.model.fields.properties[key];

                if(!definition.tabId) { definition.tabId = 'content'; }

                if(definition.tabId !== this.state.tab) { continue; }

                this.state.properties[key] = definition.label;
            }

        } else if(this.model instanceof HashBrown.Entity.Resource.Schema.FieldSchema) {
            this.state.fieldConfigEditor = null;

            let fieldType = HashBrown.Entity.View.Field[this.model.editorId];

            if(fieldType) {
                this.state.fieldConfigEditor = new fieldType({
                    model: this.model,
                    state: { name: 'config' }
                });

                this.state.fieldConfigEditor.on('change', (newValue) => {
                    this.model.config = newValue;

                    this.trigger('change', this.model);
                });
            }
        }
    }

    /**
     * Event: Change id
     */
    onChangeId(id) {
        this.model.id = id;

        this.trigger('change', this.model);
    }

    /**
     * Event: Change name
     */
    onChangeName(name) {
        this.model.name = name;

        this.trigger('change', this.model);
    }

    /**
     * Event: Click change icon
     */
    onClickChangeIcon() {
        let modal = new HashBrown.Entity.View.Modal.PickIcon();

        modal.on('change', (newIcon) => {
            this.model.icon = newIcon;

            this.trigger('change', this.model);
            this.render();
        });
    }

    /**
     * Event: Click edit field
     *
     * @param {String} key
     */
    onClickEditField(key) {
        if(!this.model.fields) { this.model.fields = {}; }
        if(!this.model.fields.properties) { this.model.fields.properties = {}; }
        if(!this.model.fields.properties[key]) { this.model.fields.properties[key] = { tabId: this.state.tab, schemaId: 'string' }; }

        let modal = new HashBrown.Entity.View.Modal.EditField({
            model: {
                tabOptions: this.state.tabOptions,
                key: key,
                definition: this.model.fields.properties[key]
            }
        });

        modal.on('changekey', (newKey) => {
            this.onChangeFieldKey(key, newKey);

            key = newKey;

            this.render();
        });

        modal.on('change', (newValue) => {
            this.onChangeFieldDefinition(key, newValue);

            this.render();
        });
    }

    /**
     * Event: Click start tour
     */
    onClickStartTour() {
        HashBrown.Service.SchemaService.startTour();
    }

    /**
     * Event: Change field key
     */
    onChangeFieldKey(oldKey, newKey) {
        let keys = Object.keys(this.model.fields.properties);

        let newFields = {};

        for(let key of keys) {
            let value = this.model.fields.properties[key];

            if(key === oldKey) { key = newKey; }

            newFields[key] = value;
        }

        this.model.fields.properties = newFields;

        this.trigger('change', this.model);
    }

    /**
     * Event: Change field definition
     */
    onChangeFieldDefinition(key, newValue) {
        if(!this.model.fields) { this.model.fields = {}; }
        if(!this.model.fields.properties) { this.model.fields.properties = {}; }
        if(!this.model.fields.properties[key]) { this.model.fields.properties[key] = { schemaId: 'string' }; }

        this.model.fields.properties[key] = newValue;

        this.trigger('change', this.model);
    }

    /**
     * Event: Change field sorting
     */
    onChangeFieldSorting(fields) {
        let newFields = {};

        // Add fields from list widget
        for(let key in fields) {
            let definition = this.model.fields.properties[key];

            newFields[key] = definition || { tabId: this.state.tab, label: fields[key] };
        }

        // Add back remaining fields not in the current view
        for(let key in this.model.fields.properties) {
            let definition = this.model.fields.properties[key];

            if(definition.tabId === this.state.tab) { continue; }

            newFields[key] = definition;
        }

        let isNewField = Object.keys(fields).length > Object.keys(this.model.fields.properties).length;

        this.model.fields.properties = newFields;

        if(isNewField) {
            this.onClickEditField(Object.keys(fields).pop());
        }

        this.trigger('change', this.model);
    }

    /**
     * Event: Change tabs
     */
    onChangeTabs(tabs) {
        this.model.tabs = tabs;

        this.render();
    }

    /**
     * Event: Switch tab
     */
    onSwitchTab(tab) {
        this.state.tab = tab;

        this.render();
    }

    /**
     * Event: Click save
     */
    async onClickSave() {
        await HashBrown.Service.SchemaService.setSchemaById(this.model.id, this.model);

        UI.notifySmall(`"${this.state.title}" saved successfully`, null, 3);
    }

    /**
     * Event: Click import
     */
    onClickImport() {
        let modal = UI.prompt(
            'Import schemas',
            'URL to uischema.org definitions',
            'text',
            'https://uischema.org/schemas.json',
            async (url) => {
                if(!url) { throw new Error('Please specify a URL'); }

                await HashBrown.Service.RequestService.request('post', 'schemas/import?url=' + url);

                HashBrown.Service.EventService.trigger('resource');
            }
        );
    }
}

module.exports = SchemaEditor;
<commit_msg>Cleanup<commit_after>'use strict';

/**
 * The editor for schema resources
 */
class SchemaEditor extends HashBrown.Entity.View.ResourceEditor.ResourceEditorBase {
    static get category() { return 'schemas'; }
    static get itemType() { return HashBrown.Entity.Resource.Schema.SchemaBase; }

    /**
     * Constructor
     */
    constructor(params) {
        super(params);

        this.template = require('template/resourceEditor/schemaEditor.js');
    }

    /**
     * Fetches the model
     */
    async fetch() {
        if(this.state.id) {
            this.model = await HashBrown.Service.SchemaService.getSchemaById(this.state.id);
            this.state.compiledSchema = await HashBrown.Service.SchemaService.getSchemaById(this.model.id, true);
        }
    }

    /**
     * Pre render
     */
    prerender() {
        this.state.title = this.model.name;
        this.state.icon = this.model.icon;

        if(this.model instanceof HashBrown.Entity.Resource.Schema.ContentSchema) {
            this.state.tab = this.state.tab || 'content';
            this.state.properties = {};
            this.state.parentTabs = {};
            this.state.tabOptions = {};

            // Build parent tabs and tab options
            for(let tabId in this.state.compiledSchema.tabs) {
                this.state.tabOptions[this.state.compiledSchema.tabs[tabId]] = tabId;

                if(this.model.tabs[tabId]) { continue; }

                this.state.parentTabs[tabId] = this.state.compiledSchema.tabs[tabId];
            }

            for(let tabId in this.model.tabs) {
                this.state.tabOptions[this.model.tabs[tabId]] = tabId;
            }

            if(!this.model.fields || !this.model.fields.properties) { return; }

            for(let key in this.model.fields.properties) {
                let definition = this.model.fields.properties[key];

                if(!definition.tabId) { definition.tabId = 'content'; }

                if(definition.tabId !== this.state.tab) { continue; }

                this.state.properties[key] = definition.label;
            }

        } else if(this.model instanceof HashBrown.Entity.Resource.Schema.FieldSchema) {
            this.state.fieldConfigEditor = null;

            let fieldType = HashBrown.Entity.View.Field[this.model.editorId];

            if(fieldType) {
                this.state.fieldConfigEditor = new fieldType({
                    model: this.model,
                    state: { name: 'config' }
                });

                this.state.fieldConfigEditor.on('change', (newValue) => {
                    this.model.config = newValue;

                    this.trigger('change', this.model);
                });
            }
        }
    }

    /**
     * Event: Change id
     */
    onChangeId(id) {
        this.model.id = id;

        this.trigger('change', this.model);
    }

    /**
     * Event: Change name
     */
    onChangeName(name) {
        this.model.name = name;

        this.trigger('change', this.model);
    }

    /**
     * Event: Click change icon
     */
    onClickChangeIcon() {
        let modal = new HashBrown.Entity.View.Modal.PickIcon();

        modal.on('change', (newIcon) => {
            this.model.icon = newIcon;

            this.trigger('change', this.model);
            this.render();
        });
    }

    /**
     * Event: Click edit field
     *
     * @param {String} key
     */
    onClickEditField(key) {
        if(!this.model.fields) { this.model.fields = {}; }
        if(!this.model.fields.properties) { this.model.fields.properties = {}; }
        if(!this.model.fields.properties[key]) { this.model.fields.properties[key] = { tabId: this.state.tab, schemaId: 'string' }; }

        let modal = new HashBrown.Entity.View.Modal.EditField({
            model: {
                tabOptions: this.state.tabOptions,
                key: key,
                definition: this.model.fields.properties[key]
            }
        });

        modal.on('changekey', (newKey) => {
            this.onChangeFieldKey(key, newKey);

            key = newKey;

            this.render();
        });

        modal.on('change', (newValue) => {
            this.onChangeFieldDefinition(key, newValue);

            this.render();
        });
    }

    /**
     * Event: Click start tour
     */
    onClickStartTour() {
        HashBrown.Service.SchemaService.startTour();
    }

    /**
     * Event: Change field key
     */
    onChangeFieldKey(oldKey, newKey) {
        let keys = Object.keys(this.model.fields.properties);

        let newFields = {};

        for(let key of keys) {
            let value = this.model.fields.properties[key];

            if(key === oldKey) { key = newKey; }

            newFields[key] = value;
        }

        this.model.fields.properties = newFields;

        this.trigger('change', this.model);
    }

    /**
     * Event: Change field definition
     */
    onChangeFieldDefinition(key, newValue) {
        if(!this.model.fields) { this.model.fields = {}; }
        if(!this.model.fields.properties) { this.model.fields.properties = {}; }
        if(!this.model.fields.properties[key]) { this.model.fields.properties[key] = { schemaId: 'string' }; }

        this.model.fields.properties[key] = newValue;

        this.trigger('change', this.model);
    }

    /**
     * Event: Change field sorting
     */
    onChangeFieldSorting(fields) {
        let newFields = {};

        // Add fields from list widget
        for(let key in fields) {
            let definition = this.model.fields.properties[key];

            newFields[key] = definition || { tabId: this.state.tab, label: fields[key] };
        }

        // Add back remaining fields not in the current view
        for(let key in this.model.fields.properties) {
            let definition = this.model.fields.properties[key];

            if(definition.tabId === this.state.tab) { continue; }

            newFields[key] = definition;
        }

        let isNewField = Object.keys(fields).length > Object.keys(this.model.fields.properties).length;

        this.model.fields.properties = newFields;

        if(isNewField) {
            this.onClickEditField(Object.keys(fields).pop());
        }

        this.trigger('change', this.model);
    }

    /**
     * Event: Change tabs
     */
    onChangeTabs(tabs) {
        this.model.tabs = tabs;

        this.render();
    }

    /**
     * Event: Switch tab
     */
    onSwitchTab(tab) {
        this.state.tab = tab;

        this.render();
    }

    /**
     * Event: Click save
     */
    async onClickSave() {
        await HashBrown.Service.SchemaService.setSchemaById(this.model.id, this.model);

        UI.notifySmall(`"${this.state.title}" saved successfully`, null, 3);
    }

    /**
     * Event: Click import
     */
    onClickImport() {
        let modal = UI.prompt(
            'Import schemas',
            'URL to uischema.org definitions',
            'text',
            'https://uischema.org/schemas.json',
            async (url) => {
                if(!url) { UI.notify('Missing URL', 'Please specify a URL'); }

                await HashBrown.Service.RequestService.request('post', 'schemas/import?url=' + url);

                HashBrown.Service.EventService.trigger('resource');

                UI.notifySmall('Schemas imported successfully from ' + url, null, 3);
            }
        );
    }
}

module.exports = SchemaEditor;
<|endoftext|>


<commit_before>wax = wax || {};

wax.interaction = function() {
    var gm = wax.gm(),
        interaction = {},
        _downLock = false,
        _clickTimeout = null,
        // Active feature
        // Down event
        _d,
        // Touch tolerance
        tol = 4,
        grid,
        attach,
        detach,
        parent,
        map,
        tileGrid;

    var defaultEvents = {
        mousemove: onMove,
        touchstart: onDown,
        mousedown: onDown
    };

    var touchEnds = {
        touchend: onUp,
        touchmove: onUp,
        touchcancel: touchCancel
    };

    var pointerEnds = {
        MSPointerUp: onUp,
        MSPointerMove: onUp,
        MSPointerCancel: touchCancel
    };

    // Abstract getTile method. Depends on a tilegrid with
    // grid[ [x, y, tile] ] structure.
    function getTile(e) {
        var g = grid();
        var regExp = new RegExp(gm.tileRegexp());
        for (var i = 0; i < g.length; i++) {
            if (e) {
                var isInside = ((g[i][0] <= e.y) &&
                     ((g[i][0] + 256) > e.y) &&
                      (g[i][1] <= e.x) &&
                     ((g[i][1] + 256) > e.x));
                if(isInside && regExp.exec(g[i][2].src)) {
                    return g[i][2];
                }
            }
        }
        return false;
    }

    // Clear the double-click timeout to prevent double-clicks from
    // triggering popups.
    function killTimeout() {
        if (_clickTimeout) {
            window.clearTimeout(_clickTimeout);
            _clickTimeout = null;
            return true;
        } else {
            return false;
        }
    }

    function onMove(e) {
        // If the user is actually dragging the map, exit early
        // to avoid performance hits.
        if (_downLock) return;

        var _e = (e.type != "MSPointerMove" ? e : e.originalEvent);
        var pos = wax.u.eventoffset(_e);

        interaction.screen_feature(pos, function(feature) {
            if (feature) {
                bean.fire(interaction, 'on', {
                    parent: parent(),
                    data: feature,
                    formatter: gm.formatter().format,
                    e: e
                });
            } else {
                bean.fire(interaction, 'off');
            }
        });
    }

    // A handler for 'down' events - which means `mousedown` and `touchstart`
    function onDown(e) {

        // Prevent interaction offset calculations happening while
        // the user is dragging the map.
        //
        // Store this event so that we can compare it to the
        // up event
        _downLock = true;
        var _e = (e.type != "MSPointerDown" ? e : e.originalEvent);
        _d = wax.u.eventoffset(_e);
        if (e.type === 'mousedown') {
            bean.add(document.body, 'click', onUp);
            // track mouse up to remove lockDown when the drags end
            bean.add(document.body, 'mouseup', dragEnd);

        // Only track single-touches. Double-touches will not affect this
        // control
        } else if (e.type === 'touchstart' && e.touches.length === 1) {
            // Don't make the user click close if they hit another tooltip
            bean.fire(interaction, 'off');
            // Touch moves invalidate touches
            bean.add(parent(), touchEnds);
        } else if (e.originalEvent.type === "MSPointerDown" && e.originalEvent.touches && e.originalEvent.touches.length === 1) {
          // Don't make the user click close if they hit another tooltip
            bean.fire(interaction, 'off');
            // Touch moves invalidate touches
            bean.add(parent(), pointerEnds);
        }

    }

    function dragEnd() {
        _downLock = false;
    }

    function touchCancel() {
        bean.remove(parent(), touchEnds);
        bean.remove(parent(), pointerEnds);
        _downLock = false;
    }

    function onUp(e) {
        var evt = {},
            _e = (e.type != "MSPointerMove" && e.type != "MSPointerUp" ? e : e.originalEvent),
            pos = wax.u.eventoffset(_e);
        _downLock = false;

        for (var key in _e) {
          evt[key] = _e[key];
        }

        // for (var key in e) {
        //   evt[key] = e[key];
        // }

        evt.changedTouches = [];

        bean.remove(document.body, 'mouseup', onUp);
        bean.remove(parent(), touchEnds);
        bean.remove(parent(), pointerEnds);

        if (e.type === 'touchend') {
            // If this was a touch and it survived, there's no need to avoid a double-tap
            // but also wax.u.eventoffset will have failed, since this touch
            // event doesn't have coordinates
            interaction.click(e, _d);
        } else if (pos) {
          // If pos is not defined means wax can't calculate event position,
          // So next cases aren't possible.

          if (evt.type === "MSPointerMove" || evt.type === "MSPointerUp") {
            interaction.click(evt, pos);
          } else if (Math.round(pos.y / tol) === Math.round(_d.y / tol) &&
            Math.round(pos.x / tol) === Math.round(_d.x / tol)) {
            // Contain the event data in a closure.
            // Ignore double-clicks by ignoring clicks within 300ms of
            // each other.
            if(!_clickTimeout) {
              _clickTimeout = window.setTimeout(function() {
                  _clickTimeout = null;
                  interaction.click(evt, pos);
              }, 150);
            } else {
              killTimeout();
            }
          }

        }

        return onUp;
    }

    // Handle a click event. Takes a second
    interaction.click = function(e, pos) {
        interaction.screen_feature(pos, function(feature) {
            if (feature) bean.fire(interaction, 'on', {
                parent: parent(),
                data: feature,
                formatter: gm.formatter().format,
                e: e
            });
        });
    };

    interaction.screen_feature = function(pos, callback) {
        var tile = getTile(pos);
        if (!tile) callback(null);
        gm.getGrid(tile.src, function(err, g) {
            if (err || !g) return callback(null);
            var feature = g.tileFeature(pos.x, pos.y, tile);
            callback(feature);
        });
    };

    // set an attach function that should be
    // called when maps are set
    interaction.attach = function(x) {
        if (!arguments.length) return attach;
        attach = x;
        return interaction;
    };

    interaction.detach = function(x) {
        if (!arguments.length) return detach;
        detach = x;
        return interaction;
    };

    // Attach listeners to the map
    interaction.map = function(x) {
        if (!arguments.length) return map;
        map = x;
        if (attach) attach(map);
        bean.add(parent(), defaultEvents);
        bean.add(parent(), 'touchstart', onDown);
        bean.add(parent(), 'MSPointerDown', onDown);
        return interaction;
    };

    // set a grid getter for this control
    interaction.grid = function(x) {
        if (!arguments.length) return grid;
        grid = x;
        return interaction;
    };

    // detach this and its events from the map cleanly
    interaction.remove = function(x) {
        if (detach) detach(map);
        bean.remove(parent(), defaultEvents);
        bean.fire(interaction, 'remove');
        return interaction;
    };

    // get or set a tilejson chunk of json
    interaction.tilejson = function(x) {
        if (!arguments.length) return gm.tilejson();
        gm.tilejson(x);
        return interaction;
    };

    // return the formatter, which has an exposed .format
    // function
    interaction.formatter = function() {
        return gm.formatter();
    };

    // ev can be 'on', 'off', fn is the handler
    interaction.on = function(ev, fn) {
        bean.add(interaction, ev, fn);
        return interaction;
    };

    // ev can be 'on', 'off', fn is the handler
    interaction.off = function(ev, fn) {
        bean.remove(interaction, ev, fn);
        return interaction;
    };

    // Return or set the gridmanager implementation
    interaction.gridmanager = function(x) {
        if (!arguments.length) return gm;
        gm = x;
        return interaction;
    };

    // parent should be a function that returns
    // the parent element of the map
    interaction.parent  = function(x) {
        parent = x;
        return interaction;
    };

    return interaction;
};
<commit_msg>Fixing IE11 bug when user click outside map - issue 205 in cartodb.js<commit_after>wax = wax || {};

wax.interaction = function() {
    var gm = wax.gm(),
        interaction = {},
        _downLock = false,
        _clickTimeout = null,
        // Active feature
        // Down event
        _d,
        // Touch tolerance
        tol = 4,
        grid,
        attach,
        detach,
        parent,
        map,
        tileGrid;

    var defaultEvents = {
        mousemove: onMove,
        touchstart: onDown,
        mousedown: onDown
    };

    var touchEnds = {
        touchend: onUp,
        touchmove: onUp,
        touchcancel: touchCancel
    };

    var pointerEnds = {
        MSPointerUp: onUp,
        MSPointerMove: onUp,
        MSPointerCancel: touchCancel
    };

    // Abstract getTile method. Depends on a tilegrid with
    // grid[ [x, y, tile] ] structure.
    function getTile(e) {
        var g = grid();
        var regExp = new RegExp(gm.tileRegexp());
        for (var i = 0; i < g.length; i++) {
            if (e) {
                var isInside = ((g[i][0] <= e.y) &&
                     ((g[i][0] + 256) > e.y) &&
                      (g[i][1] <= e.x) &&
                     ((g[i][1] + 256) > e.x));
                if(isInside && regExp.exec(g[i][2].src)) {
                    return g[i][2];
                }
            }
        }
        return false;
    }

    // Clear the double-click timeout to prevent double-clicks from
    // triggering popups.
    function killTimeout() {
        if (_clickTimeout) {
            window.clearTimeout(_clickTimeout);
            _clickTimeout = null;
            return true;
        } else {
            return false;
        }
    }

    function onMove(e) {
        // If the user is actually dragging the map, exit early
        // to avoid performance hits.
        if (_downLock) return;

        var _e = (e.type != "MSPointerMove" ? e : e.originalEvent);
        var pos = wax.u.eventoffset(_e);

        interaction.screen_feature(pos, function(feature) {
            if (feature) {
                bean.fire(interaction, 'on', {
                    parent: parent(),
                    data: feature,
                    formatter: gm.formatter().format,
                    e: e
                });
            } else {
                bean.fire(interaction, 'off');
            }
        });
    }

    // A handler for 'down' events - which means `mousedown` and `touchstart`
    function onDown(e) {

        // Prevent interaction offset calculations happening while
        // the user is dragging the map.
        //
        // Store this event so that we can compare it to the
        // up event
        _downLock = true;
        var _e = (e.type != "MSPointerDown" ? e : e.originalEvent);
        _d = wax.u.eventoffset(_e);
        if (e.type === 'mousedown') {
            bean.add(document.body, 'click', onUp);
            // track mouse up to remove lockDown when the drags end
            bean.add(document.body, 'mouseup', dragEnd);

        // Only track single-touches. Double-touches will not affect this
        // control
        } else if (e.type === 'touchstart' && e.touches.length === 1) {
            // Don't make the user click close if they hit another tooltip
            bean.fire(interaction, 'off');
            // Touch moves invalidate touches
            bean.add(parent(), touchEnds);
        } else if (e.originalEvent.type === "MSPointerDown" && e.originalEvent.touches && e.originalEvent.touches.length === 1) {
          // Don't make the user click close if they hit another tooltip
            bean.fire(interaction, 'off');
            // Touch moves invalidate touches
            bean.add(parent(), pointerEnds);
        }

    }

    function dragEnd() {
        _downLock = false;
    }

    function touchCancel() {
        bean.remove(parent(), touchEnds);
        bean.remove(parent(), pointerEnds);
        _downLock = false;
    }

    function onUp(e) {
        var evt = {},
            _e = (e.type != "MSPointerMove" && e.type != "MSPointerUp" ? e : e.originalEvent),
            pos = wax.u.eventoffset(_e);
        _downLock = false;

        for (var key in _e) {
          evt[key] = _e[key];
        }

        // for (var key in e) {
        //   evt[key] = e[key];
        // }

        evt.changedTouches = [];

        bean.remove(document.body, 'mouseup', onUp);
        bean.remove(parent(), touchEnds);
        bean.remove(parent(), pointerEnds);

        if (e.type === 'touchend') {
            // If this was a touch and it survived, there's no need to avoid a double-tap
            // but also wax.u.eventoffset will have failed, since this touch
            // event doesn't have coordinates
            interaction.click(e, _d);
        } else if (pos && _d) {
          // If pos is not defined means wax can't calculate event position,
          // So next cases aren't possible.

          if (evt.type === "MSPointerMove" || evt.type === "MSPointerUp") {
            interaction.click(evt, pos);
          } else if (Math.round(pos.y / tol) === Math.round(_d.y / tol) &&
            Math.round(pos.x / tol) === Math.round(_d.x / tol)) {
            // Contain the event data in a closure.
            // Ignore double-clicks by ignoring clicks within 300ms of
            // each other.
            if(!_clickTimeout) {
              _clickTimeout = window.setTimeout(function() {
                  _clickTimeout = null;
                  interaction.click(evt, pos);
              }, 150);
            } else {
              killTimeout();
            }
          }

        }

        return onUp;
    }

    // Handle a click event. Takes a second
    interaction.click = function(e, pos) {
        interaction.screen_feature(pos, function(feature) {
            if (feature) bean.fire(interaction, 'on', {
                parent: parent(),
                data: feature,
                formatter: gm.formatter().format,
                e: e
            });
        });
    };

    interaction.screen_feature = function(pos, callback) {
        var tile = getTile(pos);
        if (!tile) callback(null);
        gm.getGrid(tile.src, function(err, g) {
            if (err || !g) return callback(null);
            var feature = g.tileFeature(pos.x, pos.y, tile);
            callback(feature);
        });
    };

    // set an attach function that should be
    // called when maps are set
    interaction.attach = function(x) {
        if (!arguments.length) return attach;
        attach = x;
        return interaction;
    };

    interaction.detach = function(x) {
        if (!arguments.length) return detach;
        detach = x;
        return interaction;
    };

    // Attach listeners to the map
    interaction.map = function(x) {
        if (!arguments.length) return map;
        map = x;
        if (attach) attach(map);
        bean.add(parent(), defaultEvents);
        bean.add(parent(), 'touchstart', onDown);
        bean.add(parent(), 'MSPointerDown', onDown);
        return interaction;
    };

    // set a grid getter for this control
    interaction.grid = function(x) {
        if (!arguments.length) return grid;
        grid = x;
        return interaction;
    };

    // detach this and its events from the map cleanly
    interaction.remove = function(x) {
        if (detach) detach(map);
        bean.remove(parent(), defaultEvents);
        bean.fire(interaction, 'remove');
        return interaction;
    };

    // get or set a tilejson chunk of json
    interaction.tilejson = function(x) {
        if (!arguments.length) return gm.tilejson();
        gm.tilejson(x);
        return interaction;
    };

    // return the formatter, which has an exposed .format
    // function
    interaction.formatter = function() {
        return gm.formatter();
    };

    // ev can be 'on', 'off', fn is the handler
    interaction.on = function(ev, fn) {
        bean.add(interaction, ev, fn);
        return interaction;
    };

    // ev can be 'on', 'off', fn is the handler
    interaction.off = function(ev, fn) {
        bean.remove(interaction, ev, fn);
        return interaction;
    };

    // Return or set the gridmanager implementation
    interaction.gridmanager = function(x) {
        if (!arguments.length) return gm;
        gm = x;
        return interaction;
    };

    // parent should be a function that returns
    // the parent element of the map
    interaction.parent  = function(x) {
        parent = x;
        return interaction;
    };

    return interaction;
};
<|endoftext|>


<commit_before>//- JavaScript source code

//- custom.js ~~
//                                                      ~~ (c) SRW, 09 Dec 2013
//                                                  ~~ last updated 01 Apr 2014

(function () {
    'use strict';

 // Pragmas

    /*jslint indent: 4, maxlen: 80, node: true */

    /*properties
        cf_connecting_ip, 'cf-connecting-ip', cf_ipcountry, 'cf-ipcountry',
        cf_ray, 'cf-ray', cf_visitor, 'cf-visitor', connection, content_length,
        'content-length', dnt, env, hasOwnProperty, headers, host, ip, log,
        method, origin, parse, referer, remoteAddress, replace, split,
        timestamp, url, x_forwarded_for, 'x-forwarded-for', x_forwarded_port,
        'x-forwarded-port', x_forwarded_proto, 'x-forwarded-proto',
        x_request_start, 'x-request-start', x_wap_profile, 'x-wap-profile'
    */

 // Module definitions

    exports.log = function (request) {
     // This is a custom logging function that executes once for every request
     // if logging is enabled. This function is optional, however, because
     // QMachine provides a default logging function. I have used a convention
     // in which hyphens are converted into underscores in order to prevent
     // inconveniences with certain tools that attempt to auto-detect schemas
     // from JSON. Also, I have commented out various headers that I don't
     // personally find useful. Ironically, the "Do Not Track" header (DNT) is
     // logged alongside the tracking data because I don't understand the legal
     // stuff yet, but I will delete all personal data from entries that opted
     // out once I figure out which data are considered "personal".
        var headers, y;
        headers = request.headers;
        y = {
            host: headers.host,
            method: request.method,
            timestamp: new Date(),
            url: request.url
        };
        if (headers.hasOwnProperty('cf-connecting-ip')) {
         // See http://goo.gl/81sHn8.
            y.cf_connecting_ip = headers['cf-connecting-ip'];
        }
        if (headers.hasOwnProperty('cf-ipcountry')) {
         // See http://goo.gl/QpDIBe.
            y.cf_ipcountry = headers['cf-ipcountry'];
        }
     /*
        if (headers.hasOwnProperty('cf-ray')) {
         // See http://goo.gl/wMfrD8.
            y.cf_ray = headers['cf-ray'];
        }
        if (headers.hasOwnProperty('cf-visitor')) {
         // See http://goo.gl/N7W6wJ.
            y.cf_visitor = JSON.parse(headers['cf-visitor']);
        }
     */
        if (headers.hasOwnProperty('content-length')) {
            y.content_length = parseInt(headers['content-length'], 10);
        }
        if (headers.hasOwnProperty('dnt')) {
         // See http://goo.gl/Rrxu4L.
            y.dnt = parseInt(headers.dnt, 10);
        }
        if (headers.hasOwnProperty('origin')) {
         // See http://goo.gl/BZldtx.
            y.origin = headers.origin;
        }
        if (headers.hasOwnProperty('referer')) {
         // See http://goo.gl/BCW8Vf.
            y.referer = headers.referer;
        }
        if (headers.hasOwnProperty('x-forwarded-for')) {
         // See http://goo.gl/ZtqLv1.
            y.ip = headers['x-forwarded-for'].split(',')[0];
        } else {
            y.ip = request.connection.remoteAddress;
        }
     /*
        if (headers.hasOwnProperty('x-forwarded-port')) {
         // See http://goo.gl/B8Ks7h.
            y.x_forwarded_port = headers['x-forwarded-port'];
        }
        if (headers.hasOwnProperty('x-forwarded-proto')) {
         // See http://goo.gl/B8Ks7h.
            y.x_forwarded_proto = headers['x-forwarded-proto'];
        }
        if (headers.hasOwnProperty('x-request-start')) {
         // See http://goo.gl/B8Ks7h.
            y.x_request_start = parseInt(headers['x-request-start'], 10);
        }
     */
        if (headers.hasOwnProperty('x-wap-profile')) {
         // See http://goo.gl/vDg0CV.
            y.x_wap_profile = headers['x-wap-profile'];
        }
        return y;
    };

    exports.parse = function (x) {
     // This function needs documentation.
        return JSON.parse(x, function (key, val) {
         // This function needs documentation.
            /*jslint unparam: true */
            if (typeof val === 'string') {
                return val.replace(/[$][{]([A-Z0-9_]+)[}]/g, function ($0, $1) {
                 // This function needs documentation.
                    return process.env[$1];
                });
            }
            return val;
        });
    };

 // That's all, folks!

    return;

}());

//- vim:set syntax=javascript:
<commit_msg>Log a few more HTTP headers when applicable<commit_after>//- JavaScript source code

//- custom.js ~~
//                                                      ~~ (c) SRW, 09 Dec 2013
//                                                  ~~ last updated 14 May 2014

(function () {
    'use strict';

 // Pragmas

    /*jslint indent: 4, maxlen: 80, node: true */

    /*properties
        cf_connecting_ip, 'cf-connecting-ip', cf_ipcountry, 'cf-ipcountry',
        cf_ray, 'cf-ray', cf_visitor, 'cf-visitor', connection, content_length,
        'content-length', dnt, env, from, hasOwnProperty, headers, host, ip,
        log, method, origin, parse, referer, remoteAddress, replace, split,
        timestamp, url, via, warning, x_att_deviceid, 'x-att-deviceid',
        x_forwarded_for, 'x-forwarded-for', x_forwarded_port,
        'x-forwarded-port', x_forwarded_proto, 'x-forwarded-proto',
        x_request_start, 'x-request-start', x_wap_profile, 'x-wap-profile'
    */

 // Module definitions

    exports.log = function (request) {
     // This is a custom logging function that executes once for every request
     // if logging is enabled. This function is optional, however, because
     // QMachine provides a default logging function. I have used a convention
     // in which hyphens are converted into underscores in order to prevent
     // inconveniences with certain tools that attempt to auto-detect schemas
     // from JSON. Also, I have commented out various headers that I don't
     // personally find useful. Ironically, the "Do Not Track" header (DNT) is
     // logged alongside the tracking data because I don't understand the legal
     // stuff yet, but I will delete all personal data from entries that opted
     // out once I figure out which data are considered "personal".
        var headers, y;
        headers = request.headers;
        y = {
            host: headers.host,
            method: request.method,
            timestamp: new Date(),
            url: request.url
        };
        if (headers.hasOwnProperty('cf-connecting-ip')) {
         // See http://goo.gl/81sHn8.
            y.cf_connecting_ip = headers['cf-connecting-ip'];
        }
        if (headers.hasOwnProperty('cf-ipcountry')) {
         // See http://goo.gl/QpDIBe.
            y.cf_ipcountry = headers['cf-ipcountry'];
        }
     /*
        if (headers.hasOwnProperty('cf-ray')) {
         // See http://goo.gl/wMfrD8.
            y.cf_ray = headers['cf-ray'];
        }
        if (headers.hasOwnProperty('cf-visitor')) {
         // See http://goo.gl/N7W6wJ.
            y.cf_visitor = JSON.parse(headers['cf-visitor']);
        }
     */
        if (headers.hasOwnProperty('content-length')) {
            y.content_length = parseInt(headers['content-length'], 10);
        }
        if (headers.hasOwnProperty('dnt')) {
         // See http://goo.gl/Rrxu4L.
            y.dnt = parseInt(headers.dnt, 10);
        }
        if (headers.hasOwnProperty('from')) {
         // See http://goo.gl/RNi5So.
            y.from = headers.from;
        }
        if (headers.hasOwnProperty('origin')) {
         // See http://goo.gl/BZldtx.
            y.origin = headers.origin;
        }
        if (headers.hasOwnProperty('referer')) {
         // See http://goo.gl/BCW8Vf.
            y.referer = headers.referer;
        }
        if (headers.hasOwnProperty('via')) {
         // See http://goo.gl/RNi5So.
            y.via = headers.via;
        }
        if (headers.hasOwnProperty('warning')) {
         // See http://goo.gl/RNi5So.
            y.warning = headers.warning;
        }
        if (headers.hasOwnProperty('x-forwarded-for')) {
         // See http://goo.gl/ZtqLv1.
            y.ip = headers['x-forwarded-for'].split(',')[0];
        } else {
            y.ip = request.connection.remoteAddress;
        }
        if (headers.hasOwnProperty('x-att-deviceid')) {
         // See http://goo.gl/IRuyx.
            y.x_att_deviceid = headers['x-att-deviceid'];
        }
     /*
        if (headers.hasOwnProperty('x-forwarded-port')) {
         // See http://goo.gl/B8Ks7h.
            y.x_forwarded_port = headers['x-forwarded-port'];
        }
        if (headers.hasOwnProperty('x-forwarded-proto')) {
         // See http://goo.gl/B8Ks7h.
            y.x_forwarded_proto = headers['x-forwarded-proto'];
        }
        if (headers.hasOwnProperty('x-request-start')) {
         // See http://goo.gl/B8Ks7h.
            y.x_request_start = parseInt(headers['x-request-start'], 10);
        }
     */
        if (headers.hasOwnProperty('x-wap-profile')) {
         // See http://goo.gl/vDg0CV.
            y.x_wap_profile = headers['x-wap-profile'];
        }
        return y;
    };

    exports.parse = function (x) {
     // This function needs documentation.
        return JSON.parse(x, function (key, val) {
         // This function needs documentation.
            /*jslint unparam: true */
            if (typeof val === 'string') {
                return val.replace(/[$][{]([A-Z0-9_]+)[}]/g, function ($0, $1) {
                 // This function needs documentation.
                    return process.env[$1];
                });
            }
            return val;
        });
    };

 // That's all, folks!

    return;

}());

//- vim:set syntax=javascript:
<|endoftext|>


<commit_before>var through = require("through2")
  , falafel = require("falafel")

module.exports = function (file, opts) {
  if (/\.json$/.test(file)) return through()

  opts = opts || {}
  opts.replacement = opts.replacement || opts.r || ""

  var data = ""

  return through(
    function (buf, enc, cb) {
      data += buf
      cb()
    },
    function (cb) {
      try {
        this.push(String(parse(data, opts)))
      } catch (er) {
        cb(new Error(er.toString().replace("Error: ", "") + " (" + file + ")"))
      }
      cb()
    }
  )
}

function parse (data, opts) {
  return falafel(data, function (node) {
    if (node.type != "DebuggerStatement" && (node.type != "CallExpression" || !isConsoleLog(node.callee))) return;
    node.update(opts.replacement)
  })
}

function isConsoleLog (node) {
  return isConsole(node) && isLog(node.property)
}

function isConsole (node) {
  if (!node) return false
  if (node.type != "MemberExpression") return false
  return node.object.type == "Identifier" && node.object.name == "console"
}

var consoleApi = ["assert", "count", "debug", "dir", "error", "exception", "group", "groupCollapsed", "groupEnd", "info", "log", "profile", "profileEnd", "time", "timeEnd", "trace", "warn"]

function isLog (node) {
  return node.type == "Identifier"
    && (consoleApi.indexOf(node.name) > -1)
}<commit_msg>Fix potential double callback call<commit_after>var through = require("through2")
  , falafel = require("falafel")

module.exports = function (file, opts) {
  if (/\.json$/.test(file)) return through()

  opts = opts || {}
  opts.replacement = opts.replacement || opts.r || ""

  var data = ""

  return through(
    function (buf, enc, cb) {
      data += buf
      cb()
    },
    function (cb) {
      try {
        this.push(String(parse(data, opts)))
      } catch (er) {
        return cb(new Error(er.toString().replace("Error: ", "") + " (" + file + ")"))
      }
      cb()
    }
  )
}

function parse (data, opts) {
  return falafel(data, function (node) {
    if (node.type != "DebuggerStatement" && (node.type != "CallExpression" || !isConsoleLog(node.callee))) return;
    node.update(opts.replacement)
  })
}

function isConsoleLog (node) {
  return isConsole(node) && isLog(node.property)
}

function isConsole (node) {
  if (!node) return false
  if (node.type != "MemberExpression") return false
  return node.object.type == "Identifier" && node.object.name == "console"
}

var consoleApi = ["assert", "count", "debug", "dir", "error", "exception", "group", "groupCollapsed", "groupEnd", "info", "log", "profile", "profileEnd", "time", "timeEnd", "trace", "warn"]

function isLog (node) {
  return node.type == "Identifier"
    && (consoleApi.indexOf(node.name) > -1)
}<|endoftext|>


<commit_before>"use strict";

function Spot(val) {
  this.val = val;
  this.revealed = false;
  // TODO maybe hold coordinates?
}

function Board(size) {
  this.board = this.createBoard(size);
  this.gameOver = false;
}

Board.prototype.eachSpot = function(func){
  for(var i=0; i<this.board.length; i++) {
    for(var j=0; j<this.board[i].length; j++) {
      func(i,j);
    }
  }
}

Board.prototype.createBoard = function(size) {
  var rows = [];
  for(var i=0; i<size; i++) {
    rows.push(new Array(size));
  }
  return rows;
};

Board.prototype.seedBoard = function() {
  this.eachSpot(function(row,col){
    var random = Math.ceil(Math.random()*10);
    (random % 4 === 0) ? (this.board[row][col] = new Spot("B")) : (this.board[row][col] = new Spot(" "));
  }.bind(this));
};

Board.prototype.calculateBoard = function() {
  this.eachSpot(function(row, col){
    this.calculateSpot(row, col);
  }.bind(this));
};

Board.prototype.calculateSpot = function(row, col) {
  if(this.board[row][col].val !== "B") {
    var bombCount = this.surroundingSpots(row, col, function(x,y,r,c){
      if(this.board[r+x][c+y].val==="B") {return 1;}
    }.bind(this));
    this.board[row][col].val = bombCount;
  };
};

Board.prototype.updateSpot = function(row, col) {
  this.revealSpot(row, col);
  if(this.board[row][col].val==='0') {
    this.surroundingSpots(row, col, function(x,y,r,c){
      var currentLocation = this.board[r+x][c+y];
      if(currentLocation.val==='0' && currentLocation.revealed===false) {
        this.updateSpot((r+x),(c+y));
      }
      if((!(x===0&&y==0))&&currentLocation.val!=='B'){currentLocation.revealed=true}
    }.bind(this));
  }else if(this.board[row][col].val==='B') {
    this.gameOver = true;
  };
};

Board.prototype.surroundingSpots = function(row, col, func){
  var val = 0;
  var startX, startY, endX, endY;
  row === 0 ? startX = 0 : startX = -1;
  col === 0 ? startY = 0 : startY = -1;
  row === this.board.length-1 ? endX = 1 : endX = 2;
  col === this.board.length-1 ? endY = 1 : endY = 2;

  for(var x = startX; x < endX; x++) {
    for(var y = startY; y < endY; y++) {
      var temp = func(x, y, row, col);
      if(!(isNaN(temp))){val = val + temp;}
    }
  }
  return val.toString();
};

Board.prototype.revealSpot = function(row, col) {
  this.board[row][col].revealed = true;
};

Board.prototype.displayBoard = function() {
  for(var i=0; i<this.board.length; i++) {
    console.log(this.board[i]);
  }
};
<commit_msg>implemented a game over check, for board<commit_after>"use strict";

function Spot(val) {
  this.val = val;
  this.revealed = false;
}

function Board(size) {
  this.board = this.createBoard(size);
  this.gameOver = false;
}

// generates board of custom size
Board.prototype.createBoard = function(size) {
  var rows = [];
  for(var i=0; i<size; i++) {
    rows.push(new Array(size));
  }
  return rows;
};

// seeds board with random bomb placement
Board.prototype.seedBoard = function() {
  this.eachSpot(function(row,col){
    var random = Math.ceil(Math.random()*10);
    (random % 4 === 0) ? (this.board[row][col] = new Spot("B")) : (this.board[row][col] = new Spot(" "));
  }.bind(this));
};

// fills in non-bomb spots with bomb-count
Board.prototype.calculateBoard = function() {
  this.eachSpot(function(row, col){
    this.calculateSpot(row, col);
  }.bind(this));
};

// calculates/sets number of bombs around spot
Board.prototype.calculateSpot = function(row, col) {
  if(this.board[row][col].val !== "B") {
    var bombCount = this.surroundingSpots(row, col, function(x,y,r,c){
      if(this.board[r+x][c+y].val==="B") {return 1;}
    }.bind(this));
    this.board[row][col].val = bombCount;
  };
};

// reveals spot, and recursively reveals adjacent spots if bomb count 0
Board.prototype.updateSpot = function(row, col) {
  this.revealSpot(row, col);
  if(this.board[row][col].val==='0') {
    this.surroundingSpots(row, col, function(x,y,r,c){
      var currentLocation = this.board[r+x][c+y];
      if(currentLocation.val==='0' && currentLocation.revealed===false) {
        this.updateSpot((r+x),(c+y));
      }
      if((!(x===0&&y==0))&&currentLocation.val!=='B'){currentLocation.revealed=true}
    }.bind(this));
  }else if(this.board[row][col].val==='B') {
    this.gameOver = true;
  };
};

Board.prototype.isGameOver = function(){
  this.eachSpot(function(row, col){
    if(this.board[row][col].val==='B' && this.board[row][col].revealed===true){this.gameOver=true;}
  }.bind(this));
  if(this.gameOver!==true){
    this.gameOver = true;
    this.eachSpot(function(row, col){
      if(this.board[row][col].val!=='B' && this.board[row][col].revealed===false){this.gameOver=false;}
    }.bind(this));
  }
  return this.gameOver;
};

// loops through a spots adjacent spots
Board.prototype.surroundingSpots = function(row, col, func){
  var val = 0;
  var startX, startY, endX, endY;
  row === 0 ? startX = 0 : startX = -1;
  col === 0 ? startY = 0 : startY = -1;
  row === this.board.length-1 ? endX = 1 : endX = 2;
  col === this.board.length-1 ? endY = 1 : endY = 2;

  for(var x = startX; x < endX; x++) {
    for(var y = startY; y < endY; y++) {
      var temp = func(x, y, row, col);
      if(!(isNaN(temp))){val = val + temp;}
    }
  }
  return val.toString();
};

// loops through each spot in board
Board.prototype.eachSpot = function(func, val){
  var val = val;
  for(var i=0; i<this.board.length; i++) {
    for(var j=0; j<this.board[i].length; j++) {
      func(i,j, val);
    }
  }
  console.log("value: "+val);
  if(val!=='undefined'){return val};
}

// sets a spot to status revealed
Board.prototype.revealSpot = function(row, col) {
  this.board[row][col].revealed = true;
};

Board.prototype.displayBoard = function() {
  for(var i=0; i<this.board.length; i++) {
    console.log(this.board[i]);
  }
};
<|endoftext|>


<commit_before>define([
  'superapi'
], function (superapi) {
  'use strict';

  describe('request url', function () {
    it('should append the service url to the baseUrl', function () {
      var api = superapi.default({
        baseUrl: 'http://foo.domain.tld/api',
        services: {
          foo: {
            path: '/foo'
          }
        }
      });

      api.url('foo').should.eql('http://foo.domain.tld/api/foo');
    });

    it('should add a slash in front of route path if missing', function () {
      var api = superapi.default({
        baseUrl: 'http://foo.domain.tld/api',
        services: {
          foo: {
            path: 'foo'
          }
        }
      });

      api.url('foo').should.eql('http://foo.domain.tld/api/foo');
    });

    it('should support route as a string', function () {
      var api = superapi.default({
        baseUrl: 'http://foo.domain.tld/api',
        services: {
          foo: 'bar'
        }
      });

      api.url('foo').should.eql('http://foo.domain.tld/api/bar');
    });

    it('should throw error if path is missing', function () {
      var api = superapi.default({
        baseUrl: 'http://foo.domain.tld/api',
        services: {
          foo: {}
        }
      });

      api.url.bind(null, 'foo').should.throw();
    });
  });

  describe('request method', function () {
    it('should default to get', function () {
      var api = superapi.default({
        baseUrl: 'http://foo.domain.tld/api',
        services: {
          foo: 'bar'
        }
      });

      api.request('foo').method.should.eql('GET');
    });

    it('should support POST', function () {
      var api = superapi.default({
        baseUrl: 'http://foo.domain.tld/api',
        services: {
          foo: {
            path: 'bar',
            method: 'POST'
          }
        }
      });

      api.request('foo').method.should.eql('POST');
    });

    it('should support PUT', function () {
      var api = superapi.default({
        baseUrl: 'http://foo.domain.tld/api',
        services: {
          foo: {
            path: 'bar',
            method: 'PUT'
          }
        }
      });

      api.request('foo').method.should.eql('PUT');
    });

    it('should support DELETE', function () {
      var api = superapi.default({
        baseUrl: 'http://foo.domain.tld/api',
        services: {
          foo: {
            path: 'bar',
            method: 'DELETE'
          }
        }
      });

      api.request('foo').method.should.eql('DELETE');
    });

    it('should support HEAD', function () {
      var api = superapi.default({
        baseUrl: 'http://foo.domain.tld/api',
        services: {
          foo: {
            path: 'bar',
            method: 'HEAD'
          }
        }
      });

      api.request('foo').method.should.eql('HEAD');
    });

    it('should support PATCH', function () {
      var api = superapi.default({
        baseUrl: 'http://foo.domain.tld/api',
        services: {
          foo: {
            path: 'bar',
            method: 'PATCH'
          }
        }
      });

      api.request('foo').method.should.eql('PATCH');
    });
  });

  describe.skip('request headers', function () {
  });

  describe('service request headers', function () {
    it('should add service headers', function () {
      var api = superapi.default({
        baseUrl: 'http://foo.domain.tld/api',
        services: {
          foo: {
            path: 'bar',
            headers: {
              'Content-type': 'json'
            }
          }
        }
      });

      api.request('foo')._header.should.haveOwnProperty('content-type');
      api.request('foo')._header['content-type'].should.eql('json');
    });
  });

  describe('service request options', function () {
    it('should add headers options', function () {
      var api = superapi.default({
        baseUrl: 'http://foo.domain.tld/api',
        services: {
          foo: {
            path: 'bar',
            options: {
              type: 'json',
              accept: 'png'
            }
          }
        }
      });

      api.request('foo')._header.should.haveOwnProperty('content-type');
      api.request('foo')._header['content-type'].should.eql('application/json');

      api.request('foo')._header.should.haveOwnProperty('accept');
      api.request('foo')._header.accept.should.eql('png');
    });
  });

});
<commit_msg>test(comment): add a comment about usage of bind which do not work on PhantomJS<commit_after>define([
  'superapi'
], function (superapi) {
  'use strict';

  describe('request url', function () {
    it('should append the service url to the baseUrl', function () {
      var api = superapi.default({
        baseUrl: 'http://foo.domain.tld/api',
        services: {
          foo: {
            path: '/foo'
          }
        }
      });

      api.url('foo').should.eql('http://foo.domain.tld/api/foo');
    });

    it('should add a slash in front of route path if missing', function () {
      var api = superapi.default({
        baseUrl: 'http://foo.domain.tld/api',
        services: {
          foo: {
            path: 'foo'
          }
        }
      });

      api.url('foo').should.eql('http://foo.domain.tld/api/foo');
    });

    it('should support route as a string', function () {
      var api = superapi.default({
        baseUrl: 'http://foo.domain.tld/api',
        services: {
          foo: 'bar'
        }
      });

      api.url('foo').should.eql('http://foo.domain.tld/api/bar');
    });

    it('should throw error if path is missing', function () {
      var api = superapi.default({
        baseUrl: 'http://foo.domain.tld/api',
        services: {
          foo: {}
        }
      });

      // fail in PhantomJS as bind not available
      api.url.bind(null, 'foo').should.throw();
    });
  });

  describe('request method', function () {
    it('should default to get', function () {
      var api = superapi.default({
        baseUrl: 'http://foo.domain.tld/api',
        services: {
          foo: 'bar'
        }
      });

      api.request('foo').method.should.eql('GET');
    });

    it('should support POST', function () {
      var api = superapi.default({
        baseUrl: 'http://foo.domain.tld/api',
        services: {
          foo: {
            path: 'bar',
            method: 'POST'
          }
        }
      });

      api.request('foo').method.should.eql('POST');
    });

    it('should support PUT', function () {
      var api = superapi.default({
        baseUrl: 'http://foo.domain.tld/api',
        services: {
          foo: {
            path: 'bar',
            method: 'PUT'
          }
        }
      });

      api.request('foo').method.should.eql('PUT');
    });

    it('should support DELETE', function () {
      var api = superapi.default({
        baseUrl: 'http://foo.domain.tld/api',
        services: {
          foo: {
            path: 'bar',
            method: 'DELETE'
          }
        }
      });

      api.request('foo').method.should.eql('DELETE');
    });

    it('should support HEAD', function () {
      var api = superapi.default({
        baseUrl: 'http://foo.domain.tld/api',
        services: {
          foo: {
            path: 'bar',
            method: 'HEAD'
          }
        }
      });

      api.request('foo').method.should.eql('HEAD');
    });

    it('should support PATCH', function () {
      var api = superapi.default({
        baseUrl: 'http://foo.domain.tld/api',
        services: {
          foo: {
            path: 'bar',
            method: 'PATCH'
          }
        }
      });

      api.request('foo').method.should.eql('PATCH');
    });
  });

  describe.skip('request headers', function () {
  });

  describe('service request headers', function () {
    it('should add service headers', function () {
      var api = superapi.default({
        baseUrl: 'http://foo.domain.tld/api',
        services: {
          foo: {
            path: 'bar',
            headers: {
              'Content-type': 'json'
            }
          }
        }
      });

      api.request('foo')._header.should.haveOwnProperty('content-type');
      api.request('foo')._header['content-type'].should.eql('json');
    });
  });

  describe('service request options', function () {
    it('should add headers options', function () {
      var api = superapi.default({
        baseUrl: 'http://foo.domain.tld/api',
        services: {
          foo: {
            path: 'bar',
            options: {
              type: 'json',
              accept: 'png'
            }
          }
        }
      });

      api.request('foo')._header.should.haveOwnProperty('content-type');
      api.request('foo')._header['content-type'].should.eql('application/json');

      api.request('foo')._header.should.haveOwnProperty('accept');
      api.request('foo')._header.accept.should.eql('png');
    });
  });

});
<|endoftext|>


<commit_before>/*!
 * Colorize SVG files.
 *
 * The task currently doesn't use the standard file specifying methods with this.filesSrc.
 * An option to do it may be added in the future.
 */

/*jshint node:true */

var Q = require( 'q' ),
	path = require( 'path' ),
	asyncTask = require( 'grunt-promise-q' );

module.exports = function ( grunt ) {

	asyncTask.registerMulti(
		grunt,
		'colorizeSvg',
		'Generate colored variants of SVG images',
		function () {
			var
				data = this.data,
				options = this.options(),
				source = new Source(
					data.srcDir,
					options.images,
					options.variants,
					{
						intro: options.intro,
						prefix: options.prefix,
						cssPrependPath: data.cssPrependPath,
						selectorWithoutVariant: options.selectorWithoutVariant || options.selector,
						selectorWithVariant: options.selectorWithVariant || options.selector
					}
				);

			return source.getImageList().generate(
				new Destination(
					data.destDir,
					data.destLessFile || {
						ltr: path.join( data.destDir, 'images.less' ),
						rtl: path.join( data.destDir, 'images.rtl.less' )
					}
				)
			).then( function ( totalFiles ) {
				grunt.log.writeln( 'Created ' + totalFiles + ' SVG files.' );
			} );
		}
	);

	/* Classes */

	/**
	 * Image source.
	 *
	 * @class
	 *
	 * @constructor
	 * @param {string} path Directory containing source images
	 * @param {Object} images Lists of image configurations
	 * @param {Object} [variants] List of variant configurations, keyed by variant name
	 * @param {Object} [options] Additional options
	 */
	function Source( path, images, variants, options ) {
		this.path = path;
		this.images = images;
		this.variants = variants || {};
		this.options = options || {};
	}

	/**
	 * Get the path to source images directory.
	 *
	 * @return {string} Path
	 */
	Source.prototype.getPath = function () {
		return this.path;
	};

	/**
	 * Get image list.
	 *
	 * @return ImageList Image list
	 */
	Source.prototype.getImageList = function () {
		return new ImageList(
			this.path,
			new VariantList( this.variants ),
			this.options,
			this.images
		);
	};

	/**
	 * Destination for images.
	 *
	 * @class
	 *
	 * @constructor
	 * @param {string} path Image path
	 * @param {Object} stylesheetPath Stylesheet file path
	 * @param {string} stylesheetPath.ltr Stylesheet file path, left-to-right
	 * @param {string} stylesheetPath.rtl Stylesheet file path, right-to-left
	 */
	function Destination( path, stylesheetPath ) {
		this.path = path;
		this.stylesheetPath = stylesheetPath;
	}

	/**
	 * Get image destination directory.
	 *
	 * @return {string} Destination path
	 */
	Destination.prototype.getPath = function () {
		return this.path;
	};

	/**
	 * Get path to file of generated Less stylesheet.
	 *
	 * @param {string} textDirection Text direction to get stylesheet path for, 'ltr' or 'rtl'
	 * @return {string} Destination path
	 */
	Destination.prototype.getStylesheetPath = function ( textDirection ) {
		return this.stylesheetPath[ textDirection ];
	};

	/**
	 * Source image.
	 *
	 * @class
	 *
	 * @constructor
	 * @param {Object} list Image list
	 * @param {string} name Image name
	 * @param {Object} data Image options
	 */
	function Image( list, name, data ) {
		this.list = list;
		this.name = name;
		this.file = data.file;
		this.variantNames = ( data.variants || [] )
			.concat( this.list.getVariants().getGlobalVariantNames() )
			.filter( function ( variant, index, variants ) {
				return variants.indexOf( variant ) === index;
			} );
	}

	/**
	 * Generate CSS and images.
	 *
	 * @param {Destination} destination Destination
	 * @return {Q.Promise}
	 */
	Image.prototype.generate = function ( destination ) {
		// TODO Make configurable
		function getDeclarations( primary ) {
			// If 'primary' is not a SVG file, 'fallback' and 'primary' are intentionally the same
			var fallback = primary.replace( /\.svg$/, '.png' );
			return '.oo-ui-background-image-svg2(' +
				'\'' + ( cssPrependPath || '' ) + primary + '\', ' +
				'\'' + ( cssPrependPath || '' ) + fallback + '\'' +
				')';
		}
		function variantizeFileName( fileName, variantName ) {
			if ( variantName ) {
				return fileName.replace( /\.(\w+)$/, '-' + variantName + '.$1' );
			}
			return fileName;
		}

		var selector, declarations, direction,
			deferred = Q.defer(),
			file = typeof this.file === 'string' ?
				{ default: this.file } :
				{ ltr: this.file.ltr, rtl: this.file.rtl },
			name = this.name,
			sourcePath = this.list.getPath(),
			destinationPath = destination.getPath(),
			variants = this.list.getVariants(),
			cssClassPrefix = this.list.getCssClassPrefix(),
			cssSelectors = this.list.getCssSelectors(),
			cssPrependPath = this.list.options.cssPrependPath,
			originalSvg = {},
			rules = {
				default: [],
				ltr: [],
				rtl: []
			},
			files = {},
			uncolorizableImages = [],
			unknownVariants = [];

		// Original
		selector = cssSelectors.selectorWithoutVariant
			.replace( /{prefix}/g, cssClassPrefix )
			.replace( /{name}/g, name )
			.replace( /{variant}/g, '' );

		for ( direction in file ) {
			declarations = getDeclarations( file[ direction ] );
			rules[ direction ].push( selector + ' {\n\t' + declarations + '\n}' );

			originalSvg[ direction ] = grunt.file.read(
				path.join( sourcePath, file[ direction ] )
			);
			files[ path.join( destinationPath, file[ direction ] ) ] = originalSvg[ direction ];
		}

		// Variants
		this.variantNames.forEach( function ( variantName ) {
			var variantSvg, destinationFilePath,
				variant = variants.getVariant( variantName );

			if ( variant === undefined ) {
				unknownVariants.push( variantName );
				return;
			}

			selector = cssSelectors.selectorWithVariant
				.replace( /{prefix}/g, cssClassPrefix )
				.replace( /{name}/g, name )
				.replace( /{variant}/g, variantName );

			for ( direction in file ) {
				declarations = getDeclarations( variantizeFileName( file[ direction ], variantName ) );
				rules[ direction ].push( selector + ' {\n\t' + declarations + '\n}' );

				// TODO: Do this in a safer and more clever way
				variantSvg = originalSvg[ direction ].replace(
					/<svg[^>]*>/, '$&<style>* { fill: ' + variant.getColor() + ' }</style>'
				);

				if ( originalSvg[ direction ] === variantSvg ) {
					uncolorizableImages.push( file[ direction ] );
					continue;
				}

				destinationFilePath = path.join(
					destinationPath,
					variantizeFileName( file[ direction ], variantName )
				);
				files[ destinationFilePath ] = variantSvg;
			}
		} );

		if ( unknownVariants.length || uncolorizableImages.length ) {
			if ( unknownVariants.length ) {
				grunt.log.error(
					unknownVariants.length +
					' unknown variants requested: ' +
					unknownVariants.join( ', ' )
				);
			}
			if ( uncolorizableImages.length ) {
				grunt.log.error(
					uncolorizableImages.length +
					' invalid source images: ' +
					uncolorizableImages.join( ', ' )
				);
			}
			deferred.reject( 'Failed to generate some images' );
		} else {
			rules.ltr = rules.ltr.concat( rules.default );
			rules.rtl = rules.rtl.concat( rules.default );
			delete rules.default;
			deferred.resolve( {
				rules: rules,
				files: files
			} );
		}

		return deferred.promise;
	};

	/**
	 * List of source images.
	 *
	 * @class
	 *
	 * @constructor
	 * @param {string} path Images path
	 * @param {VariantList} variants Variants list
	 * @param {Object} options Additional options
	 * @param {Object} data List of image configurations keyed by name
	 */
	function ImageList( path, variants, options, data ) {
		var key;

		this.list = {};
		this.path = path;
		this.variants = variants;
		this.options = options;

		for ( key in data ) {
			this.list[ key ] = new Image( this, key, data[ key ] );
		}
	}

	/**
	 * Get image path.
	 *
	 * @return {string} Image path
	 */
	ImageList.prototype.getPath = function () {
		return this.path;
	};

	/**
	 * Get image variants.
	 *
	 * @return {VariantsList} Image variants
	 */
	ImageList.prototype.getVariants = function () {
		return this.variants;
	};

	/**
	 * Get CSS class prefix.
	 *
	 * @return {string} CSS class prefix
	 */
	ImageList.prototype.getCssClassPrefix = function () {
		return this.options.prefix || '';
	};

	/**
	 * Get CSS selectors.
	 *
	 * @return {Object.<string, string>} CSS selectors
	 */
	ImageList.prototype.getCssSelectors = function () {
		return {
			selectorWithoutVariant: this.options.selectorWithoutVariant || '.{prefix}-{name}',
			selectorWithVariant: this.options.selectorWithVariant || '.{prefix}-{name}-{variant}'
		};
	};

	/**
	 * Get CSS file intro.
	 *
	 * @return {string} CSS file intro
	 */
	ImageList.prototype.getCssIntro = function () {
		return this.options.intro || '';
	};

	/**
	 * Get number of images in list.
	 *
	 * @return {number} List length
	 */
	ImageList.prototype.getLength = function () {
		return Object.keys( this.list ).length;
	};

	/**
	 * Generate images and CSS.
	 *
	 * @param {Destination} destination Destination
	 * @return {Q.Promise} Promise resolved with number of generated SVG files
	 */
	ImageList.prototype.generate = function ( destination ) {
		var list = this.list,
			intro = this.getCssIntro();
		return Q.all( Object.keys( this.list ).map( function ( key ) {
			return list[ key ].generate( destination );
		} ) ).then( function ( data ) {
			var textDirection, stylesheetPath, destinationFilePath, dataFormat;
			dataFormat = {
				files: {},
				rules: {
					ltr: [],
					rtl: []
				}
			};

			data = data.reduce( function ( a, b ) {
				for ( destinationFilePath in b.files ) {
					// This de-duplicates the entries, as the same file can be used by many Images
					a.files[ destinationFilePath ] = b.files[ destinationFilePath ];
				}
				a.rules.ltr = a.rules.ltr.concat( b.rules.ltr );
				a.rules.rtl = a.rules.rtl.concat( b.rules.rtl );
				return a;
			}, dataFormat );

			for ( textDirection in data.rules ) {
				stylesheetPath = destination.getStylesheetPath( textDirection );
				grunt.file.write(
					stylesheetPath,
					intro + '\n' + data.rules[ textDirection ].join( '\n' )
				);
				grunt.log.writeln( 'Created "' + stylesheetPath + '".' );
			}
			for ( destinationFilePath in data.files ) {
				grunt.file.write( destinationFilePath, data.files[ destinationFilePath ] );
			}

			return Object.keys( data.files ).length;
		} );
	};

	/**
	 * Image variant.
	 *
	 * @class
	 *
	 * @constructor
	 * @param {VariantList} list Variant list
	 * @param {string} name Variant name
	 * @param {Object} data Variant options
	 */
	function Variant( list, name, data ) {
		// Properties
		this.list = list;
		this.name = name;
		this.color = data.color;
		this.global = data.global;
	}

	/**
	 * Check if variant is global.
	 *
	 * @return {boolean} Variant is global
	 */
	Variant.prototype.isGlobal = function () {
		return this.global;
	};

	/**
	 * Get variant color.
	 *
	 * @return {string} CSS color expression
	 */
	Variant.prototype.getColor = function () {
		return this.color;
	};

	/**
	 * List of variants.
	 *
	 * @class
	 *
	 * @constructor
	 * @param {Object} list List of variant configurations keyed by name
	 */
	function VariantList( data ) {
		var key;

		this.list = {};
		this.globals = [];

		for ( key in data ) {
			this.list[ key ] = new Variant( this, key, data[ key ] );
			if ( this.list[ key ].isGlobal() ) {
				this.globals.push( key );
			}
		}
	}

	/**
	 * Get names of global variants.
	 *
	 * @return {string[]} Global variant names
	 */
	VariantList.prototype.getGlobalVariantNames = function () {
		return this.globals;
	};

	/**
	 * Get variant by name.
	 *
	 * @param {string} name Variant name
	 * @return {Variant|undefined} Variant with matching name, or undefined of none exists.
	 */
	VariantList.prototype.getVariant = function ( name ) {
		return this.list[ name ];
	};

	/**
	 * Get number of variants in list.
	 *
	 * @return {number} List length
	 */
	VariantList.prototype.getLength = function () {
		return Object.keys( this.list ).length;
	};

};
<commit_msg>Support (poorly) per-language icon versions in colorize-svg.js<commit_after>/*!
 * Colorize SVG files.
 *
 * The task currently doesn't use the standard file specifying methods with this.filesSrc.
 * An option to do it may be added in the future.
 */

/*jshint node:true */

var Q = require( 'q' ),
	path = require( 'path' ),
	asyncTask = require( 'grunt-promise-q' );

module.exports = function ( grunt ) {

	asyncTask.registerMulti(
		grunt,
		'colorizeSvg',
		'Generate colored variants of SVG images',
		function () {
			var
				data = this.data,
				options = this.options(),
				source = new Source(
					data.srcDir,
					options.images,
					options.variants,
					{
						intro: options.intro,
						prefix: options.prefix,
						cssPrependPath: data.cssPrependPath,
						selectorWithoutVariant: options.selectorWithoutVariant || options.selector,
						selectorWithVariant: options.selectorWithVariant || options.selector
					}
				);

			return source.getImageList().generate(
				new Destination(
					data.destDir,
					data.destLessFile || {
						ltr: path.join( data.destDir, 'images.less' ),
						rtl: path.join( data.destDir, 'images.rtl.less' )
					}
				)
			).then( function ( totalFiles ) {
				grunt.log.writeln( 'Created ' + totalFiles + ' SVG files.' );
			} );
		}
	);

	/* Classes */

	/**
	 * Image source.
	 *
	 * @class
	 *
	 * @constructor
	 * @param {string} path Directory containing source images
	 * @param {Object} images Lists of image configurations
	 * @param {Object} [variants] List of variant configurations, keyed by variant name
	 * @param {Object} [options] Additional options
	 */
	function Source( path, images, variants, options ) {
		this.path = path;
		this.images = images;
		this.variants = variants || {};
		this.options = options || {};
	}

	/**
	 * Get the path to source images directory.
	 *
	 * @return {string} Path
	 */
	Source.prototype.getPath = function () {
		return this.path;
	};

	/**
	 * Get image list.
	 *
	 * @return ImageList Image list
	 */
	Source.prototype.getImageList = function () {
		return new ImageList(
			this.path,
			new VariantList( this.variants ),
			this.options,
			this.images
		);
	};

	/**
	 * Destination for images.
	 *
	 * @class
	 *
	 * @constructor
	 * @param {string} path Image path
	 * @param {Object} stylesheetPath Stylesheet file path
	 * @param {string} stylesheetPath.ltr Stylesheet file path, left-to-right
	 * @param {string} stylesheetPath.rtl Stylesheet file path, right-to-left
	 */
	function Destination( path, stylesheetPath ) {
		this.path = path;
		this.stylesheetPath = stylesheetPath;
	}

	/**
	 * Get image destination directory.
	 *
	 * @return {string} Destination path
	 */
	Destination.prototype.getPath = function () {
		return this.path;
	};

	/**
	 * Get path to file of generated Less stylesheet.
	 *
	 * @param {string} textDirection Text direction to get stylesheet path for, 'ltr' or 'rtl'
	 * @return {string} Destination path
	 */
	Destination.prototype.getStylesheetPath = function ( textDirection ) {
		return this.stylesheetPath[ textDirection ];
	};

	/**
	 * Source image.
	 *
	 * @class
	 *
	 * @constructor
	 * @param {Object} list Image list
	 * @param {string} name Image name
	 * @param {Object} data Image options
	 */
	function Image( list, name, data ) {
		this.list = list;
		this.name = name;
		this.file = data.file;
		this.variantNames = ( data.variants || [] )
			.concat( this.list.getVariants().getGlobalVariantNames() )
			.filter( function ( variant, index, variants ) {
				return variants.indexOf( variant ) === index;
			} );
	}

	/**
	 * Generate CSS and images.
	 *
	 * @param {Destination} destination Destination
	 * @return {Q.Promise}
	 */
	Image.prototype.generate = function ( destination ) {
		// TODO Make configurable
		function getDeclarations( primary ) {
			// If 'primary' is not a SVG file, 'fallback' and 'primary' are intentionally the same
			var fallback = primary.replace( /\.svg$/, '.png' );
			return '.oo-ui-background-image-svg2(' +
				'\'' + ( cssPrependPath || '' ) + primary + '\', ' +
				'\'' + ( cssPrependPath || '' ) + fallback + '\'' +
				')';
		}
		function variantizeFileName( fileName, variantName ) {
			if ( variantName ) {
				return fileName.replace( /\.(\w+)$/, '-' + variantName + '.$1' );
			}
			return fileName;
		}

		var selector, declarations, direction, lang, langSelector,
			deferred = Q.defer(),
			file = typeof this.file === 'string' ?
				{ default: this.file } :
				{ ltr: this.file.ltr, rtl: this.file.rtl },
			moreLangs = this.file.lang || {},
			name = this.name,
			sourcePath = this.list.getPath(),
			destinationPath = destination.getPath(),
			variants = this.list.getVariants(),
			cssClassPrefix = this.list.getCssClassPrefix(),
			cssSelectors = this.list.getCssSelectors(),
			cssPrependPath = this.list.options.cssPrependPath,
			originalSvg = {},
			rules = {
				default: [],
				ltr: [],
				rtl: []
			},
			files = {},
			uncolorizableImages = [],
			unknownVariants = [];

		// Expand shorthands:
		// { "en,de,fr": "foo.svg" } → { "en": "foo.svg", "de": "foo.svg", "fr": "foo.svg" }
		moreLangs = Object.keys( moreLangs ).reduce( function ( langs, langList ) {
			langList.split( ',' ).forEach( function ( lang ) {
				langs[ lang ] = moreLangs[ langList ];
			} );
			return langs;
		}, {} );

		// Original
		selector = cssSelectors.selectorWithoutVariant
			.replace( /{prefix}/g, cssClassPrefix )
			.replace( /{name}/g, name )
			.replace( /{variant}/g, '' );

		for ( direction in file ) {
			declarations = getDeclarations( file[ direction ] );
			rules[ direction ].push( selector + ' {\n\t' + declarations + '\n}' );

			originalSvg[ direction ] = grunt.file.read(
				path.join( sourcePath, file[ direction ] )
			);
			files[ path.join( destinationPath, file[ direction ] ) ] = originalSvg[ direction ];

			for ( lang in moreLangs ) {
				if ( file[ direction ] === moreLangs[ lang ] ) {
					continue;
				}

				// This will not work for selectors ending in a pseudo-element.
				langSelector = ':lang(' + lang + ')';
				declarations = getDeclarations( moreLangs[ lang ] );
				rules[ direction ].push(
					selector.replace( /,|$/g, langSelector + '$&' ) +
					' {\n\t' + declarations + '\n}'
				);

				originalSvg[ 'lang-' + lang ] = grunt.file.read(
					path.join( sourcePath, moreLangs[ lang ] )
				);
				files[ path.join( destinationPath, moreLangs[ lang ] ) ] = originalSvg[ 'lang-' + lang ];
			}
		}

		// Variants
		this.variantNames.forEach( function ( variantName ) {
			var variantSvg, destinationFilePath,
				variant = variants.getVariant( variantName );

			if ( variant === undefined ) {
				unknownVariants.push( variantName );
				return;
			}

			selector = cssSelectors.selectorWithVariant
				.replace( /{prefix}/g, cssClassPrefix )
				.replace( /{name}/g, name )
				.replace( /{variant}/g, variantName );

			for ( direction in file ) {
				declarations = getDeclarations( variantizeFileName( file[ direction ], variantName ) );
				rules[ direction ].push( selector + ' {\n\t' + declarations + '\n}' );

				// TODO: Do this in a safer and more clever way
				variantSvg = originalSvg[ direction ].replace(
					/<svg[^>]*>/, '$&<style>* { fill: ' + variant.getColor() + ' }</style>'
				);

				if ( originalSvg[ direction ] === variantSvg ) {
					uncolorizableImages.push( file[ direction ] );
					continue;
				}

				destinationFilePath = path.join(
					destinationPath,
					variantizeFileName( file[ direction ], variantName )
				);
				files[ destinationFilePath ] = variantSvg;

				for ( lang in moreLangs ) {
					if ( file[ direction ] === moreLangs[ lang ] ) {
						continue;
					}

					langSelector = ':lang(' + lang + ')';
					declarations = getDeclarations( variantizeFileName( moreLangs[ lang ], variantName ) );
					rules[ direction ].push(
						selector.replace( /,|$/g, langSelector + '$&' ) +
						' {\n\t' + declarations + '\n}'
					);

					variantSvg = originalSvg[ 'lang-' + lang ].replace(
						/<svg[^>]*>/, '$&<style>* { fill: ' + variant.getColor() + ' }</style>'
					);

					if ( originalSvg[ 'lang-' + lang ] === variantSvg ) {
						uncolorizableImages.push( moreLangs[ lang ] );
						continue;
					}

					destinationFilePath = path.join(
						destinationPath,
						variantizeFileName( moreLangs[ lang ], variantName )
					);
					files[ destinationFilePath ] = variantSvg;
				}
			}
		} );

		if ( unknownVariants.length || uncolorizableImages.length ) {
			if ( unknownVariants.length ) {
				grunt.log.error(
					unknownVariants.length +
					' unknown variants requested: ' +
					unknownVariants.join( ', ' )
				);
			}
			if ( uncolorizableImages.length ) {
				grunt.log.error(
					uncolorizableImages.length +
					' invalid source images: ' +
					uncolorizableImages.join( ', ' )
				);
			}
			deferred.reject( 'Failed to generate some images' );
		} else {
			rules.ltr = rules.ltr.concat( rules.default );
			rules.rtl = rules.rtl.concat( rules.default );
			delete rules.default;
			deferred.resolve( {
				rules: rules,
				files: files
			} );
		}

		return deferred.promise;
	};

	/**
	 * List of source images.
	 *
	 * @class
	 *
	 * @constructor
	 * @param {string} path Images path
	 * @param {VariantList} variants Variants list
	 * @param {Object} options Additional options
	 * @param {Object} data List of image configurations keyed by name
	 */
	function ImageList( path, variants, options, data ) {
		var key;

		this.list = {};
		this.path = path;
		this.variants = variants;
		this.options = options;

		for ( key in data ) {
			this.list[ key ] = new Image( this, key, data[ key ] );
		}
	}

	/**
	 * Get image path.
	 *
	 * @return {string} Image path
	 */
	ImageList.prototype.getPath = function () {
		return this.path;
	};

	/**
	 * Get image variants.
	 *
	 * @return {VariantsList} Image variants
	 */
	ImageList.prototype.getVariants = function () {
		return this.variants;
	};

	/**
	 * Get CSS class prefix.
	 *
	 * @return {string} CSS class prefix
	 */
	ImageList.prototype.getCssClassPrefix = function () {
		return this.options.prefix || '';
	};

	/**
	 * Get CSS selectors.
	 *
	 * @return {Object.<string, string>} CSS selectors
	 */
	ImageList.prototype.getCssSelectors = function () {
		return {
			selectorWithoutVariant: this.options.selectorWithoutVariant || '.{prefix}-{name}',
			selectorWithVariant: this.options.selectorWithVariant || '.{prefix}-{name}-{variant}'
		};
	};

	/**
	 * Get CSS file intro.
	 *
	 * @return {string} CSS file intro
	 */
	ImageList.prototype.getCssIntro = function () {
		return this.options.intro || '';
	};

	/**
	 * Get number of images in list.
	 *
	 * @return {number} List length
	 */
	ImageList.prototype.getLength = function () {
		return Object.keys( this.list ).length;
	};

	/**
	 * Generate images and CSS.
	 *
	 * @param {Destination} destination Destination
	 * @return {Q.Promise} Promise resolved with number of generated SVG files
	 */
	ImageList.prototype.generate = function ( destination ) {
		var list = this.list,
			intro = this.getCssIntro();
		return Q.all( Object.keys( this.list ).map( function ( key ) {
			return list[ key ].generate( destination );
		} ) ).then( function ( data ) {
			var textDirection, stylesheetPath, destinationFilePath, dataFormat;
			dataFormat = {
				files: {},
				rules: {
					ltr: [],
					rtl: []
				}
			};

			data = data.reduce( function ( a, b ) {
				for ( destinationFilePath in b.files ) {
					// This de-duplicates the entries, as the same file can be used by many Images
					a.files[ destinationFilePath ] = b.files[ destinationFilePath ];
				}
				a.rules.ltr = a.rules.ltr.concat( b.rules.ltr );
				a.rules.rtl = a.rules.rtl.concat( b.rules.rtl );
				return a;
			}, dataFormat );

			for ( textDirection in data.rules ) {
				stylesheetPath = destination.getStylesheetPath( textDirection );
				grunt.file.write(
					stylesheetPath,
					intro + '\n' + data.rules[ textDirection ].join( '\n' )
				);
				grunt.log.writeln( 'Created "' + stylesheetPath + '".' );
			}
			for ( destinationFilePath in data.files ) {
				grunt.file.write( destinationFilePath, data.files[ destinationFilePath ] );
			}

			return Object.keys( data.files ).length;
		} );
	};

	/**
	 * Image variant.
	 *
	 * @class
	 *
	 * @constructor
	 * @param {VariantList} list Variant list
	 * @param {string} name Variant name
	 * @param {Object} data Variant options
	 */
	function Variant( list, name, data ) {
		// Properties
		this.list = list;
		this.name = name;
		this.color = data.color;
		this.global = data.global;
	}

	/**
	 * Check if variant is global.
	 *
	 * @return {boolean} Variant is global
	 */
	Variant.prototype.isGlobal = function () {
		return this.global;
	};

	/**
	 * Get variant color.
	 *
	 * @return {string} CSS color expression
	 */
	Variant.prototype.getColor = function () {
		return this.color;
	};

	/**
	 * List of variants.
	 *
	 * @class
	 *
	 * @constructor
	 * @param {Object} list List of variant configurations keyed by name
	 */
	function VariantList( data ) {
		var key;

		this.list = {};
		this.globals = [];

		for ( key in data ) {
			this.list[ key ] = new Variant( this, key, data[ key ] );
			if ( this.list[ key ].isGlobal() ) {
				this.globals.push( key );
			}
		}
	}

	/**
	 * Get names of global variants.
	 *
	 * @return {string[]} Global variant names
	 */
	VariantList.prototype.getGlobalVariantNames = function () {
		return this.globals;
	};

	/**
	 * Get variant by name.
	 *
	 * @param {string} name Variant name
	 * @return {Variant|undefined} Variant with matching name, or undefined of none exists.
	 */
	VariantList.prototype.getVariant = function ( name ) {
		return this.list[ name ];
	};

	/**
	 * Get number of variants in list.
	 *
	 * @return {number} List length
	 */
	VariantList.prototype.getLength = function () {
		return Object.keys( this.list ).length;
	};

};
<|endoftext|>