<commit_before>// CartoDB SQL API
//
// all requests expect the following URL args:
// - `sql` {String} SQL to execute
//
// for private (read/write) queries:
// - OAuth. Must have proper OAuth 1.1 headers. For OAuth 1.1 spec see Google
//
// eg. /api/v1/?sql=SELECT 1 as one (with a load of OAuth headers or URL arguments)
//
// for public (read only) queries:
// - sql only, provided the subdomain exists in CartoDB and the table's sharing options are public
//
// eg. vizzuality.cartodb.com/api/v1/?sql=SELECT * from my_table
//
//

var path = require('path');

var express = require('express')
    , app      = express.createServer(
    express.logger({
        buffer: true,
        format: '[:date] :req[X-Real-IP] \033[90m:method\033[0m \033[36m:req[Host]:url\033[0m \033[90m:status :response-time ms -> :res[Content-Type]\033[0m'
    }))
    , Step        = require('step')
    , crypto      = require('crypto')
    , fs          = require('fs')
    , zlib        = require('zlib')
    , util        = require('util')
    , spawn       = require('child_process').spawn
    , Meta        = require(global.settings.app_root + '/app/models/metadata')
    , oAuth       = require(global.settings.app_root + '/app/models/oauth')
    , PSQL        = require(global.settings.app_root + '/app/models/psql')
    , ApiKeyAuth  = require(global.settings.app_root + '/app/models/apikey_auth')
    , _           = require('underscore')
    , LRU         = require('lru-cache')
    , formats     = require(global.settings.app_root + '/app/models/formats')
    ;

var tableCache = LRU({
  // store no more than these many items in the cache
  max: global.settings.tableCacheMax || 8192,
  // consider entries expired after these many milliseconds (10 minutes by default)
  maxAge: global.settings.tableCacheMaxAge || 1000*60*10
});

function pad(n) { return n < 10 ? '0' + n : n }
Date.prototype.toJSON = function() {
  var s = this.getFullYear() + '-'
      + pad(this.getMonth() + 1) + '-'
      + pad(this.getDate()) + 'T'
      + pad(this.getHours()) + ':'
      + pad(this.getMinutes()) + ':'
      + pad(this.getSeconds());
  var offset = this.getTimezoneOffset();
  if (offset == 0) s += 'Z';
  else {
    s += ( offset < 0 ? '+' : '-' )
      + pad(Math.abs(offset / 60))
      + pad(Math.abs(offset % 60))

  }
  return s;
}

app.use(express.bodyParser());
app.enable('jsonp callback');
app.set("trust proxy", true);

// basic routing
app.options('*', function(req,res) { setCrossDomain(res); res.end(); });
app.all(global.settings.base_url+'/sql',     function(req, res) { handleQuery(req, res) } );
app.all(global.settings.base_url+'/sql.:f',  function(req, res) { handleQuery(req, res) } );
app.get(global.settings.base_url+'/cachestatus',  function(req, res) { handleCacheStatus(req, res) } );

// Return true of the given query may write to the database
//
// NOTE: this is a fuzzy check, the return could be true even
//       if the query doesn't really write anything.
//       But you can be pretty sure of a false return.
//
function queryMayWrite(sql) {
  var mayWrite = false;
  var pattern = RegExp("(alter|insert|update|delete|create|drop|truncate)", "i");
  if ( pattern.test(sql) ) {
    mayWrite = true;
  }
  return mayWrite;
}

function sanitize_filename(filename) {
  filename = path.basename(filename, path.extname(filename));
  filename = filename.replace(/[;()\[\]<>'"\s]/g, '_');
  //console.log("Sanitized: " + filename);
  return filename;
}

// request handlers
function handleQuery(req, res) {

    //var supportedFormats = ['json', 'geojson', 'topojson', 'csv', 'svg', 'shp', 'kml', 'arraybuffer'];

    // extract input
    var body      = (req.body) ? req.body : {};
    _.extend(req.query, body);
    var sql       = req.query.q;
    var api_key   = req.query.api_key;
    var database  = req.query.database; // TODO: Deprecate
    var limit     = parseInt(req.query.rows_per_page);
    var offset    = parseInt(req.query.page);
    var requestedFormat = req.query.format;
    var format    = _.isArray(requestedFormat) ? _.last(requestedFormat) : requestedFormat;
    var requestedFilename = req.query.filename;
    var filename  = requestedFilename;
    var requestedSkipfields = req.query.skipfields;
    var skipfields;
    var dp        = req.query.dp; // decimal point digits (defaults to 6)
    var gn        = "the_geom"; // TODO: read from configuration file
    var user_id;
    var tableCacheItem;
    var requestProtocol = req.protocol;

    try {

        // sanitize and apply defaults to input
        dp        = (dp       === "" || _.isUndefined(dp))       ? '6'  : dp;
        format    = (format   === "" || _.isUndefined(format))   ? 'json' : format.toLowerCase();
        filename  = (filename === "" || _.isUndefined(filename)) ? 'cartodb-query' : sanitize_filename(filename);
        sql       = (sql      === "" || _.isUndefined(sql))      ? null : sql;
        database  = (database === "" || _.isUndefined(database)) ? null : database;
        limit     = (!_.isNaN(limit))  ? limit : null;
        offset    = (!_.isNaN(offset)) ? offset * limit : null;
        console.log("offset", offset);

        // Accept both comma-separated string or array of comma-separated strings
        if ( requestedSkipfields ) {
          if ( _.isString(requestedSkipfields) ) skipfields = requestedSkipfields.split(',');
          else if ( _.isArray(requestedSkipfields) ) {
            skipfields = [];
            _.each(requestedSkipfields, function(ele) {
              skipfields = skipfields.concat(ele.split(','));
            });
          }
        } else {
          skipfields = [];
        }

        //if ( -1 === supportedFormats.indexOf(format) )
        if ( ! formats.hasOwnProperty(format) )
          throw new Error("Invalid format: " + format);

        if (!_.isString(sql)) throw new Error("You must indicate a sql query");

        // initialise MD5 key of sql for cache lookups
        var sql_md5 = generateMD5(sql);

        // placeholder for connection
        var pg;

        var authenticated;

        var formatter;

        // 1. Get database from redis via the username stored in the host header subdomain
        // 2. Run the request through OAuth to get R/W user id if signed
        // 3. Get the list of tables affected by the query
        // 4. Setup headers
        // 5. Send formatted results back
        Step(
            function getDatabaseName() {
                if (_.isNull(database)) {
                    Meta.getDatabase(req, this);
                } else {
                    // database hardcoded in query string (deprecated??): don't use redis
                    return database;
                }
            },
            function setDBGetUser(err, data) {
                if (err) throw err;

                database = (data === "" || _.isNull(data) || _.isUndefined(data)) ? database : data;

                // If the database could not be found, the user is non-existant
                if (_.isNull(database)) {
                    var msg = "Sorry, we can't find this CartoDB. Please check that you have entered the correct domain.";
                    err = new Error(msg);
                    err.http_status = 404;
                    throw err;
                }

                if(api_key) {
                    ApiKeyAuth.verifyRequest(req, this);
                } else {
                    oAuth.verifyRequest(req, this, requestProtocol);
                }
            },
            function queryExplain(err, data){
                if (err) throw err;
                user_id = data;
                // store postgres connection
                pg = new PSQL(user_id, database);

                authenticated = ! _.isNull(user_id);

                // get all the tables from Cache or SQL
                tableCacheItem = tableCache.get(sql_md5);
                if (tableCacheItem) {
                   tableCacheItem.hits++;
                   return false;
                } else {
                   pg.query("SELECT CDB_QueryTables($quotesql$" + sql + "$quotesql$)", this);
                }
            },
            function setHeaders(err, result){
                if (err) throw err;

                // store explain result in local Cache
                if ( ! tableCacheItem ) {

                    if ( result.rowCount === 1 ) {
                      tableCacheItem = {
                        affected_tables: result.rows[0].cdb_querytables,
                        // check if query may possibly write
                        may_write: queryMayWrite(sql),
                        // initialise hit counter
                        hits: 1
                      };
                      tableCache.set(sql_md5, tableCacheItem);
                    } else {
                      console.log("[ERROR] Unexpected result from CDB_QueryTables($quotesql$" + sql + "$quotesql$)");
                      console.dir(result);
                    }
                }

                if ( tableCacheItem ) {
                    var affected_tables = tableCacheItem.affected_tables.split(/^\{(.*)\}$/)[1].split(',');
                    for ( var i=0; i<affected_tables.length; ++i ) {
                      var t = affected_tables[i];
                      if ( t.match(/\.?pg_/) ) {
                        var e = new SyntaxError("system tables are forbidden");
                        e.http_status = 403;
                        throw(e);
                      }
                    }
                }

                var fClass = formats[format]
                formatter = new fClass();

                // configure headers for given format
                var use_inline = !requestedFormat && !requestedFilename;
                res.header("Content-Disposition", getContentDisposition(format, filename, use_inline));
                res.header("Content-Type", formatter.getContentType());

                // allow cross site post
                setCrossDomain(res);

                // set cache headers
                res.header('X-Cache-Channel', generateCacheKey(database, tableCacheItem, authenticated));
                var cache_policy = req.query.cache_policy;
                if ( cache_policy == 'persist' ) {
                  res.header('Cache-Control', 'public,max-age=31536000'); // 1 year
                } else {
                  // don't cache the result !
                  // NOTE: both Varnish and Cloudfront ignore this header,
                  //       X-Cache-Channel is for controlling Varnish
                  //       Cloudfront can only be skipped perturbating the URL
                  //
                  res.header('Last-Modified', new Date().toUTCString());
                  res.header('Cache-Control', 'no-cache,max-age=0,must-revalidate,public');
                }

                return result;
            },
            function generateFormat(err, result){
                if (err) throw err;

                // TODO: drop this, fix UI!
                sql = PSQL.window_sql(sql,limit,offset);

                var opts = {
                  sink: res,
                  gn: gn,
                  dp: dp,
                  skipfields: skipfields,
                  database: database,
                  user_id: user_id,
                  sql: sql,
                  filename: filename
                }

                formatter.sendResponse(opts, this);
            },
            function errorHandle(err){
                if ( err ) handleException(err, res);
            }
        );
    } catch (err) {
        console.log('[ERROR]\n' + err);
        handleException(err, res);
    }
}

function handleCacheStatus(req, res){
    var tableCacheValues = tableCache.values();
    var totalExplainHits = _.reduce(tableCacheValues, function(memo, res) { return memo + res.hits}, 0);
    var totalExplainKeys = tableCacheValues.length;
    res.send({explain: {pid: process.pid, hits: totalExplainHits, keys : totalExplainKeys }});
}

// TODO: delegate to formats
function getContentDisposition(format, filename, inline) {
    var ext = 'json';
    if (format === 'geojson'){
        ext = 'geojson';
    }
    else if (format === 'topojson'){
        ext = 'topojson';
    }
    else if (format === 'csv'){
        ext = 'csv';
    }
    else if (format === 'svg'){
        ext = 'svg';
    }
    else if (format === 'shp'){
        ext = 'zip';
    }
    else if (format === 'kml'){
        ext = 'kml';
    }
    var time = new Date().toUTCString();
    return ( inline ? 'inline' : 'attachment' ) +'; filename=' + filename + '.' + ext + '; modification-date="' + time + '";';
}

function setCrossDomain(res){
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "X-Requested-With, X-Prototype-Version, X-CSRF-Token");
}

function generateCacheKey(database, query_info, is_authenticated){
    if ( ! query_info || ( is_authenticated && query_info.may_write ) ) {
      return "NONE";
    } else {
      return database + ":" + query_info.affected_tables.split(/^\{(.*)\}$/)[1];
    }
}

function generateMD5(data){
    var hash = crypto.createHash('md5');
    hash.update(data);
    return hash.digest('hex');
}

function handleException(err, res){
    var msg = (global.settings.environment == 'development') ? {error:[err.message], stack: err.stack} : {error:[err.message]}
    if (global.settings.environment !== 'test'){
        // TODO: email this Exception report
        console.log("EXCEPTION REPORT")
        console.log(err.message);
        console.log(err.stack);
    }

    // allow cross site post
    setCrossDomain(res);

    // Force inline content disposition
    res.header("Content-Disposition", 'inline');

    // if the exception defines a http status code, use that, else a 400
    if (!_.isUndefined(err.http_status)){
        res.send(msg, err.http_status);
    } else {
        res.send(msg, 400);
    }
}

module.exports = app;
<commit_msg>removed debug console.log<commit_after>// CartoDB SQL API
//
// all requests expect the following URL args:
// - `sql` {String} SQL to execute
//
// for private (read/write) queries:
// - OAuth. Must have proper OAuth 1.1 headers. For OAuth 1.1 spec see Google
//
// eg. /api/v1/?sql=SELECT 1 as one (with a load of OAuth headers or URL arguments)
//
// for public (read only) queries:
// - sql only, provided the subdomain exists in CartoDB and the table's sharing options are public
//
// eg. vizzuality.cartodb.com/api/v1/?sql=SELECT * from my_table
//
//

var path = require('path');

var express = require('express')
    , app      = express.createServer(
    express.logger({
        buffer: true,
        format: '[:date] :req[X-Real-IP] \033[90m:method\033[0m \033[36m:req[Host]:url\033[0m \033[90m:status :response-time ms -> :res[Content-Type]\033[0m'
    }))
    , Step        = require('step')
    , crypto      = require('crypto')
    , fs          = require('fs')
    , zlib        = require('zlib')
    , util        = require('util')
    , spawn       = require('child_process').spawn
    , Meta        = require(global.settings.app_root + '/app/models/metadata')
    , oAuth       = require(global.settings.app_root + '/app/models/oauth')
    , PSQL        = require(global.settings.app_root + '/app/models/psql')
    , ApiKeyAuth  = require(global.settings.app_root + '/app/models/apikey_auth')
    , _           = require('underscore')
    , LRU         = require('lru-cache')
    , formats     = require(global.settings.app_root + '/app/models/formats')
    ;

var tableCache = LRU({
  // store no more than these many items in the cache
  max: global.settings.tableCacheMax || 8192,
  // consider entries expired after these many milliseconds (10 minutes by default)
  maxAge: global.settings.tableCacheMaxAge || 1000*60*10
});

function pad(n) { return n < 10 ? '0' + n : n }
Date.prototype.toJSON = function() {
  var s = this.getFullYear() + '-'
      + pad(this.getMonth() + 1) + '-'
      + pad(this.getDate()) + 'T'
      + pad(this.getHours()) + ':'
      + pad(this.getMinutes()) + ':'
      + pad(this.getSeconds());
  var offset = this.getTimezoneOffset();
  if (offset == 0) s += 'Z';
  else {
    s += ( offset < 0 ? '+' : '-' )
      + pad(Math.abs(offset / 60))
      + pad(Math.abs(offset % 60))

  }
  return s;
}

app.use(express.bodyParser());
app.enable('jsonp callback');
app.set("trust proxy", true);

// basic routing
app.options('*', function(req,res) { setCrossDomain(res); res.end(); });
app.all(global.settings.base_url+'/sql',     function(req, res) { handleQuery(req, res) } );
app.all(global.settings.base_url+'/sql.:f',  function(req, res) { handleQuery(req, res) } );
app.get(global.settings.base_url+'/cachestatus',  function(req, res) { handleCacheStatus(req, res) } );

// Return true of the given query may write to the database
//
// NOTE: this is a fuzzy check, the return could be true even
//       if the query doesn't really write anything.
//       But you can be pretty sure of a false return.
//
function queryMayWrite(sql) {
  var mayWrite = false;
  var pattern = RegExp("(alter|insert|update|delete|create|drop|truncate)", "i");
  if ( pattern.test(sql) ) {
    mayWrite = true;
  }
  return mayWrite;
}

function sanitize_filename(filename) {
  filename = path.basename(filename, path.extname(filename));
  filename = filename.replace(/[;()\[\]<>'"\s]/g, '_');
  //console.log("Sanitized: " + filename);
  return filename;
}

// request handlers
function handleQuery(req, res) {

    //var supportedFormats = ['json', 'geojson', 'topojson', 'csv', 'svg', 'shp', 'kml', 'arraybuffer'];

    // extract input
    var body      = (req.body) ? req.body : {};
    _.extend(req.query, body);
    var sql       = req.query.q;
    var api_key   = req.query.api_key;
    var database  = req.query.database; // TODO: Deprecate
    var limit     = parseInt(req.query.rows_per_page);
    var offset    = parseInt(req.query.page);
    var requestedFormat = req.query.format;
    var format    = _.isArray(requestedFormat) ? _.last(requestedFormat) : requestedFormat;
    var requestedFilename = req.query.filename;
    var filename  = requestedFilename;
    var requestedSkipfields = req.query.skipfields;
    var skipfields;
    var dp        = req.query.dp; // decimal point digits (defaults to 6)
    var gn        = "the_geom"; // TODO: read from configuration file
    var user_id;
    var tableCacheItem;
    var requestProtocol = req.protocol;

    try {

        // sanitize and apply defaults to input
        dp        = (dp       === "" || _.isUndefined(dp))       ? '6'  : dp;
        format    = (format   === "" || _.isUndefined(format))   ? 'json' : format.toLowerCase();
        filename  = (filename === "" || _.isUndefined(filename)) ? 'cartodb-query' : sanitize_filename(filename);
        sql       = (sql      === "" || _.isUndefined(sql))      ? null : sql;
        database  = (database === "" || _.isUndefined(database)) ? null : database;
        limit     = (!_.isNaN(limit))  ? limit : null;
        offset    = (!_.isNaN(offset)) ? offset * limit : null;

        // Accept both comma-separated string or array of comma-separated strings
        if ( requestedSkipfields ) {
          if ( _.isString(requestedSkipfields) ) skipfields = requestedSkipfields.split(',');
          else if ( _.isArray(requestedSkipfields) ) {
            skipfields = [];
            _.each(requestedSkipfields, function(ele) {
              skipfields = skipfields.concat(ele.split(','));
            });
          }
        } else {
          skipfields = [];
        }

        //if ( -1 === supportedFormats.indexOf(format) )
        if ( ! formats.hasOwnProperty(format) )
          throw new Error("Invalid format: " + format);

        if (!_.isString(sql)) throw new Error("You must indicate a sql query");

        // initialise MD5 key of sql for cache lookups
        var sql_md5 = generateMD5(sql);

        // placeholder for connection
        var pg;

        var authenticated;

        var formatter;

        // 1. Get database from redis via the username stored in the host header subdomain
        // 2. Run the request through OAuth to get R/W user id if signed
        // 3. Get the list of tables affected by the query
        // 4. Setup headers
        // 5. Send formatted results back
        Step(
            function getDatabaseName() {
                if (_.isNull(database)) {
                    Meta.getDatabase(req, this);
                } else {
                    // database hardcoded in query string (deprecated??): don't use redis
                    return database;
                }
            },
            function setDBGetUser(err, data) {
                if (err) throw err;

                database = (data === "" || _.isNull(data) || _.isUndefined(data)) ? database : data;

                // If the database could not be found, the user is non-existant
                if (_.isNull(database)) {
                    var msg = "Sorry, we can't find this CartoDB. Please check that you have entered the correct domain.";
                    err = new Error(msg);
                    err.http_status = 404;
                    throw err;
                }

                if(api_key) {
                    ApiKeyAuth.verifyRequest(req, this);
                } else {
                    oAuth.verifyRequest(req, this, requestProtocol);
                }
            },
            function queryExplain(err, data){
                if (err) throw err;
                user_id = data;
                // store postgres connection
                pg = new PSQL(user_id, database);

                authenticated = ! _.isNull(user_id);

                // get all the tables from Cache or SQL
                tableCacheItem = tableCache.get(sql_md5);
                if (tableCacheItem) {
                   tableCacheItem.hits++;
                   return false;
                } else {
                   pg.query("SELECT CDB_QueryTables($quotesql$" + sql + "$quotesql$)", this);
                }
            },
            function setHeaders(err, result){
                if (err) throw err;

                // store explain result in local Cache
                if ( ! tableCacheItem ) {

                    if ( result.rowCount === 1 ) {
                      tableCacheItem = {
                        affected_tables: result.rows[0].cdb_querytables,
                        // check if query may possibly write
                        may_write: queryMayWrite(sql),
                        // initialise hit counter
                        hits: 1
                      };
                      tableCache.set(sql_md5, tableCacheItem);
                    } else {
                      console.log("[ERROR] Unexpected result from CDB_QueryTables($quotesql$" + sql + "$quotesql$)");
                      console.dir(result);
                    }
                }

                if ( tableCacheItem ) {
                    var affected_tables = tableCacheItem.affected_tables.split(/^\{(.*)\}$/)[1].split(',');
                    for ( var i=0; i<affected_tables.length; ++i ) {
                      var t = affected_tables[i];
                      if ( t.match(/\.?pg_/) ) {
                        var e = new SyntaxError("system tables are forbidden");
                        e.http_status = 403;
                        throw(e);
                      }
                    }
                }

                var fClass = formats[format]
                formatter = new fClass();

                // configure headers for given format
                var use_inline = !requestedFormat && !requestedFilename;
                res.header("Content-Disposition", getContentDisposition(format, filename, use_inline));
                res.header("Content-Type", formatter.getContentType());

                // allow cross site post
                setCrossDomain(res);

                // set cache headers
                res.header('X-Cache-Channel', generateCacheKey(database, tableCacheItem, authenticated));
                var cache_policy = req.query.cache_policy;
                if ( cache_policy == 'persist' ) {
                  res.header('Cache-Control', 'public,max-age=31536000'); // 1 year
                } else {
                  // don't cache the result !
                  // NOTE: both Varnish and Cloudfront ignore this header,
                  //       X-Cache-Channel is for controlling Varnish
                  //       Cloudfront can only be skipped perturbating the URL
                  //
                  res.header('Last-Modified', new Date().toUTCString());
                  res.header('Cache-Control', 'no-cache,max-age=0,must-revalidate,public');
                }

                return result;
            },
            function generateFormat(err, result){
                if (err) throw err;

                // TODO: drop this, fix UI!
                sql = PSQL.window_sql(sql,limit,offset);

                var opts = {
                  sink: res,
                  gn: gn,
                  dp: dp,
                  skipfields: skipfields,
                  database: database,
                  user_id: user_id,
                  sql: sql,
                  filename: filename
                }

                formatter.sendResponse(opts, this);
            },
            function errorHandle(err){
                if ( err ) handleException(err, res);
            }
        );
    } catch (err) {
        console.log('[ERROR]\n' + err);
        handleException(err, res);
    }
}

function handleCacheStatus(req, res){
    var tableCacheValues = tableCache.values();
    var totalExplainHits = _.reduce(tableCacheValues, function(memo, res) { return memo + res.hits}, 0);
    var totalExplainKeys = tableCacheValues.length;
    res.send({explain: {pid: process.pid, hits: totalExplainHits, keys : totalExplainKeys }});
}

// TODO: delegate to formats
function getContentDisposition(format, filename, inline) {
    var ext = 'json';
    if (format === 'geojson'){
        ext = 'geojson';
    }
    else if (format === 'topojson'){
        ext = 'topojson';
    }
    else if (format === 'csv'){
        ext = 'csv';
    }
    else if (format === 'svg'){
        ext = 'svg';
    }
    else if (format === 'shp'){
        ext = 'zip';
    }
    else if (format === 'kml'){
        ext = 'kml';
    }
    var time = new Date().toUTCString();
    return ( inline ? 'inline' : 'attachment' ) +'; filename=' + filename + '.' + ext + '; modification-date="' + time + '";';
}

function setCrossDomain(res){
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "X-Requested-With, X-Prototype-Version, X-CSRF-Token");
}

function generateCacheKey(database, query_info, is_authenticated){
    if ( ! query_info || ( is_authenticated && query_info.may_write ) ) {
      return "NONE";
    } else {
      return database + ":" + query_info.affected_tables.split(/^\{(.*)\}$/)[1];
    }
}

function generateMD5(data){
    var hash = crypto.createHash('md5');
    hash.update(data);
    return hash.digest('hex');
}

function handleException(err, res){
    var msg = (global.settings.environment == 'development') ? {error:[err.message], stack: err.stack} : {error:[err.message]}
    if (global.settings.environment !== 'test'){
        // TODO: email this Exception report
        console.log("EXCEPTION REPORT")
        console.log(err.message);
        console.log(err.stack);
    }

    // allow cross site post
    setCrossDomain(res);

    // Force inline content disposition
    res.header("Content-Disposition", 'inline');

    // if the exception defines a http status code, use that, else a 400
    if (!_.isUndefined(err.http_status)){
        res.send(msg, err.http_status);
    } else {
        res.send(msg, 400);
    }
}

module.exports = app;
<|endoftext|>


<commit_before>/**
 * File Contents Description.
 *
 * Site Kit by Google, Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */
import invariant from 'invariant';

// Actions
const SET_REGISTRY_KEY = 'SET_REGISTRY_KEY';

const INITIAL_STATE = {
	registryKey: undefined,
};

export const actions = {
	/**
	 * Sets the registryKey in the data store.
	 *
	 * @since n.e.x.t
	 *
	 * @param {(number|undefined)} registryKey The registryKey for a given store.
	 * @return {Object} Redux-style action.
	 */
	setRegistryKey( registryKey ) {
		invariant( registryKey, 'registryKey is required.' );
		return {
			payload: { registryKey },
			type: SET_REGISTRY_KEY,
		};
	},
};

/**
 * Registry reducer.
 *
 * @since n.e.x.t
 *
 * @param {Object}             state          Data store's state.
 * @param {Object}             action         Redux-style action.
 * @param {(number|undefined)} action.payload The registryKey for a given store.
 * @param {string}             action.type    The action type.
 * @return {Object} Data store's state
 */
export const reducer = ( state, { payload, type } ) => {
	switch ( type ) {
		case SET_REGISTRY_KEY: {
			const { registryKey } = payload;
			return {
				...state,
				registryKey,
			};
		}
		default: {
			return { ...state };
		}
	}
};

export const selectors = {
	/**
	 * Returns the registry key being used for a given store.
	 *
	 * @since n.e.x.t
	 * @private
	 *
	 * @param {Object} state Data store's state.
	 * @return {(number|undefined)} The registryKey for a given store.
	 */
	getRegistryKey( state ) {
		const { registryKey } = state;

		return registryKey;
	},
};

export default {
	INITIAL_STATE,
	actions,
	reducer,
	selectors,
};
<commit_msg>Apply suggestions from code review<commit_after>/**
 * File Contents Description.
 *
 * Site Kit by Google, Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */
import invariant from 'invariant';

// Actions
const SET_REGISTRY_KEY = 'SET_REGISTRY_KEY';

const INITIAL_STATE = {
	registryKey: undefined,
};

export const actions = {
	/**
	 * Sets the registryKey in the data store.
	 *
	 * @since n.e.x.t
	 *
	 * @param {string} registryKey The registryKey for a given store.
	 * @return {Object} Redux-style action.
	 */
	setRegistryKey( registryKey ) {
		invariant( registryKey, 'registryKey is required.' );
		return {
			payload: { registryKey },
			type: SET_REGISTRY_KEY,
		};
	},
};

/**
 * Registry reducer.
 *
 * @since n.e.x.t
 *
 * @param {Object}             state          Data store's state.
 * @param {Object}             action         Redux-style action.
 * @return {Object} Data store's state
 */
export const reducer = ( state, { payload, type } ) => {
	switch ( type ) {
		case SET_REGISTRY_KEY: {
			const { registryKey } = payload;
			return {
				...state,
				registryKey,
			};
		}
		default: {
			return { ...state };
		}
	}
};

export const selectors = {
	/**
	 * Returns the registry key being used for a given store.
	 *
	 * @since n.e.x.t
	 * @private
	 *
	 * @param {Object} state Data store's state.
	 * @return {(string|undefined)} The registryKey for a given store. Returns `undefined` if the key has not yet been set.
	 */
	getRegistryKey( state ) {
		const { registryKey } = state;

		return registryKey;
	},
};

export default {
	INITIAL_STATE,
	actions,
	reducer,
	selectors,
};
<|endoftext|>


<commit_before>'use strict';

var fs = require('fs');
var assign = require('lodash-node/modern/objects/assign');

var file = '/tmp/tomo.json';
var UTF_8 = 'utf-8';

function reviver(key, val) {
    if (/time$/i.test(key)) {
        return new Date(val);
    }
    return val;
}

function save(data) {
    var newData = assign({}, data, {
        bedTime: new Date()
    });
    var json = JSON.stringify(newData, null, 4) + '\n';
    fs.writeFileSync(file, json, UTF_8);
}

function load() {
    try {
        var json = fs.readFileSync(file, UTF_8);
        var obj = JSON.parse(json, reviver);
        return obj;
    } catch (e) {
        return Object.freeze({
            version: 1,
            bedTime: new Date(),
            fullness: 10
        });
    }
}

module.exports = {
    save: save,
    load: load
};
<commit_msg>Adds version to tomo.json<commit_after>'use strict';

var fs = require('fs');
var assign = require('lodash-node/modern/objects/assign');

var file = '/tmp/tomo.json';
var UTF_8 = 'utf-8';

function reviver(key, val) {
    if (/time$/i.test(key)) {
        return new Date(val);
    }
    return val;
}

function save(data) {
    var newData = assign({}, data, {
        version: 1,
        bedTime: new Date()
    });
    var json = JSON.stringify(newData, null, 4) + '\n';
    fs.writeFileSync(file, json, UTF_8);
}

function load() {
    try {
        var json = fs.readFileSync(file, UTF_8);
        var obj = JSON.parse(json, reviver);
        return obj;
    } catch (e) {
        return Object.freeze({
            version: 1,
            bedTime: new Date(),
            fullness: 10
        });
    }
}

module.exports = {
    save: save,
    load: load
};
<|endoftext|>


<commit_before>/*
 * Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */
import styled from 'styled-components';
import { useCallback, useLayoutEffect, useMemo, useRef } from 'react';
import { useFeature } from 'flagged';

/**
 * WordPress dependencies
 */
import { __, sprintf } from '@wordpress/i18n';

/**
 * Internal dependencies
 */
import { BEZIER } from '../../../animation';
import { trackEvent } from '../../../tracking';
import { useConfig } from '../../app/config';
import { resolveRoute, useRouteHistory } from '../../app/router';
import {
  BUTTON_TYPES,
  primaryPaths,
  secondaryPaths,
  Z_INDEX,
  APP_ROUTES,
} from '../../constants';
import { DASHBOARD_LEFT_NAV_WIDTH } from '../../constants/pageStructure';
import useFocusOut from '../../utils/useFocusOut';
import { useNavContext } from '../navProvider';
import {
  AppInfo,
  Content,
  NavButton,
  NavLink,
  NavList,
  NavListItem,
  Rule,
  WebStoriesHeading,
} from './navigationComponents';

export const AppFrame = styled.div`
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
`;

export const PageContent = styled.div`
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: ${({ fullWidth }) =>
    fullWidth ? '0' : `${DASHBOARD_LEFT_NAV_WIDTH}px`};

  @media ${({ theme }) => theme.breakpoint.tablet} {
    left: 0;
  }
`;

export const LeftRailContainer = styled.nav.attrs({
  ['data-testid']: 'dashboard-left-rail',
})`
  position: absolute;
  display: flex;
  justify-content: space-between;
  flex-direction: column;
  top: 0;
  bottom: 0;
  width: ${DASHBOARD_LEFT_NAV_WIDTH}px;
  background: ${({ theme }) => theme.colors.white};
  border-right: ${({ theme }) => theme.borders.gray50};
  z-index: ${Z_INDEX.LAYOUT_FIXED};
  transition: transform 0.25s ${BEZIER.outCubic}, opacity 0.25s linear;

  @media ${({ theme }) => theme.breakpoint.tablet} {
    padding-left: 0;
    opacity: ${({ isOpen }) => (isOpen ? 1 : 0)};
    visibility: ${({ isOpen }) => (isOpen ? 'visible' : 'hidden')};
    transform: translateX(${({ isOpen }) => (isOpen ? 'none' : `-100%`)});
  }
`;

export function LeftRail() {
  const { state } = useRouteHistory();
  const {
    newStoryURL,
    version,
    capabilities: { canManageSettings } = {},
  } = useConfig();
  const leftRailRef = useRef(null);
  const upperContentRef = useRef(null);

  const enableInProgressViews = useFeature('enableInProgressViews');
  const enableSettingsViews =
    useFeature('enableSettingsView') && canManageSettings;

  const {
    state: { sideBarVisible },
    actions: { toggleSideBar },
  } = useNavContext();

  const onContainerClickCapture = useCallback(
    ({ target }) => {
      if (
        target === leftRailRef.current ||
        target === upperContentRef.current
      ) {
        return;
      }
      toggleSideBar();
    },
    [toggleSideBar, leftRailRef, upperContentRef]
  );

  const enabledPaths = useMemo(() => {
    if (enableInProgressViews) {
      return primaryPaths;
    }
    return primaryPaths.filter((path) => !path.inProgress);
  }, [enableInProgressViews]);

  const enabledSecondaryPaths = useMemo(() => {
    let copyOfSecondaryPaths = enableSettingsViews
      ? [...secondaryPaths]
      : secondaryPaths.filter(
          (path) => !path.value.includes(APP_ROUTES.EDITOR_SETTINGS)
        );

    if (enableInProgressViews) {
      return copyOfSecondaryPaths;
    }
    return copyOfSecondaryPaths.filter((path) => !path.inProgress);
  }, [enableInProgressViews, enableSettingsViews]);

  const handleSideBarClose = useCallback(() => {
    if (sideBarVisible) {
      toggleSideBar();
    }
  }, [toggleSideBar, sideBarVisible]);

  useFocusOut(leftRailRef, handleSideBarClose, [sideBarVisible]);

  useLayoutEffect(() => {
    if (sideBarVisible && leftRailRef.current) {
      leftRailRef.current.focus();
    }
  }, [sideBarVisible]);

  const onCreateNewStoryClick = useCallback(async () => {
    await trackEvent('dashboard', 'create_new_story');
  }, []);

  return (
    <LeftRailContainer
      onClickCapture={onContainerClickCapture}
      ref={leftRailRef}
      isOpen={sideBarVisible}
      tabIndex={-1}
      role="navigation"
      aria-label={__('Main dashboard navigation', 'web-stories')}
    >

        <Content>
          <WebStoriesHeading>
            {__('Web Stories', 'web-stories')}
          </WebStoriesHeading>
        </Content>
        <Content>
          <NavButton
            type={BUTTON_TYPES.CTA}
            href={newStoryURL}
            isLink
            onClick={onCreateNewStoryClick}
          >
            {__('Create New Story', 'web-stories')}
          </NavButton>
        </Content>
        <Content>
          <NavList>
            {enabledPaths.map((path) => (
              <NavListItem key={path.value}>
                <NavLink
                  active={path.value === state.currentPath}
                  href={resolveRoute(path.value)}
                >
                  {path.label}
                </NavLink>
              </NavListItem>
            ))}
          </NavList>
        </Content>
        <Rule />
        <Content>
          <NavList>
            {enabledSecondaryPaths.map((path) => (
              <NavListItem key={path.value}>
                <NavLink
                  active={path.value === state.currentPath}
                  href={resolveRoute(path.value)}
                >
                  {path.label}
                </NavLink>
              </NavListItem>
            ))}
          </NavList>
        </Content>

      <Content>
        <AppInfo>
          {__('\u00A9 2020 Google', 'web-stories')}

          {sprintf(
            /* translators: %s: editor version. */
            __('Version %s', 'web-stories'),
            version
          )}
        </AppInfo>
      </Content>
    </LeftRailContainer>
  );
}

export { default as NavMenuButton } from './menuButton';
<commit_msg>change the "create story" and "use template" CTAs to be primary buttons because the focus style is consistent with the rest of the app and button style changes are coming with design system. This will hopefully hold us over, helping indicate to keyboard users what is in focus without any jarring temporary styles<commit_after>/*
 * Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * External dependencies
 */
import styled from 'styled-components';
import { useCallback, useLayoutEffect, useMemo, useRef } from 'react';
import { useFeature } from 'flagged';

/**
 * WordPress dependencies
 */
import { __, sprintf } from '@wordpress/i18n';

/**
 * Internal dependencies
 */
import { BEZIER } from '../../../animation';
import { trackEvent } from '../../../tracking';
import { useConfig } from '../../app/config';
import { resolveRoute, useRouteHistory } from '../../app/router';
import {
  BUTTON_TYPES,
  primaryPaths,
  secondaryPaths,
  Z_INDEX,
  APP_ROUTES,
} from '../../constants';
import { DASHBOARD_LEFT_NAV_WIDTH } from '../../constants/pageStructure';
import useFocusOut from '../../utils/useFocusOut';
import { useNavContext } from '../navProvider';
import {
  AppInfo,
  Content,
  NavButton,
  NavLink,
  NavList,
  NavListItem,
  Rule,
  WebStoriesHeading,
} from './navigationComponents';

export const AppFrame = styled.div`
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
`;

export const PageContent = styled.div`
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: ${({ fullWidth }) =>
    fullWidth ? '0' : `${DASHBOARD_LEFT_NAV_WIDTH}px`};

  @media ${({ theme }) => theme.breakpoint.tablet} {
    left: 0;
  }
`;

export const LeftRailContainer = styled.nav.attrs({
  ['data-testid']: 'dashboard-left-rail',
})`
  position: absolute;
  display: flex;
  justify-content: space-between;
  flex-direction: column;
  top: 0;
  bottom: 0;
  width: ${DASHBOARD_LEFT_NAV_WIDTH}px;
  background: ${({ theme }) => theme.colors.white};
  border-right: ${({ theme }) => theme.borders.gray50};
  z-index: ${Z_INDEX.LAYOUT_FIXED};
  transition: transform 0.25s ${BEZIER.outCubic}, opacity 0.25s linear;

  @media ${({ theme }) => theme.breakpoint.tablet} {
    padding-left: 0;
    opacity: ${({ isOpen }) => (isOpen ? 1 : 0)};
    visibility: ${({ isOpen }) => (isOpen ? 'visible' : 'hidden')};
    transform: translateX(${({ isOpen }) => (isOpen ? 'none' : `-100%`)});
  }
`;

export function LeftRail() {
  const { state } = useRouteHistory();
  const {
    newStoryURL,
    version,
    capabilities: { canManageSettings } = {},
  } = useConfig();
  const leftRailRef = useRef(null);
  const upperContentRef = useRef(null);

  const enableInProgressViews = useFeature('enableInProgressViews');
  const enableSettingsViews =
    useFeature('enableSettingsView') && canManageSettings;

  const {
    state: { sideBarVisible },
    actions: { toggleSideBar },
  } = useNavContext();

  const onContainerClickCapture = useCallback(
    ({ target }) => {
      if (
        target === leftRailRef.current ||
        target === upperContentRef.current
      ) {
        return;
      }
      toggleSideBar();
    },
    [toggleSideBar, leftRailRef, upperContentRef]
  );

  const enabledPaths = useMemo(() => {
    if (enableInProgressViews) {
      return primaryPaths;
    }
    return primaryPaths.filter((path) => !path.inProgress);
  }, [enableInProgressViews]);

  const enabledSecondaryPaths = useMemo(() => {
    let copyOfSecondaryPaths = enableSettingsViews
      ? [...secondaryPaths]
      : secondaryPaths.filter(
          (path) => !path.value.includes(APP_ROUTES.EDITOR_SETTINGS)
        );

    if (enableInProgressViews) {
      return copyOfSecondaryPaths;
    }
    return copyOfSecondaryPaths.filter((path) => !path.inProgress);
  }, [enableInProgressViews, enableSettingsViews]);

  const handleSideBarClose = useCallback(() => {
    if (sideBarVisible) {
      toggleSideBar();
    }
  }, [toggleSideBar, sideBarVisible]);

  useFocusOut(leftRailRef, handleSideBarClose, [sideBarVisible]);

  useLayoutEffect(() => {
    if (sideBarVisible && leftRailRef.current) {
      leftRailRef.current.focus();
    }
  }, [sideBarVisible]);

  const onCreateNewStoryClick = useCallback(async () => {
    await trackEvent('dashboard', 'create_new_story');
  }, []);

  return (
    <LeftRailContainer
      onClickCapture={onContainerClickCapture}
      ref={leftRailRef}
      isOpen={sideBarVisible}
      tabIndex={-1}
      role="navigation"
      aria-label={__('Main dashboard navigation', 'web-stories')}
    >

        <Content>
          <WebStoriesHeading>
            {__('Web Stories', 'web-stories')}
          </WebStoriesHeading>
        </Content>
        <Content>
          <NavButton
            type={BUTTON_TYPES.PRIMARY}
            href={newStoryURL}
            isLink
            onClick={onCreateNewStoryClick}
          >
            {__('Create New Story', 'web-stories')}
          </NavButton>
        </Content>
        <Content>
          <NavList>
            {enabledPaths.map((path) => (
              <NavListItem key={path.value}>
                <NavLink
                  active={path.value === state.currentPath}
                  href={resolveRoute(path.value)}
                >
                  {path.label}
                </NavLink>
              </NavListItem>
            ))}
          </NavList>
        </Content>
        <Rule />
        <Content>
          <NavList>
            {enabledSecondaryPaths.map((path) => (
              <NavListItem key={path.value}>
                <NavLink
                  active={path.value === state.currentPath}
                  href={resolveRoute(path.value)}
                >
                  {path.label}
                </NavLink>
              </NavListItem>
            ))}
          </NavList>
        </Content>

      <Content>
        <AppInfo>
          {__('\u00A9 2020 Google', 'web-stories')}

          {sprintf(
            /* translators: %s: editor version. */
            __('Version %s', 'web-stories'),
            version
          )}
        </AppInfo>
      </Content>
    </LeftRailContainer>
  );
}

export { default as NavMenuButton } from './menuButton';
<|endoftext|>


<commit_before>import React, {PropTypes as P} from 'react'
import {Row, Col} from 'react-bootstrap'
import {connect} from 'react-redux'
import {loadServers} from './services/servers/actions'
import Server from './components/Server'

import './styles.styl'

class Servers extends React.Component {
  componentDidMount() {
    this.props.loadServers()
  }
  render() {
    if (!this.props.data || this.props.fetching) {
      return
Loading...

    }
    console.log(this.props.data.toJS())
    return (

          {this.props.data.map((server, idx) => {
            if (!server) {
              return
            }
            const serverInfo = server.get('server_info')
            const gameInfo = server.get('game_info')
            if (serverInfo.get('hidden')) {
              return
            }
            return (
              <Server serverInfo={serverInfo} gameInfo={gameInfo} />
            )
          })}

    )
  }
}

function mapStateToProps(state) {
  const {fetching, error, data} = state.servers
  return (
    { fetching
    , error
    , data
    })
}

export default connect(
  mapStateToProps,
  {loadServers}
)(Servers)
<commit_msg>Fix missing tbody tag.<commit_after>import React, {PropTypes as P} from 'react'
import {Row, Col} from 'react-bootstrap'
import {connect} from 'react-redux'
import {loadServers} from './services/servers/actions'
import Server from './components/Server'

import './styles.styl'

class Servers extends React.Component {
  componentDidMount() {
    this.props.loadServers()
  }
  render() {
    if (!this.props.data || this.props.fetching) {
      return
Loading...

    }
    console.log(this.props.data.toJS())
    return (

            {this.props.data.map((server, idx) => {
              if (!server) {
                return
              }
              const serverInfo = server.get('server_info')
              const gameInfo = server.get('game_info')
              if (serverInfo.get('hidden')) {
                return
              }
              return (
                <Server key={idx} serverInfo={serverInfo} gameInfo={gameInfo} />
              )
            })}

    )
  }
}

function mapStateToProps(state) {
  const {fetching, error, data} = state.servers
  return (
    { fetching
    , error
    , data
    })
}

export default connect(
  mapStateToProps,
  {loadServers}
)(Servers)
<|endoftext|>


<commit_before>'use strict';

var cwd = process.cwd();
var path = require('path');
var cmd = require('commander');
var fs = require('fs');
var pkg = require(path.join(cwd, '/package.json'));
var semver = require('semver');
var sh = require('shelljs');

function replace (file, pattern, replacement) {
  var str = fs.readFileSync(file).toString();
  str = str.replace(pattern, replacement);
  fs.writeFileSync(file, str);
};

cmd
  .option('-s, --semver [version]', 'The semantic version to release in lieu of --type. This takes precedence over --type.')
  .option('-t, --type [major, minor or patch]', 'The type of release being performed in lieu of --semver.')
  .parse(process.argv);

module.exports = function () {
  var currentVersion = pkg.version;
  var nextVersion = cmd.semver || semver.inc(
    currentVersion,
    cmd.type || 'patch'
  );

  console.log(currentVersion, nextVersion);

  // sh.exec('npm run lint');
  // sh.exec('npm run test');
  // replace('src/version.js', currentVersion, nextVersion);
  // replace('bower.json', currentVersion, nextVersion);
  // replace('package.json', currentVersion, nextVersion);
  // sh.exec('npm run build');
  // sh.exec('git commit -am "' + currentVersion + ' -> ' + nextVersion + '"');
  // sh.exec('git tag -a ' + nextVersion + ' -m ' + nextVersion);
  // sh.exec('git push');
  // sh.exec('git push --tags');
  // sh.exec('npm publish');
};
<commit_msg>Update release task.<commit_after>'use strict';

var cwd = process.cwd();
var path = require('path');
var fs = require('fs');
var pkg = require(path.join(cwd, '/package.json'));
var semver = require('semver');
var sh = require('shelljs');

function replace (file, pattern, replacement) {
  var str = fs.readFileSync(file).toString();
  str = str.replace(pattern, replacement);
  fs.writeFileSync(file, str);
};

module.exports = function () {
  var currentVersion = pkg.version;
  var nextVersion = this.semver || semver.inc(
    currentVersion,
    this.type || 'patch'
  );

  sh.exec('npm run lint');
  sh.exec('npm run test');
  replace('src/version.js', currentVersion, nextVersion);
  replace('bower.json', currentVersion, nextVersion);
  replace('package.json', currentVersion, nextVersion);
  sh.exec('npm run build');
  sh.exec('git commit -am "' + currentVersion + ' -> ' + nextVersion + '"');
  sh.exec('git tag -a ' + nextVersion + ' -m ' + nextVersion);
  sh.exec('git push');
  sh.exec('git push --tags');
  sh.exec('npm publish');
};
<|endoftext|>


<commit_before>/**
 * @fileOverview graph
 * @author huangtonger@aliyun.com
 */

const EventEmitter = require('@antv/g/lib/').EventEmitter;
const G = require('@antv/g');
const Util = require('../util');
const Global = require('../global');
const Item = require('../item');

const Controller = require('./controller');

class Graph extends EventEmitter {
  /**
   * Access to the default configuration properties
   * @return {object} default configuration
   */
  getDefaultCfg() {
    return {
      /**
       * Container could be dom object or dom id
       * @type {object|string|undefined}
       */
      container: undefined,

      /**
       * Canvas width
       * @type {number|undefined}
       * unit pixel if undefined force fit width
       */
      width: undefined,

      /**
       * Canvas height
       * @type {number|undefined}
       * unit pixel if undefined force fit height
       */
      height: undefined,
      /**
       * renderer canvas or svg
       * @type {string}
       */
      renderer: 'canvas',
      /**
       * control graph behaviors
       * @type Array
       */
      mode: [],
      /**
       * source data
       * @type object
       */
      data: null,
      /**
       *  capture events
       *  @type boolean
       */
      event: true,
      /**
       * group node & edges into different graphic groups
       * @private
       * @type boolean
       */
      groupByTypes: true,
      /**
       * determine if it's a directed graph
       * @type boolean
       */
      directed: false,
      /**
       * when data or shape changed, should canvas draw automatically
       */
      autoPaint: true
    };
  }

  constructor(inputCfg) {
    super();
    this._cfg = Util.mix({}, this.getDefaultCfg(), inputCfg);    // merge graph configs
    this.node = [];                                              // all the node instances
    this.edge = [];                                              // all the edge instances
    this.itemById = {};                                          // all the item indexed by id
    this._init();
  }
  _init() {
    this._initCanvas();
    const eventController = new Controller.Event(this);
    const viewController = new Controller.FitView(this);
    const modeController = new Controller.Mode(this);
    this.set({ eventController, viewController, modeController });
    if (this.get('fitView')) {
      viewController.fitView();
    }
  }
  _initCanvas() {
    let container = this.get('container');
    if (Util.isString(container)) {
      container = document.getElementById(container);
      this.set('container', container);
    }
    if (!container) {
      throw Error('invalid container');
    }
    const canvas = new G.Canvas({
      containerDOM: container,
      width: this.get('width'),
      height: this.get('height'),
      renderer: this.get('renderer'),
      pixelRatio: this.get('pixelRatio')
    });
    this.canvas = canvas;
    const id = canvas.get('el').id;
    const group = canvas.addGroup({ id: id + '-root', className: Global.rootContainerClassName });
    if (this.get('groupByTypes')) {
      const edgeGroup = group.addGroup({ id: id + '-edge', className: Global.edgeContainerClassName });
      const nodeGroup = group.addGroup({ id: id + '-node', className: Global.nodeContainerClassName });
      this.set({ nodeGroup, edgeGroup });
    }
    this.set({ canvas, group });
  }
  get(key) {
    return this._cfg[key];
  }
  set(key, val) {
    if (Util.isPlainObject(key)) {
      this._cfg = Util.mix({}, this._cfg, key);
    } else {
      this._cfg[key] = val;
    }
    return this;
  }
  update(item, cfg) {
    if (Util.isString(item)) {
      item = this.itemById[item];
    }
    item.update(cfg);
    if (this.get('autoPaint')) {
      this.paint();
    }
    return item;
  }
  add(type, model) {
    const parent = this.get(type + 'Group') || this.get('group');
    let source = model.source;
    let target = model.target;
    if (type === 'edge') {
      if (source && Util.isString(source)) {
        source = this.itemById[source];
      }
      if (target && Util.isString(target)) {
        target = this.itemById[target];
      }
    }
    const item = new Item[Util.upperFirst(type)]({
      model,
      source,
      target,
      group: parent.addGroup()
    });
    // 这个地方存一个 Map 就好了，为什么数组和map 都存储
    this[type].push(item);
    this.itemById[item.get('id')] = item;
    return item;
  }
  remove(item) {
    if (Util.isString(item)) {
      item = this.itemById[item];
    }
    if (!item) {
      return;
    }
    const items = this[item.get('type')];
    const index = items.indexOf(item);
    items.splice(index, 1);
    delete this.itemById[item.get('id')];
    item.destroy();
  }
  data(data) {
    this.set('data', data);
  }
  render() {
    const self = this;
    const data = this.get('data');
    if (!data) {
      throw new Error('data must be defined first');
    }
    this.clear();
    let item;
    this.emit('beforerender');
    Util.each(data, model => {
      item = new Item[model.type](model);
      self[item].push(item);
    });
    this.paint();
    this.emit('afterrender');
  }
  changeData(data) {
    if (!data) {
      return;
    }
    const self = this;
    const items = {
      node: [],
      edge: []
    };
    let item;
    Util.each(data, model => {
      item = self.itemById[model.id];
      if (item) {
        item._update(model);
      } else {
        item = new Item[model.type](model);
      }
      items[model.type].push(item);
    });
    Util.each(self.itemById, item => {
      if (items.node.indexOf(item) < 0 && items.edge.indexOf(item) < 0) {
        self.remove(item);
      }
    });
    this.node = items.node;
    this.edge = items.edge;
    this.paint();
    return this;
  }
  paint() {
    this.get('canvas').draw();
  }
  /**
   * @return {object} data
   */
  save() {
    // TODO
    return this;
  }
  /**
   * change canvas size
   * @param  {number} width  input width
   * @param  {number} height input height
   * @return {object} this
   */
  changeSize(width, height) {
    this.get('viewController').changeSize(width, height);
    return this;
  }
  _updateMatrix(matrix) {
    const rootGroup = this.get('group');
    const minZoom = this.get('minZoom');
    const maxZoom = this.get('maxZoom');
    if (minZoom && matrix.elements[0] < minZoom) {
      return;
    }
    if (maxZoom && matrix.elements[0] > maxZoom) {
      return;
    }
    rootGroup.setMatrix(matrix);
  }
  translate(dx, dy) {
    this.get('group').translate(dx, dy);
  }
  moveTo(x, y) {
    this.get('group').move(x, y);
  }
  fitView() {
    this.get('viewController').fitView();
  }
  addBehaviors(behaviors, modes) {
    this.get('modeController').manipulateBehaviors(behaviors, modes, true);
    return this;
  }
  removeBehaviors(behaviors, modes) {
    this.get('modeController').manipulateBehaviors(behaviors, modes, false);
    return this;
  }
  setMode(mode) {
    this.set('mode', mode);
    this.get('modeController').setMode(mode);
    return this;
  }
  currentMode() {
    return this.get('mode');
  }
  getZoom() {
    return this.get('group').getMatrix()[0];
  }
  zoom(ratio, center) {
    const matrix = Util.clone(this.get('group').getMatrix());
    if (center) {
      Util.mat3.translate(matrix, matrix, [ -center.x, -center.y ]);
      Util.mat3.scale(matrix, matrix, [ ratio, ratio ]);
      Util.mat3.translate(matrix, matrix, [ center.x, center.y ]);
    } else {
      Util.mat3.scale(matrix, matrix, [ ratio, ratio ]);
    }
    this._updateMatrix(matrix);
  }
  focus(item) {
    this.get('ViewController').focus(item);
  }
  findById(id) {
    return this.itemById[id];
  }
  find(type, fn) {
    let result;
    const items = this[type];
    Util.each(items, (item, i) => {
      if (fn(item, i)) {
        result = item;
        return false;
      }
    });
    return result;
  }
  findAll(type, fn) {
    const result = [];
    Util.each(this[type], (item, i) => {
      if (fn(item, i)) {
        result.push(item);
      }
    });
    return result;
  }
  setAutoPaint(auto) {
    this.set('autoPaint', auto);
  }
  clear() {
    const group = this.get('group');
    Util.each(this.node, node => {
      node.destroy();
    });
    Util.each(this.edge, edge => {
      edge.destroy();
    });
    group.clear();
    this.node = [];
    this.edge = [];
    return this;
  }
  destroy() {
    this.clear();
    this.get('eventController').destroy();
    this.canvas.destroy();
    this.itemById = null;
    this.destroyed = true;
    return this;
  }
}

module.exports = Graph;
<commit_msg>feat(behavior): set src & target before init edge<commit_after>/**
 * @fileOverview graph
 * @author huangtonger@aliyun.com
 */

const EventEmitter = require('@antv/g/lib/').EventEmitter;
const G = require('@antv/g');
const Util = require('../util');
const Global = require('../global');
const Item = require('../item');

const Controller = require('./controller');

class Graph extends EventEmitter {
  /**
   * Access to the default configuration properties
   * @return {object} default configuration
   */
  getDefaultCfg() {
    return {
      /**
       * Container could be dom object or dom id
       * @type {object|string|undefined}
       */
      container: undefined,

      /**
       * Canvas width
       * @type {number|undefined}
       * unit pixel if undefined force fit width
       */
      width: undefined,

      /**
       * Canvas height
       * @type {number|undefined}
       * unit pixel if undefined force fit height
       */
      height: undefined,
      /**
       * renderer canvas or svg
       * @type {string}
       */
      renderer: 'canvas',
      /**
       * control graph behaviors
       * @type Array
       */
      mode: [],
      /**
       * source data
       * @type object
       */
      data: null,
      /**
       *  capture events
       *  @type boolean
       */
      event: true,
      /**
       * group node & edges into different graphic groups
       * @private
       * @type boolean
       */
      groupByTypes: true,
      /**
       * determine if it's a directed graph
       * @type boolean
       */
      directed: false,
      /**
       * when data or shape changed, should canvas draw automatically
       */
      autoPaint: true
    };
  }

  constructor(inputCfg) {
    super();
    this._cfg = Util.mix({}, this.getDefaultCfg(), inputCfg);    // merge graph configs
    this.node = [];                                              // all the node instances
    this.edge = [];                                              // all the edge instances
    this.itemById = {};                                          // all the item indexed by id
    this._init();
  }
  _init() {
    this._initCanvas();
    const eventController = new Controller.Event(this);
    const viewController = new Controller.FitView(this);
    const modeController = new Controller.Mode(this);
    this.set({ eventController, viewController, modeController });
    if (this.get('fitView')) {
      viewController.fitView();
    }
  }
  _initCanvas() {
    let container = this.get('container');
    if (Util.isString(container)) {
      container = document.getElementById(container);
      this.set('container', container);
    }
    if (!container) {
      throw Error('invalid container');
    }
    const canvas = new G.Canvas({
      containerDOM: container,
      width: this.get('width'),
      height: this.get('height'),
      renderer: this.get('renderer'),
      pixelRatio: this.get('pixelRatio')
    });
    this.canvas = canvas;
    const id = canvas.get('el').id;
    const group = canvas.addGroup({ id: id + '-root', className: Global.rootContainerClassName });
    if (this.get('groupByTypes')) {
      const edgeGroup = group.addGroup({ id: id + '-edge', className: Global.edgeContainerClassName });
      const nodeGroup = group.addGroup({ id: id + '-node', className: Global.nodeContainerClassName });
      this.set({ nodeGroup, edgeGroup });
    }
    this.set({ canvas, group });
  }
  get(key) {
    return this._cfg[key];
  }
  set(key, val) {
    if (Util.isPlainObject(key)) {
      this._cfg = Util.mix({}, this._cfg, key);
    } else {
      this._cfg[key] = val;
    }
    return this;
  }
  update(item, cfg) {
    if (Util.isString(item)) {
      item = this.itemById[item];
    }
    item.update(cfg);
    if (this.get('autoPaint')) {
      this.paint();
    }
    return item;
  }
  add(type, model) {
    const parent = this.get(type + 'Group') || this.get('group');
    let item;
    if (type === 'edge') {
      let source = model.source;
      let target = model.target;
      if (source && Util.isString(source)) {
        source = this.itemById[source];
      }
      if (target && Util.isString(target)) {
        target = this.itemById[target];
      }
      item = new Item[Util.upperFirst(type)]({
        model,
        source,
        target,
        group: parent.addGroup()
      });
    } else {
      item = new Item[Util.upperFirst(type)]({
        model,
        group: parent.addGroup()
      });
    }
    // 这个地方存一个 Map 就好了，为什么数组和map 都存储
    this[type].push(item);
    this.itemById[item.get('id')] = item;
    return item;
  }
  remove(item) {
    if (Util.isString(item)) {
      item = this.itemById[item];
    }
    if (!item) {
      return;
    }
    const items = this[item.get('type')];
    const index = items.indexOf(item);
    items.splice(index, 1);
    delete this.itemById[item.get('id')];
    item.destroy();
  }
  data(data) {
    this.set('data', data);
  }
  render() {
    const self = this;
    const data = this.get('data');
    if (!data) {
      throw new Error('data must be defined first');
    }
    this.clear();
    let item;
    this.emit('beforerender');
    Util.each(data, model => {
      item = new Item[model.type](model);
      self[item].push(item);
    });
    this.paint();
    this.emit('afterrender');
  }
  changeData(data) {
    if (!data) {
      return;
    }
    const self = this;
    const items = {
      node: [],
      edge: []
    };
    let item;
    Util.each(data, model => {
      item = self.itemById[model.id];
      if (item) {
        item._update(model);
      } else {
        item = new Item[model.type](model);
      }
      items[model.type].push(item);
    });
    Util.each(self.itemById, item => {
      if (items.node.indexOf(item) < 0 && items.edge.indexOf(item) < 0) {
        self.remove(item);
      }
    });
    this.node = items.node;
    this.edge = items.edge;
    this.paint();
    return this;
  }
  paint() {
    this.get('canvas').draw();
  }
  /**
   * @return {object} data
   */
  save() {
    // TODO
    return this;
  }
  /**
   * change canvas size
   * @param  {number} width  input width
   * @param  {number} height input height
   * @return {object} this
   */
  changeSize(width, height) {
    this.get('viewController').changeSize(width, height);
    return this;
  }
  _updateMatrix(matrix) {
    const rootGroup = this.get('group');
    const minZoom = this.get('minZoom');
    const maxZoom = this.get('maxZoom');
    if (minZoom && matrix.elements[0] < minZoom) {
      return;
    }
    if (maxZoom && matrix.elements[0] > maxZoom) {
      return;
    }
    rootGroup.setMatrix(matrix);
  }
  translate(dx, dy) {
    this.get('group').translate(dx, dy);
  }
  moveTo(x, y) {
    this.get('group').move(x, y);
  }
  fitView() {
    this.get('viewController').fitView();
  }
  addBehaviors(behaviors, modes) {
    this.get('modeController').manipulateBehaviors(behaviors, modes, true);
    return this;
  }
  removeBehaviors(behaviors, modes) {
    this.get('modeController').manipulateBehaviors(behaviors, modes, false);
    return this;
  }
  setMode(mode) {
    this.set('mode', mode);
    this.get('modeController').setMode(mode);
    return this;
  }
  currentMode() {
    return this.get('mode');
  }
  getZoom() {
    return this.get('group').getMatrix()[0];
  }
  zoom(ratio, center) {
    const matrix = Util.clone(this.get('group').getMatrix());
    if (center) {
      Util.mat3.translate(matrix, matrix, [ -center.x, -center.y ]);
      Util.mat3.scale(matrix, matrix, [ ratio, ratio ]);
      Util.mat3.translate(matrix, matrix, [ center.x, center.y ]);
    } else {
      Util.mat3.scale(matrix, matrix, [ ratio, ratio ]);
    }
    this._updateMatrix(matrix);
  }
  focus(item) {
    this.get('ViewController').focus(item);
  }
  findById(id) {
    return this.itemById[id];
  }
  find(type, fn) {
    let result;
    const items = this[type];
    Util.each(items, (item, i) => {
      if (fn(item, i)) {
        result = item;
        return false;
      }
    });
    return result;
  }
  findAll(type, fn) {
    const result = [];
    Util.each(this[type], (item, i) => {
      if (fn(item, i)) {
        result.push(item);
      }
    });
    return result;
  }
  setAutoPaint(auto) {
    this.set('autoPaint', auto);
  }
  clear() {
    const group = this.get('group');
    Util.each(this.node, node => {
      node.destroy();
    });
    Util.each(this.edge, edge => {
      edge.destroy();
    });
    group.clear();
    this.node = [];
    this.edge = [];
    return this;
  }
  destroy() {
    this.clear();
    this.get('eventController').destroy();
    this.canvas.destroy();
    this.itemById = null;
    this.destroyed = true;
    return this;
  }
}

module.exports = Graph;
<|endoftext|>


<commit_before>/*
 * grunt-coffee
 * https://github.com/avalade/grunt-coffee
 *
 * Copyright (c) 2012 Aaron D. Valade
 * Licensed under the MIT license.
 */

module.exports = function(grunt) {
  var path = require('path');

  // Please see the grunt documentation for more information regarding task and
  // helper creation: https://github.com/cowboy/grunt/blob/master/docs/toc.md

  // ==========================================================================
  // TASKS
  // ==========================================================================

  grunt.registerMultiTask('coffee', 'Compile CoffeeScript files', function() {
    var dest = this.file.dest,
        options = this.data.options;
    grunt.file.expandFiles(this.file.src).forEach(function(filepath) {
      grunt.helper('coffee', filepath, dest, options);
    });

    if (grunt.task.current.errorCount) {
      return false;
    }
  });

  // ==========================================================================
  // HELPERS
  // ==========================================================================

  grunt.registerHelper('coffee', function(src, destPath, options) {
    var coffee = require('coffee-script'),
        js = '';

    var dest = path.join(destPath,
                         path.basename(src, '.coffee') + '.js');

    options = options || {};
    if( options.bare !== false ) {
      options.bare = true;
    }

    try {
      js = coffee.compile(grunt.file.read(src), options);
    } catch (e) {
      grunt.log.error("Error in " + src + ":\n" + e);
    }
    if (this.errorCount) { return false; }
    grunt.file.write(dest, js);
  });
};
<commit_msg>deep clone options parameter<commit_after>/*
 * grunt-coffee
 * https://github.com/avalade/grunt-coffee
 *
 * Copyright (c) 2012 Aaron D. Valade
 * Licensed under the MIT license.
 */

module.exports = function(grunt) {
  var path = require('path');

  // Please see the grunt documentation for more information regarding task and
  // helper creation: https://github.com/cowboy/grunt/blob/master/docs/toc.md

  // ==========================================================================
  // TASKS
  // ==========================================================================

  grunt.registerMultiTask('coffee', 'Compile CoffeeScript files', function() {
    var dest = this.file.dest,
        options = this.data.options;
    grunt.file.expandFiles(this.file.src).forEach(function(filepath) {
      grunt.helper('coffee', filepath, dest, JSON.parse(JSON.stringify(options)));
    });

    if (grunt.task.current.errorCount) {
      return false;
    }
  });

  // ==========================================================================
  // HELPERS
  // ==========================================================================

  grunt.registerHelper('coffee', function(src, destPath, options) {
    var coffee = require('coffee-script'),
        js = '';

    var dest = path.join(destPath,
                         path.basename(src, '.coffee') + '.js');

    options = options || {};
    if( options.bare !== false ) {
      options.bare = true;
    }

    try {
      js = coffee.compile(grunt.file.read(src), options);
    } catch (e) {
      grunt.log.error("Error in " + src + ":\n" + e);
    }
    if (this.errorCount) { return false; }
    grunt.file.write(dest, js);
  });
};
<|endoftext|>


<commit_before>import config from '../config/config'
import { gsap, debug } from '../utils'
import Timelines from './timelines'
import { EventEmitter } from 'events'
import { emitChange } from '../utils/emitter'

/**
 * Group.
 */
class Group extends EventEmitter {

  _name = 'untitled'
  _timeScale = 1
  _timelines = new Timelines()

  timeline = null

  /**
   * Create a group instance.
   *
   * @param {object} props
   */
  constructor(props = {}) {
    super()
    this.setMaxListeners(Infinity)

    if (!props.name || typeof props.name !== 'string' || props.name.trim() === '') {
      throw new Error('Cannot create group without a name.')
    }

    const defaults = {
      name: 'untitled',
      timeScale: 1,
      timelines: new Timelines()
    }

    Object.assign(this, {
      ...defaults,
      ...props
    })
  }

  /**
   * Get timelines
   *
   * @returns {Timelines}
   */
  get timelines() {
    return this._timelines
  }

  /**
   * Set timelines
   *
   * @param {Timelines} timelines
   */
  @emitChange()
  set timelines(timelines) {
    if (!(timelines instanceof Timelines)) {
      timelines = new Timelines(Array.from(timelines))
    }
    this._timelines = timelines
  }

  /**
   * Get current timeScale
   *
   * @returns {number}
   */
  get timeScale() {
    return this._timeScale
  }

  /**
   * Set timeScale
   *
   * @param {number} scale
   */
  @emitChange()
  set timeScale(scale) {
    if (!(typeof scale === 'number' && isFinite(scale))) {
      throw new Error('timeScale needs to be a number')
    }

    if (this.timeline && this.timeline instanceof config.gsap.timeline) {
      this.timeline.timeScale(scale)
    }

    this._timeScale = scale
  }

  /**
   * Get the timeline duration.
   * Equal to this.timeline.duration()
   *
   * @returns {number}
   */
  get duration() {
    return this.timeline ? this.timeline.duration() : 0
  }

  /**
   * Set the timeline duration.
   * Updates the group timeScale
   *
   * @param {number} val
   */
  @emitChange()
  set duration(val) {
    if (this.timeline && this.timeline instanceof config.gsap.timeline) {
      this.timeline.duration(val)
      this.timeScale = this.timeline.timeScale()
      this._duration = this.timeline.duration()
    }
  }

  /**
   * Get name
   *
   * @returns {string}
   */
  get name() {
    return this._name
  }

  /**
   * Set name
   *
   * @param {string} name
   */
  @emitChange()
  set name(name) {
    if (typeof name !== 'string') {
      throw new Error('Name needs to be a string')
    }
    this._name = name
  }

  /**
   * Convert group to object
   *
   * @returns {object}
   */
  toObject() {
    const name = this.name
    const timeScale = this.timeScale
    const timelines = this.timelines.toArray()

    return { name, timeScale, timelines }
  }

  /**
   * Construct gsap timeline
   *
   * @returns {TimelineMax|TimelineLite}
   */
  construct() {
    try {
      if (!config.gsap.timeline || !config.gsap.tween) {
        if (debug) {
          console.warn(`
            Trying to construct group ${this.name}, but GSAP cannot be found.

            Did you forgot to call spirit.setup() ?

            @usage

                spirit.setup().then(function(){
                  // gsap is loaded here..
                })
          `)
        }
        throw new Error('GSAP cannot be found')
      }

      // initiate an empty GSAP timeline
      if (this.timeline && this.timeline instanceof config.gsap.timeline) {
        gsap.killTimeline(this.timeline)
      } else {
        this.timeline = new config.gsap.timeline({ paused: true }) // eslint-disable-line new-cap
      }

      // create a valid GSAP timeline out of timelines
      this.timelines.each(timeline => {
        if (timeline.type === 'dom') {
          const el = timeline.transformObject
          if (!(el instanceof window.Element)) {
            throw new Error('transformObject is not an Element')
          }
          this.timeline.add(gsap.generateTimeline(timeline).play(), 0, 'start')
        }
      })

      // update timescale
      this.timeline.timeScale(this.timeScale)
      this._duration = this.timeline.duration()
    } catch (err) {
      if (debug) {
        console.error(err)
      }
      throw new Error(`Could not construct timeline: ${err.message}`)
    }

    this.emit('construct', this.timeline)
    return this.timeline
  }

}

Group.fromObject = function(obj) {
  return new Group(obj)
}

export default Group
<commit_msg>Add group.reset which clears all animation data / attributes on elements<commit_after>import config from '../config/config'
import { gsap, debug } from '../utils'
import Timelines from './timelines'
import { EventEmitter } from 'events'
import { emitChange } from '../utils/emitter'

/**
 * Group.
 */
class Group extends EventEmitter {

  _name = 'untitled'
  _timeScale = 1
  _timelines = new Timelines()

  timeline = null

  /**
   * Create a group instance.
   *
   * @param {object} props
   */
  constructor(props = {}) {
    super()
    this.setMaxListeners(Infinity)

    if (!props.name || typeof props.name !== 'string' || props.name.trim() === '') {
      throw new Error('Cannot create group without a name.')
    }

    const defaults = {
      name: 'untitled',
      timeScale: 1,
      timelines: new Timelines()
    }

    Object.assign(this, {
      ...defaults,
      ...props
    })
  }

  /**
   * Get timelines
   *
   * @returns {Timelines}
   */
  get timelines() {
    return this._timelines
  }

  /**
   * Set timelines
   *
   * @param {Timelines} timelines
   */
  @emitChange()
  set timelines(timelines) {
    if (!(timelines instanceof Timelines)) {
      timelines = new Timelines(Array.from(timelines))
    }
    this._timelines = timelines
  }

  /**
   * Get current timeScale
   *
   * @returns {number}
   */
  get timeScale() {
    return this._timeScale
  }

  /**
   * Set timeScale
   *
   * @param {number} scale
   */
  @emitChange()
  set timeScale(scale) {
    if (!(typeof scale === 'number' && isFinite(scale))) {
      throw new Error('timeScale needs to be a number')
    }

    if (this.timeline && this.timeline instanceof config.gsap.timeline) {
      this.timeline.timeScale(scale)
    }

    this._timeScale = scale
  }

  /**
   * Get the timeline duration.
   * Equal to this.timeline.duration()
   *
   * @returns {number}
   */
  get duration() {
    return this.timeline ? this.timeline.duration() : 0
  }

  /**
   * Set the timeline duration.
   * Updates the group timeScale
   *
   * @param {number} val
   */
  @emitChange()
  set duration(val) {
    if (this.timeline && this.timeline instanceof config.gsap.timeline) {
      this.timeline.duration(val)
      this.timeScale = this.timeline.timeScale()
      this._duration = this.timeline.duration()
    }
  }

  /**
   * Get name
   *
   * @returns {string}
   */
  get name() {
    return this._name
  }

  /**
   * Set name
   *
   * @param {string} name
   */
  @emitChange()
  set name(name) {
    if (typeof name !== 'string') {
      throw new Error('Name needs to be a string')
    }
    this._name = name
  }

  /**
   * Convert group to object
   *
   * @returns {object}
   */
  toObject() {
    const name = this.name
    const timeScale = this.timeScale
    const timelines = this.timelines.toArray()

    return { name, timeScale, timelines }
  }

  reset() {
    if (this.timeline) {
      gsap.killTimeline(this.timeline)
    }
  }

  /**
   * Construct gsap timeline
   *
   * @returns {TimelineMax|TimelineLite}
   */
  construct() {
    try {
      if (!config.gsap.timeline || !config.gsap.tween) {
        if (debug) {
          console.warn(`
            Trying to construct group ${this.name}, but GSAP cannot be found.

            Did you forgot to call spirit.setup() ?

            @usage

                spirit.setup().then(function(){
                  // gsap is loaded here..
                })
          `)
        }
        throw new Error('GSAP cannot be found')
      }

      // initiate an empty GSAP timeline
      if (this.timeline && this.timeline instanceof config.gsap.timeline) {
        gsap.killTimeline(this.timeline)
      } else {
        this.timeline = new config.gsap.timeline({ paused: true }) // eslint-disable-line new-cap
      }

      // create a valid GSAP timeline out of timelines
      this.timelines.each(timeline => {
        if (timeline.type === 'dom') {
          const el = timeline.transformObject
          if (!(el instanceof window.Element)) {
            throw new Error('transformObject is not an Element')
          }
          this.timeline.add(gsap.generateTimeline(timeline).play(), 0, 'start')
        }
      })

      // update timescale
      this.timeline.timeScale(this.timeScale)
      this._duration = this.timeline.duration()
    } catch (err) {
      if (debug) {
        console.error(err)
      }
      throw new Error(`Could not construct timeline: ${err.message}`)
    }

    this.emit('construct', this.timeline)
    return this.timeline
  }

}

Group.fromObject = function(obj) {
  return new Group(obj)
}

export default Group
<|endoftext|>


<commit_before>

module.exports = function(grunt) {

    var jade = require('jade'),
        path = require('path'),
        fs = require('fs'),
        yaml = require('js-yaml'),
        htmlUtils = require('./lib/htmlUtils');

    var buildAndLinkHtml = htmlUtils.buildAndLinkHtml;

    var processMarkdown = function(parameters) {
        var docDir = parameters.docDir;
        var properties = parameters.properties;
        var normalizeHeaders = properties['normalizeHeaders'] === undefined ? true : properties['normalizeHeaders'];
        var htmlContent = buildAndLinkHtml({
            'docDir': docDir,
            'normalizeHeaders': normalizeHeaders
        }, properties, 1);
        var render = jade.compileFile(path.join(parameters.webDir, "index.jade"));
        htmlContent.guideLinks = htmlUtils.highlightCurrentGuide(parameters.guideLinks, parameters.guideFile);
        htmlContent.icon = parameters.icon.file;
        var output = render({
            "content": htmlContent,
            "title": properties["name"]
        });
        if (parameters.icon.style) {
            output = htmlUtils.applyIconStyle(output, parameters.icon.style);
        }

        grunt.file.write(path.join(parameters.output, parameters.guideFile), output);
    };

    var copyResources = function (options) {
        grunt.config('copy', {
            docmd_resources: {
                files: [
                    {
                        expand: true,
                        cwd: path.join(options.docs, options.resourcesName),
                        src: ["**/*"],
                        dest: path.join(options.output, options.resourcesName)
                    }
                ]
            },
            docmd_bower: {
                files: [
                    {
                        expand: true,
                        cwd: path.join(__dirname, "../bower_components"),
                        src: ["*/dist/**/*"],
                        dest: path.join(options.output, 'lib')
                    }
                ]
            },
            docmd_user_lib: {
                files: [
                    {
                        expand: true,
                        cwd: path.join(options.webDir, 'lib'),
                        src: ["*"],
                        dest: path.join(options.output, 'lib')
                    }
                ]
            }

        });
        grunt.task.run('copy:docmd_resources', 'copy:docmd_bower', 'copy:docmd_user_lib');
    };
    var setupDirectories = function (options) {
        grunt.config('clean', {
            options: {
                force: true
            },
            docmd_lib: [
                path.join(options.webDir, "lib")
            ],
            docmd_output: [
                options.output
            ]
        });
        grunt.task.run('clean:docmd_lib', 'clean:docmd_output');
        grunt.registerTask('docmd_setup', function () {
            grunt.file.mkdir(options.output);
            grunt.file.mkdir(path.join(options.webDir, 'lib'));
        });
        grunt.task.run('docmd_setup');
    };
    var handleWebDependencies = function (options) {
        process.chdir(path.join(__dirname, '../'));
        grunt.config('bower', {
            'options': {
                copy: false
            },
            'install': {}
        });
        grunt.task.run('bower');
        grunt.task.run('docmd_chdir');

        grunt.config('concat', {
            'docmd_user_js': {
                options: {
                    separator: ';\n'
                },
                src: [
                    path.join(options.jsDir, '*.js')
                ],
                dest: path.join(options.webDir, 'lib', 'user.js')
            },
            'docmd_user_css': {
                src: [
                    path.join(options.cssDir, '*.css')
                ],
                dest: path.join(options.webDir, 'lib', 'user.css')
            }
        });
        grunt.task.run('concat:docmd_user_js', 'concat:docmd_user_css');
    };

    grunt.registerTask('doc_md', function() {
        var options = this.options({
            webDir: path.join(__dirname, "../webpage"),
            resourcesName: "resources",
            jsDir: "./js",
            cssDir: "./css"
        });
        var dataDirs = options.directories;
        var base = grunt.option('base') || process.cwd();

        grunt.registerTask('docmd_chdir', function() {
            process.chdir(base);
        });

        if (!options.docs) {
            grunt.warn("docs directory not specified");
        }
        if (!options.output) {
            grunt.warn("output directory not specified");
        }
        if (!dataDirs || dataDirs.length == 0) {
            grunt.warn("markdown directory not specified");
        }

        grunt.loadNpmTasks('grunt-contrib-clean');
        grunt.loadNpmTasks('grunt-bower-concat');
        grunt.loadNpmTasks('grunt-contrib-concat');
        grunt.loadNpmTasks('grunt-contrib-copy');
        grunt.loadNpmTasks('grunt-bower-task');

        setupDirectories(options);

        handleWebDependencies(options);

        grunt.registerTask('docmd_markdown', function() {
            var guides = [];
            dataDirs.forEach(function(directory) {
                var guide = {};
                var propsFile = path.join(options.docs, directory, "properties.yml");
                var properties = yaml.safeLoad(fs.readFileSync(propsFile), {
                    onWarning: function() {
                        grunt.fail.warn("Could not properly parse " + propsFile + "as a yaml file");
                    }
                });
                guide.propertiesFile = properties;
                guide.link = properties['referenceId'] || htmlUtils.buildId(properties['name']) + '.html';
                guide.text = properties['name'];
                guide.directory = directory;
                guides.push(guide);
            });

            var guideLinksHtml = htmlUtils.buildGuideLinks(guides);

            guides.forEach(function(guide) {
                processMarkdown({
                    webDir: options.webDir,
                    properties: guide.propertiesFile,
                    guideFile: guide.link,
                    guideLinks: guideLinksHtml,
                    output: options.output,
                    docDir: path.join(options.docs, guide.directory),
                    icon: guide.propertiesFile.icon
                });
            });
        });
        grunt.task.run('docmd_markdown');

        copyResources(options);
    });
};

<commit_msg>Fixes a bug in handling guides without icons<commit_after>

module.exports = function(grunt) {

    var jade = require('jade'),
        path = require('path'),
        fs = require('fs'),
        yaml = require('js-yaml'),
        htmlUtils = require('./lib/htmlUtils');

    var buildAndLinkHtml = htmlUtils.buildAndLinkHtml;

    var processMarkdown = function(parameters) {
        var docDir = parameters.docDir;
        var properties = parameters.properties;
        var normalizeHeaders = properties['normalizeHeaders'] === undefined ? true : properties['normalizeHeaders'];
        var htmlContent = buildAndLinkHtml({
            'docDir': docDir,
            'normalizeHeaders': normalizeHeaders
        }, properties, 1);
        var render = jade.compileFile(path.join(parameters.webDir, "index.jade"));
        htmlContent.guideLinks = htmlUtils.highlightCurrentGuide(parameters.guideLinks, parameters.guideFile);
        if (parameters.icon) {
            htmlContent.icon = parameters.icon.file;
        }
        var output = render({
            "content": htmlContent,
            "title": properties["name"]
        });
        if (parameters.icon.style) {
            output = htmlUtils.applyIconStyle(output, parameters.icon.style);
        }

        grunt.file.write(path.join(parameters.output, parameters.guideFile), output);
    };

    var copyResources = function (options) {
        grunt.config('copy', {
            docmd_resources: {
                files: [
                    {
                        expand: true,
                        cwd: path.join(options.docs, options.resourcesName),
                        src: ["**/*"],
                        dest: path.join(options.output, options.resourcesName)
                    }
                ]
            },
            docmd_bower: {
                files: [
                    {
                        expand: true,
                        cwd: path.join(__dirname, "../bower_components"),
                        src: ["*/dist/**/*"],
                        dest: path.join(options.output, 'lib')
                    }
                ]
            },
            docmd_user_lib: {
                files: [
                    {
                        expand: true,
                        cwd: path.join(options.webDir, 'lib'),
                        src: ["*"],
                        dest: path.join(options.output, 'lib')
                    }
                ]
            }

        });
        grunt.task.run('copy:docmd_resources', 'copy:docmd_bower', 'copy:docmd_user_lib');
    };
    var setupDirectories = function (options) {
        grunt.config('clean', {
            options: {
                force: true
            },
            docmd_lib: [
                path.join(options.webDir, "lib")
            ],
            docmd_output: [
                options.output
            ]
        });
        grunt.task.run('clean:docmd_lib', 'clean:docmd_output');
        grunt.registerTask('docmd_setup', function () {
            grunt.file.mkdir(options.output);
            grunt.file.mkdir(path.join(options.webDir, 'lib'));
        });
        grunt.task.run('docmd_setup');
    };
    var handleWebDependencies = function (options) {
        process.chdir(path.join(__dirname, '../'));
        grunt.config('bower', {
            'options': {
                copy: false
            },
            'install': {}
        });
        grunt.task.run('bower');
        grunt.task.run('docmd_chdir');

        grunt.config('concat', {
            'docmd_user_js': {
                options: {
                    separator: ';\n'
                },
                src: [
                    path.join(options.jsDir, '*.js')
                ],
                dest: path.join(options.webDir, 'lib', 'user.js')
            },
            'docmd_user_css': {
                src: [
                    path.join(options.cssDir, '*.css')
                ],
                dest: path.join(options.webDir, 'lib', 'user.css')
            }
        });
        grunt.task.run('concat:docmd_user_js', 'concat:docmd_user_css');
    };

    grunt.registerTask('doc_md', function() {
        var options = this.options({
            webDir: path.join(__dirname, "../webpage"),
            resourcesName: "resources",
            jsDir: "./js",
            cssDir: "./css"
        });
        var dataDirs = options.directories;
        var base = grunt.option('base') || process.cwd();

        grunt.registerTask('docmd_chdir', function() {
            process.chdir(base);
        });

        if (!options.docs) {
            grunt.warn("docs directory not specified");
        }
        if (!options.output) {
            grunt.warn("output directory not specified");
        }
        if (!dataDirs || dataDirs.length == 0) {
            grunt.warn("markdown directory not specified");
        }

        grunt.loadNpmTasks('grunt-contrib-clean');
        grunt.loadNpmTasks('grunt-bower-concat');
        grunt.loadNpmTasks('grunt-contrib-concat');
        grunt.loadNpmTasks('grunt-contrib-copy');
        grunt.loadNpmTasks('grunt-bower-task');

        setupDirectories(options);

        handleWebDependencies(options);

        grunt.registerTask('docmd_markdown', function() {
            var guides = [];
            dataDirs.forEach(function(directory) {
                var guide = {};
                var propsFile = path.join(options.docs, directory, "properties.yml");
                var properties = yaml.safeLoad(fs.readFileSync(propsFile), {
                    onWarning: function() {
                        grunt.fail.warn("Could not properly parse " + propsFile + "as a yaml file");
                    }
                });
                guide.propertiesFile = properties;
                guide.link = properties['referenceId'] || htmlUtils.buildId(properties['name']) + '.html';
                guide.text = properties['name'];
                guide.directory = directory;
                guides.push(guide);
            });

            var guideLinksHtml = htmlUtils.buildGuideLinks(guides);

            guides.forEach(function(guide) {
                processMarkdown({
                    webDir: options.webDir,
                    properties: guide.propertiesFile,
                    guideFile: guide.link,
                    guideLinks: guideLinksHtml,
                    output: options.output,
                    docDir: path.join(options.docs, guide.directory),
                    icon: guide.propertiesFile.icon
                });
            });
        });
        grunt.task.run('docmd_markdown');

        copyResources(options);
    });
};

<|endoftext|>