<commit_before>/*
 * Assemble
 * https://github.com/assemble/assemble
 *
 * Copyright (c) 2012 Brian Woodward
 * Licensed under the MIT license.
 */

module.exports = function(grunt) {

  // Grunt utilities
  var file     = grunt.file;
  var log      = grunt.log;
  var kindOf   = grunt.util.kindOf;
  var _        = grunt.util._;

  var assemble = require('../lib/assemble');

  // external dependencies
  var path     = require('path');
  var fs       = require('fs');
  var util     = require('util');

  var extensions = assemble.Utils.ExtensionMap;

  grunt.registerMultiTask('assemble', 'Compile template files with specified engines', function(){

    var options = this.options({
      layout        : '',
      partials      : {},
      data          : {},
      assets        : 'dist/assets',
      ext           : '.html'
    });

    logBlock("options: ", util.inspect(options));
    logBlock("this.files: ", util.inspect(this.files));

    // try to get a src to use for configuration
    var src;
    this.files.forEach(function(fp) {
      src = fp.src;
    });

    if(!src || src.length === 0) {
      grunt.warn('No source files found.');
      return false;
    }

    // find an engine to use
    options.engine = options.engine || getEngineOf(src);
    if(!options.engine) {
      grunt.warn('No compatible engine available');
      return false;
    }

    var EngineLoader = options.EngineLoader = assemble.EngineLoader(options);
    var engine = null;
    EngineLoader.getEngine(function(err, results) {
      if(err) {
        console.log(err);
        return;
      }
      engine = options.engine = results;
    });

    var yamlPreprocessor = EngineLoader.getPreprocessor('YamlPreprocessor');

    var partials      = file.expand(options.partials);
    var dataFiles     = file.expand(options.data);
    var fileExt       = extension(src);
    var filenameRegex = /[^\\\/:*?"<>|\r\n]+$/i;
    var fileExtRegex  = new RegExp("\\." + fileExt + "$");

    options.filenameRegex = filenameRegex;
    options.fileExtRegex = fileExtRegex;

    var done = this.async();

    // clear out the partials and data objects on options
    options.partials = {};
    options.data = {};

    // load default layout
    var defaultLayoutName,
        defaultLayout,
        defaultLayoutData = {};

    loadLayout(
      options.layout,
      {
        filenameRegex: filenameRegex,
        fileExtRegex: fileExtRegex,
        EngineLoader: EngineLoader,
        engine: engine
      },
      function(err, results) {
        if(!err) {
          defaultLayoutName = results.layoutName;
          defaultLayout = results.layout;
          defaultLayoutData = results.data;
        } else {
          grunt.warn(err.message);
        }
      });

    // merge any layoutData with options
    options.data = _.extend(defaultLayoutData.context, options.data || {});

    var complete = 0;
    var increment = 10;

    // load partials if specified
    if(partials && partials.length > 0) {
      complete = 0;
      increment = Math.round(partials.length / 10);
      log.write(('\n' + 'Processing partials...').grey);

      partials.forEach(function(filepath) {
        var filename = _.first(filepath.match(filenameRegex)).replace(fileExtRegex, '');
        grunt.verbose.writeln(('Processing ' + filename + ' partial').cyan);
        if(complete%increment === 0) {
          log.write('.'.cyan);
        }

        var partial = fs.readFileSync(filepath, 'utf8');

        partial = engine.compile(partial, {
          preprocessers: [
            yamlPreprocessor(filename, function(output) {
              options.data[output.name] = _.extend(output.output.context, options.data[output.name] || {});
            })
          ]
        });

        // register the partial with the engine
        engine.engine.registerPartial(filename, partial);
        complete++;
      });
      log.notverbose.writeln('\n');
    }

    // load data if specified
    if(dataFiles && dataFiles.length > 0) {
      complete = 0;
      increment = Math.round(dataFiles.length / 10);
      log.writeln(('\n' + 'Begin processing data...').grey);

      dataFiles.forEach(function(filepath) {
        var filename = _.first(filepath.match(filenameRegex)).replace(/\.json/,'');
        if(complete%increment === 0) {
          log.notverbose.write('.'.cyan);
        }

        if(filename === 'data') {
          // if this is the base data.json file, load it into the options.data object directly
          options.data = _.extend(options.data || {}, grunt.file.readJSON(filepath));
        } else {
          // otherwise it's an element in options.data
          var d = grunt.file.readJSON(filepath);
          if(d[filename]) {
            // json object contains root object name so extend it in options.json
            options.data[filename] = _.extend(options.data[filename] || {}, d[filename]);
          } else {
            // add the entire object
            options.data[filename] = _.extend(options.data[filename] || {}, d);
          }
        }
        complete++;
      });
      log.writeln('\n');
    }

    options.defaultLayout     = defaultLayout;
    options.defaultLayoutName = defaultLayoutName;

    // build each page
    log.writeln(('\n' + 'Building partials...').grey);

    var findBasePath = function(srcFiles, basePath) {
      if (basePath === false) {
        return '';
      }

      if (grunt.util.kindOf(basePath) === 'string' && basePath.length >= 1) {
        return grunt.util._(path.normalize(basePath)).trim(path.sep);
      }

      var foundPath;
      var basePaths = [];
      var dirName;

      srcFiles.forEach(function(srcFile) {
        srcFile = path.normalize(srcFile);
        dirName = path.dirname(srcFile);

        basePaths.push(dirName.split(path.sep));
      });

      basePaths = grunt.util._.intersection.apply([], basePaths);

      foundPath = path.join.apply(path, basePaths);

      if (foundPath === '.') {
        foundPath = '';
      }

      return foundPath;
    };

    var basePath = findBasePath(src, true);
    //var assetsPath = path.join(dest, options.assets);
    var assetsPath = options.assets;
    if(assetsPath === "." || assetsPath.length === 0) {
      assetsPath = dest;
    }

    this.files.forEach(function(filePair) {
      // validate that the source object exists
      // and there are files at the source.
      if(!filePair.src) {
        grunt.warn('Missing src property.');
        return false;
      }
      if(filePair.src.length === 0) {
        grunt.warn('Source files not found.');
        return false;
      }

      // validate that the dest object exists
      if(!filePair.dest || filePair.dest.length === 0) {
        grunt.warn('Missing dest property.');
        return false;
      }
      var dest = path.normalize(filePair.dest);

      filePair.src.forEach(function(srcFile) {
        srcFile  = path.normalize(srcFile);
        filename = path.basename(srcFile).replace(fileExtRegex,'');

        grunt.verbose.writeln('Reading ' + filename.magenta);

        relative = path.dirname(srcFile);
        relative = _(relative).strRight(basePath).trim(path.sep);
        relative = relative.replace(/\.\.(\/|\\)/g, '');

        destFile = path.join(dest, relative, filename + options.ext);

        // setup options.assets so it's the relative path to the
        // dest assets folder from the new dest file
        options.assets = urlNormalize(
          path.relative(
            path.resolve(path.join(dest, relative)),
            path.resolve(assetsPath)
          ));

        grunt.verbose.writeln(('\t' + 'Src: '    + srcFile));
        grunt.verbose.writeln(('\t' + 'Dest: '   + destFile));
        grunt.verbose.writeln(('\t' + 'Assets: ' + options.assets));

        build(srcFile, filename, options, function(err, result) {
          if(err) {
            grunt.warn(err);
            done(false);
            return;
          }

          file.write(destFile, result);
          grunt.log.ok('File ' + (filename + options.ext).magenta + ' created.' + ' ok '.green);
        }); // build

      }); // filePair.src.forEach
    }); // this.files.forEach

    if(done) {
      done();
    }

  });

  // ==========================================================================
  // HELPERS
  // ==========================================================================
  var build = function(src, filename, options, callback) {

    var page           = fs.readFileSync(src, 'utf8'),
        layout         = options.defaultLayout,
        data           = options.data,
        engine         = options.engine,
        EngineLoader   = options.EngineLoader,
        context        = {};

    context.layoutName = _(options.defaultLayoutName).humanize();
    context.pageName   = _(filename).humanize();
    context.pageName   = filename;

    //options.data = null;

    try {

      var pageContext = {};
      var yamlPreprocessor = EngineLoader.getPreprocessor('YamlPreprocessor');

      page = engine.compile(page, {
        preprocessers: [
          yamlPreprocessor(filename, function(output) {
            grunt.verbose.writeln(output.name + ' data retreived');
            pageContext = output.output.context;
          })
        ]
      });

      options.data   = undefined;
      options.layout = undefined;
      options.engine = undefined;
      options.EngineLoader = undefined;
      context        = _.extend(context, options, data, pageContext);
      options.data   = data;
      options.layout = layout;
      options.engine = engine;
      options.EngineLoader = EngineLoader;

      // if pageContext contains a layout, use that one instead
      // of the default layout
      if(pageContext && pageContext.layout) {

        var pageLayoutName = null,
            pageLayout = null,
            pageLayoutContext = {};

        context = processContext(grunt, context);

        loadLayout(
          context.layout,
          {
            filenameRegex: options.filenameRegex,
            fileExtRegex: options.fileExtRegex,
            EngineLoader: EngineLoader,
            engine: engine
          },
          function(err, results) {
            if(!err) {
              pageLayoutName = results.layoutName;
              pageLayout = results.layout;
              pageLayoutContext = results.data;
            } else {
              grunt.warn(err.message);
            }
          }
        );

        if(pageLayout) {
          layout = pageLayout;
          context.layoutName = pageLayoutName;
          data = _.extend(data, pageLayoutContext);
        }

      }

      engine.engine.registerPartial("body", page);

      context = processContext(grunt, context);
      page = layout(context);

      callback(null, page);
    } catch(err) {
      callback(err);
      return;
    }
  };

  var processContext = function(grunt, context) {
      var originalConfigData = grunt.config.data;
      grunt.config.data = _.extend(originalConfigData, context);
      context = grunt.config.process(context);
      grunt.config.data = originalConfigData;

      return context;
  };

  var loadLayout = function(src, options, callback) {

    var loadFile = true;
    var layout = '';
    var layoutName = 'layout';

    // if the src is empty, create a default layout in memory
    if(src === '' || src.length === 0) {
      loadFile = false;
      layout = "{{>body}}";
    }

    if(loadFile) {
      // validate that the layout file exists
      grunt.log.writeln(src);
      layout = path.normalize(src);
      grunt.log.writeln(layout);

      if(!fs.existsSync(layout)) {
        var err = 'Layout file (' + layout + ') not found.';
        grunt.warn(err);
        if(callback) {
          callback({message: err}, null);
        }
        return false;
      }

      // load layout
      layoutName = _.first(layout.match(options.filenameRegex)).replace(options.fileExtRegex,'');
      layout = fs.readFileSync(layout, 'utf8');
    }

    var layoutData = {};
    var yamlPreprocessor = options.EngineLoader.getPreprocessor('YamlPreprocessor');
    layout = options.engine.compile(layout, {
      preprocessers: [
        yamlPreprocessor(layoutName, function(output) {
          grunt.verbose.writeln(output.name + ' data retreived');
          layoutData = output.output;
        })
      ]
    });

    var results = {
      layoutName: layoutName,
      layout: layout,
      data: layoutData
    };

    if(callback) {
      callback(null, results);
    }
    return results;
  };

  var detectDestType = function(dest) {
    if(_.endsWith(dest, path.sep)) {
      return "directory";
    } else {
      return "file";
    }
  };

  var logBlock = function(heading, message) {
    grunt.verbose.writeln(heading.cyan);
    grunt.verbose.writeln(message);
    grunt.verbose.writeln();
  };

  var getEngineOf = function(fileName) {
    var ext = extension(fileName);
    return  _( _(extensions).keys() ).include(ext) ? extensions[ext] : false;
  };

  var extension = function(fileName) {
    grunt.verbose.writeln('extension');
    grunt.verbose.writeln(fileName);
    if(kindOf(fileName) === "array" && fileName.length > 0) {
      fileName = fileName[0];
    }
    return _(fileName.match(/[^.]*$/)).last();
  };

  var urlNormalize = function(urlString) {
    return urlString.replace(/\\/g, '/');
  };

};
<commit_msg>adding ability to read json or yaml data files<commit_after>/*
 * Assemble
 * https://github.com/assemble/assemble
 *
 * Copyright (c) 2012 Brian Woodward
 * Licensed under the MIT license.
 */

module.exports = function(grunt) {

  // Grunt utilities
  var file     = grunt.file;
  var log      = grunt.log;
  var kindOf   = grunt.util.kindOf;
  var _        = grunt.util._;

  var assemble = require('../lib/assemble');

  // external dependencies
  var path     = require('path');
  var fs       = require('fs');
  var util     = require('util');

  var extensions = assemble.Utils.ExtensionMap;

  grunt.registerMultiTask('assemble', 'Compile template files with specified engines', function(){

    var options = this.options({
      layout        : '',
      partials      : {},
      data          : {},
      assets        : 'dist/assets',
      ext           : '.html'
    });

    logBlock("options: ", util.inspect(options));
    logBlock("this.files: ", util.inspect(this.files));

    // try to get a src to use for configuration
    var src;
    this.files.forEach(function(fp) {
      src = fp.src;
    });

    if(!src || src.length === 0) {
      grunt.warn('No source files found.');
      return false;
    }

    // find an engine to use
    options.engine = options.engine || getEngineOf(src);
    if(!options.engine) {
      grunt.warn('No compatible engine available');
      return false;
    }

    var EngineLoader = options.EngineLoader = assemble.EngineLoader(options);
    var engine = null;
    EngineLoader.getEngine(function(err, results) {
      if(err) {
        console.log(err);
        return;
      }
      engine = options.engine = results;
    });

    var yamlPreprocessor = EngineLoader.getPreprocessor('YamlPreprocessor');

    var partials      = file.expand(options.partials);
    var dataFiles     = file.expand(options.data);
    var fileExt       = extension(src);
    var filenameRegex = /[^\\\/:*?"<>|\r\n]+$/i;
    var fileExtRegex  = new RegExp("\\." + fileExt + "$");

    options.filenameRegex = filenameRegex;
    options.fileExtRegex = fileExtRegex;

    var done = this.async();

    // clear out the partials and data objects on options
    options.partials = {};
    options.data = {};

    // load default layout
    var defaultLayoutName,
        defaultLayout,
        defaultLayoutData = {};

    loadLayout(
      options.layout,
      {
        filenameRegex: filenameRegex,
        fileExtRegex: fileExtRegex,
        EngineLoader: EngineLoader,
        engine: engine
      },
      function(err, results) {
        if(!err) {
          defaultLayoutName = results.layoutName;
          defaultLayout = results.layout;
          defaultLayoutData = results.data;
        } else {
          grunt.warn(err.message);
        }
      });

    // merge any layoutData with options
    options.data = _.extend(defaultLayoutData.context, options.data || {});

    var complete = 0;
    var increment = 10;

    // load partials if specified
    if(partials && partials.length > 0) {
      complete = 0;
      increment = Math.round(partials.length / 10);
      log.write(('\n' + 'Processing partials...').grey);

      partials.forEach(function(filepath) {
        var filename = _.first(filepath.match(filenameRegex)).replace(fileExtRegex, '');
        grunt.verbose.writeln(('Processing ' + filename + ' partial').cyan);
        if(complete%increment === 0) {
          log.write('.'.cyan);
        }

        var partial = fs.readFileSync(filepath, 'utf8');

        partial = engine.compile(partial, {
          preprocessers: [
            yamlPreprocessor(filename, function(output) {
              options.data[output.name] = _.extend(output.output.context, options.data[output.name] || {});
            })
          ]
        });

        // register the partial with the engine
        engine.engine.registerPartial(filename, partial);
        complete++;
      });
      log.notverbose.writeln('\n');
    }

    // load data if specified
    if(dataFiles && dataFiles.length > 0) {
      complete = 0;
      increment = Math.round(dataFiles.length / 10);
      log.writeln(('\n' + 'Begin processing data...').grey);

      dataFiles.forEach(function(filepath) {
        var ext = path.extname(filepath);
        var filename = path.basename(filepath, dataExt);

        var fileReader = dataFileReaderFactory(ext);

        if(complete%increment === 0) {
          log.notverbose.write('.'.cyan);
        }

        if(filename === 'data') {
          // if this is the base data file, load it into the options.data object directly
          options.data = _.extend(options.data || {}, fileReader(filepath));
        } else {
          // otherwise it's an element in options.data
          var d = fileReader(filepath);
          if(d[filename]) {
            // json object contains root object name so extend it in options.json
            options.data[filename] = _.extend(options.data[filename] || {}, d[filename]);
          } else {
            // add the entire object
            options.data[filename] = _.extend(options.data[filename] || {}, d);
          }
        }
        complete++;
      });
      log.writeln('\n');
    }

    options.defaultLayout     = defaultLayout;
    options.defaultLayoutName = defaultLayoutName;

    // build each page
    log.writeln(('\n' + 'Building partials...').grey);

    var findBasePath = function(srcFiles, basePath) {
      if (basePath === false) {
        return '';
      }

      if (grunt.util.kindOf(basePath) === 'string' && basePath.length >= 1) {
        return grunt.util._(path.normalize(basePath)).trim(path.sep);
      }

      var foundPath;
      var basePaths = [];
      var dirName;

      srcFiles.forEach(function(srcFile) {
        srcFile = path.normalize(srcFile);
        dirName = path.dirname(srcFile);

        basePaths.push(dirName.split(path.sep));
      });

      basePaths = grunt.util._.intersection.apply([], basePaths);

      foundPath = path.join.apply(path, basePaths);

      if (foundPath === '.') {
        foundPath = '';
      }

      return foundPath;
    };

    var basePath = findBasePath(src, true);
    //var assetsPath = path.join(dest, options.assets);
    var assetsPath = options.assets;
    if(assetsPath === "." || assetsPath.length === 0) {
      assetsPath = dest;
    }

    this.files.forEach(function(filePair) {
      // validate that the source object exists
      // and there are files at the source.
      if(!filePair.src) {
        grunt.warn('Missing src property.');
        return false;
      }
      if(filePair.src.length === 0) {
        grunt.warn('Source files not found.');
        return false;
      }

      // validate that the dest object exists
      if(!filePair.dest || filePair.dest.length === 0) {
        grunt.warn('Missing dest property.');
        return false;
      }
      var dest = path.normalize(filePair.dest);

      filePair.src.forEach(function(srcFile) {
        srcFile  = path.normalize(srcFile);
        filename = path.basename(srcFile).replace(fileExtRegex,'');

        grunt.verbose.writeln('Reading ' + filename.magenta);

        relative = path.dirname(srcFile);
        relative = _(relative).strRight(basePath).trim(path.sep);
        relative = relative.replace(/\.\.(\/|\\)/g, '');

        destFile = path.join(dest, relative, filename + options.ext);

        // setup options.assets so it's the relative path to the
        // dest assets folder from the new dest file
        options.assets = urlNormalize(
          path.relative(
            path.resolve(path.join(dest, relative)),
            path.resolve(assetsPath)
          ));

        grunt.verbose.writeln(('\t' + 'Src: '    + srcFile));
        grunt.verbose.writeln(('\t' + 'Dest: '   + destFile));
        grunt.verbose.writeln(('\t' + 'Assets: ' + options.assets));

        build(srcFile, filename, options, function(err, result) {
          if(err) {
            grunt.warn(err);
            done(false);
            return;
          }

          file.write(destFile, result);
          grunt.log.ok('File ' + (filename + options.ext).magenta + ' created.' + ' ok '.green);
        }); // build

      }); // filePair.src.forEach
    }); // this.files.forEach

    if(done) {
      done();
    }

  });

  // ==========================================================================
  // HELPERS
  // ==========================================================================
  var build = function(src, filename, options, callback) {

    var page           = fs.readFileSync(src, 'utf8'),
        layout         = options.defaultLayout,
        data           = options.data,
        engine         = options.engine,
        EngineLoader   = options.EngineLoader,
        context        = {};

    context.layoutName = _(options.defaultLayoutName).humanize();
    context.pageName   = _(filename).humanize();
    context.pageName   = filename;

    //options.data = null;

    try {

      var pageContext = {};
      var yamlPreprocessor = EngineLoader.getPreprocessor('YamlPreprocessor');

      page = engine.compile(page, {
        preprocessers: [
          yamlPreprocessor(filename, function(output) {
            grunt.verbose.writeln(output.name + ' data retreived');
            pageContext = output.output.context;
          })
        ]
      });

      options.data   = undefined;
      options.layout = undefined;
      options.engine = undefined;
      options.EngineLoader = undefined;
      context        = _.extend(context, options, data, pageContext);
      options.data   = data;
      options.layout = layout;
      options.engine = engine;
      options.EngineLoader = EngineLoader;

      // if pageContext contains a layout, use that one instead
      // of the default layout
      if(pageContext && pageContext.layout) {

        var pageLayoutName = null,
            pageLayout = null,
            pageLayoutContext = {};

        context = processContext(grunt, context);

        loadLayout(
          context.layout,
          {
            filenameRegex: options.filenameRegex,
            fileExtRegex: options.fileExtRegex,
            EngineLoader: EngineLoader,
            engine: engine
          },
          function(err, results) {
            if(!err) {
              pageLayoutName = results.layoutName;
              pageLayout = results.layout;
              pageLayoutContext = results.data;
            } else {
              grunt.warn(err.message);
            }
          }
        );

        if(pageLayout) {
          layout = pageLayout;
          context.layoutName = pageLayoutName;
          data = _.extend(data, pageLayoutContext);
        }

      }

      engine.engine.registerPartial("body", page);

      context = processContext(grunt, context);
      page = layout(context);

      callback(null, page);
    } catch(err) {
      callback(err);
      return;
    }
  };

  var processContext = function(grunt, context) {
      var originalConfigData = grunt.config.data;
      grunt.config.data = _.extend(originalConfigData, context);
      context = grunt.config.process(context);
      grunt.config.data = originalConfigData;

      return context;
  };

  var loadLayout = function(src, options, callback) {

    var loadFile = true;
    var layout = '';
    var layoutName = 'layout';

    // if the src is empty, create a default layout in memory
    if(src === '' || src.length === 0) {
      loadFile = false;
      layout = "{{>body}}";
    }

    if(loadFile) {
      // validate that the layout file exists
      grunt.log.writeln(src);
      layout = path.normalize(src);
      grunt.log.writeln(layout);

      if(!fs.existsSync(layout)) {
        var err = 'Layout file (' + layout + ') not found.';
        grunt.warn(err);
        if(callback) {
          callback({message: err}, null);
        }
        return false;
      }

      // load layout
      layoutName = _.first(layout.match(options.filenameRegex)).replace(options.fileExtRegex,'');
      layout = fs.readFileSync(layout, 'utf8');
    }

    var layoutData = {};
    var yamlPreprocessor = options.EngineLoader.getPreprocessor('YamlPreprocessor');
    layout = options.engine.compile(layout, {
      preprocessers: [
        yamlPreprocessor(layoutName, function(output) {
          grunt.verbose.writeln(output.name + ' data retreived');
          layoutData = output.output;
        })
      ]
    });

    var results = {
      layoutName: layoutName,
      layout: layout,
      data: layoutData
    };

    if(callback) {
      callback(null, results);
    }
    return results;
  };

  var detectDestType = function(dest) {
    if(_.endsWith(dest, path.sep)) {
      return "directory";
    } else {
      return "file";
    }
  };

  var logBlock = function(heading, message) {
    grunt.verbose.writeln(heading.cyan);
    grunt.verbose.writeln(message);
    grunt.verbose.writeln();
  };

  var getEngineOf = function(fileName) {
    var ext = extension(fileName);
    return  _( _(extensions).keys() ).include(ext) ? extensions[ext] : false;
  };

  var extension = function(fileName) {
    grunt.verbose.writeln('extension');
    grunt.verbose.writeln(fileName);
    if(kindOf(fileName) === "array" && fileName.length > 0) {
      fileName = fileName[0];
    }
    return _(fileName.match(/[^.]*$/)).last();
  };

  var urlNormalize = function(urlString) {
    return urlString.replace(/\\/g, '/');
  };

  var dataFileReaderFactory = function(ext) {
    var reader = grunt.file.readJSON;
    switch(ext) {
      case '.json':
        reader = grunt.file.readJSON;
        break;

      case '.yml':
      case '.yaml':
        reader = grunt.file.readYAML;
        break;
    }
    return reader;
  }

};
<|endoftext|>


<commit_before>/*
 * grunt-contrib-imagemin
 * http://gruntjs.com/
 *
 * Copyright (c) 2012 Sindre Sorhus, contributors
 * Licensed under the MIT license.
 */

'use strict';

module.exports = function (grunt) {
    var path = require('path');
    var fs = require('fs');
    var os = require('os');
    var crypto = require('crypto');
    var childProcess = require('child_process');
    var filesize = require('filesize');
    var chalk = require('chalk');
    var optipngPath = require('optipng-bin').path;
    var jpegtranPath = require('jpegtran-bin').path;
    var tmpdir = os.tmpdir ? os.tmpdir() : os.tmpDir();
    var cacheDir = path.join(tmpdir, 'grunt-contrib-imagemin.cache');

    function hashFile(filePath) {
        var content = grunt.file.read(filePath);
        return crypto.createHash('sha1').update(content).digest('hex');
    }

    grunt.registerMultiTask('imagemin', 'Minify PNG and JPEG images', function () {
        var done = this.async();
        var options = this.options({
            optimizationLevel: 7,
            progressive: true
        });
        var optipngArgs = ['-strip', 'all'];
        var jpegtranArgs = ['-copy', 'none', '-optimize'];
        var totalSaved = 0;

        if (typeof options.optimizationLevel === 'number') {
            optipngArgs.push('-o', options.optimizationLevel);
        }

        if (options.progressive === true) {
            jpegtranArgs.push('-progressive');
        }

        grunt.verbose.writeflags(options, 'Options');

        grunt.util.async.forEachLimit(this.files, 30, function (file, next) {
            optimize(file.src[0], file.dest, next);
        }.bind(this), function (err) {
            if (err) {
                grunt.warn(err);
            }

            grunt.log.writeln('Minified ' + this.files.length + ' ' +
                (this.files.length === 1 ? 'image' : 'images') +
                chalk.gray(' (saved '  + filesize(totalSaved) + ')'));
            done();
        }.bind(this));

        function optimize(src, dest, next) {
            var cp;
            var originalSize = fs.statSync(src).size;
            var cachePath = path.join(cacheDir, hashFile(src));

            function processed(err, result, code) {
                var saved, savedMsg;

                if (err) {
                    grunt.log.writeln(err);
                }

                saved = originalSize - fs.statSync(dest).size;
                totalSaved += saved;

                if (result && (result.stderr.indexOf('already optimized') !== -1 || saved < 10)) {
                    savedMsg = 'already optimized';
                } else {
                    savedMsg = 'saved ' + filesize(saved);
                }

                if (!grunt.file.exists(cachePath)) {
                    grunt.file.copy(dest, cachePath);

                    if (grunt.option('verbose')) {
                        grunt.log.writeln('[caching] ' + src + ' → ' + cachePath);
                    }
                }

                grunt.log.writeln(chalk.green('✔ ') + src + chalk.gray(' (' + savedMsg + ')'));
                next();
            }

            grunt.file.mkdir(path.dirname(dest));

            if (grunt.file.exists(cachePath)) {
                if (grunt.option('verbose')) {
                    grunt.log.writeln('[cached] ' + src + ' ← ' + cachePath);
                }

                grunt.file.copy(cachePath, dest);
                processed();
            } else if (path.extname(src).toLowerCase() === '.png') {
                // OptiPNG can't overwrite without creating a backup file
                // https://sourceforge.net/tracker/?func=detail&aid=3607244&group_id=151404&atid=780913
                if (dest !== src && grunt.file.exists(dest)) {
                    grunt.file.delete(dest);
                }

                cp = grunt.util.spawn({
                    cmd: optipngPath,
                    args: optipngArgs.concat(['-out', dest, src])
                }, processed);
            } else if (['.jpg', '.jpeg'].indexOf(path.extname(src).toLowerCase()) !== -1) {
                cp = grunt.util.spawn({
                    cmd: jpegtranPath,
                    args: jpegtranArgs.concat(['-outfile', dest, src])
                }, processed);
            } else {
                next();
            }

            if (cp && grunt.option('verbose')) {
                cp.stdout.pipe(process.stdout);
                cp.stderr.pipe(process.stderr);
            }
        }
    });
};
<commit_msg>Limit async operations to the number of CPUs<commit_after>/*
 * grunt-contrib-imagemin
 * http://gruntjs.com/
 *
 * Copyright (c) 2012 Sindre Sorhus, contributors
 * Licensed under the MIT license.
 */

'use strict';

module.exports = function (grunt) {
    var path = require('path');
    var fs = require('fs');
    var os = require('os');
    var crypto = require('crypto');
    var childProcess = require('child_process');
    var filesize = require('filesize');
    var chalk = require('chalk');
    var optipngPath = require('optipng-bin').path;
    var jpegtranPath = require('jpegtran-bin').path;
    var numCPUs = os.cpus().length;
    var tmpdir = os.tmpdir ? os.tmpdir() : os.tmpDir();
    var cacheDir = path.join(tmpdir, 'grunt-contrib-imagemin.cache');

    function hashFile(filePath) {
        var content = grunt.file.read(filePath);
        return crypto.createHash('sha1').update(content).digest('hex');
    }

    grunt.registerMultiTask('imagemin', 'Minify PNG and JPEG images', function () {
        var done = this.async();
        var options = this.options({
            optimizationLevel: 7,
            progressive: true
        });
        var optipngArgs = ['-strip', 'all'];
        var jpegtranArgs = ['-copy', 'none', '-optimize'];
        var totalSaved = 0;

        if (typeof options.optimizationLevel === 'number') {
            optipngArgs.push('-o', options.optimizationLevel);
        }

        if (options.progressive === true) {
            jpegtranArgs.push('-progressive');
        }

        grunt.verbose.writeflags(options, 'Options');

        grunt.util.async.forEachLimit(this.files, numCPUs, function (file, next) {
            optimize(file.src[0], file.dest, next);
        }.bind(this), function (err) {
            if (err) {
                grunt.warn(err);
            }

            grunt.log.writeln('Minified ' + this.files.length + ' ' +
                (this.files.length === 1 ? 'image' : 'images') +
                chalk.gray(' (saved '  + filesize(totalSaved) + ')'));
            done();
        }.bind(this));

        function optimize(src, dest, next) {
            var cp;
            var originalSize = fs.statSync(src).size;
            var cachePath = path.join(cacheDir, hashFile(src));

            function processed(err, result, code) {
                var saved, savedMsg;

                if (err) {
                    grunt.log.writeln(err);
                }

                saved = originalSize - fs.statSync(dest).size;
                totalSaved += saved;

                if (result && (result.stderr.indexOf('already optimized') !== -1 || saved < 10)) {
                    savedMsg = 'already optimized';
                } else {
                    savedMsg = 'saved ' + filesize(saved);
                }

                if (!grunt.file.exists(cachePath)) {
                    grunt.file.copy(dest, cachePath);

                    if (grunt.option('verbose')) {
                        grunt.log.writeln('[caching] ' + src + ' → ' + cachePath);
                    }
                }

                grunt.log.writeln(chalk.green('✔ ') + src + chalk.gray(' (' + savedMsg + ')'));
                next();
            }

            grunt.file.mkdir(path.dirname(dest));

            if (grunt.file.exists(cachePath)) {
                if (grunt.option('verbose')) {
                    grunt.log.writeln('[cached] ' + src + ' ← ' + cachePath);
                }

                grunt.file.copy(cachePath, dest);
                processed();
            } else if (path.extname(src).toLowerCase() === '.png') {
                // OptiPNG can't overwrite without creating a backup file
                // https://sourceforge.net/tracker/?func=detail&aid=3607244&group_id=151404&atid=780913
                if (dest !== src && grunt.file.exists(dest)) {
                    grunt.file.delete(dest);
                }

                cp = grunt.util.spawn({
                    cmd: optipngPath,
                    args: optipngArgs.concat(['-out', dest, src])
                }, processed);
            } else if (['.jpg', '.jpeg'].indexOf(path.extname(src).toLowerCase()) !== -1) {
                cp = grunt.util.spawn({
                    cmd: jpegtranPath,
                    args: jpegtranArgs.concat(['-outfile', dest, src])
                }, processed);
            } else {
                next();
            }

            if (cp && grunt.option('verbose')) {
                cp.stdout.pipe(process.stdout);
                cp.stderr.pipe(process.stderr);
            }
        }
    });
};
<|endoftext|>


<commit_before>const events = require( 'events' )
const util = require( 'util' )
const pckg = require( '../package.json' )
const mongoClient = require('mongodb').MongoClient
const ObjectID = require('mongodb').ObjectID
const dataTransform = require( './transform-data' )

/**
 * Connects deepstream to MongoDb.
 *
 * Collections, ids and performance
 * --------------------------------------------------
 * Deepstream treats its storage like a simple key value store. But there are a few things
 * we can do to speed it up when using MongoDb. Mainly: using smaller (e.g. more granular) collections and using successive Id's
 *
 *
 * To support multiple collections pass a splitChar setting to this class. This setting specifies a character
 * at which keys will be split and ordered into collections. This sounds a bit complicated, but all that means is the following:
 *
 * Imagine you want to store a few users. Just specify their recordNames as e.g.
 *
 *  user/i4vcg5j1-16n1qrnziuog
 *  user/i4vcg5x9-a2wc3g9pbhmi
 *  user/i4vcg74u-21ufhl1qs8fh
 *
 * and in your options set
 *
 * { splitChar: '/' }
 *
 * This way the MongoDB connector will create a 'user' collection the first time
 * it encounters this recordName and will subsequently store users in it. This will
 * improve the speed of read operations since MongoDb has to look through a smaller
 * amount of datasets to find your record
 *
 * On top of this, it makes sense to use successive ids. MongoDb will optimise collections
 * by putting documents with similar ids next to each other. Fortunately, the build-in getUid()
 * method of the deepstream client already produces semi-succesive ids. Notice how the first bits of the
 * ids (user/i4vcg5) are all the same. These are Base36 encoded timestamps, facilitating almost succesive ordering.
 *
 * @param {Object} options
 *
 * {
 *    // Optional: Collections for items without a splitChar or if no splitChar is specified. Defaults to 'deepstream_docs'
   defaultCollection: <String>,

   // Optional: A char that seperates the collection name from the document id. Defaults to null
   splitChar: <String>,

   // Full connection URL for MongoDb. Format is mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]
   // More details can be found here: http://docs.mongodb.org/manual/reference/connection-string/
   connectionString: <String>
   }
 *
 * @constructor
 */
var Connector = function( options ) {
  this.isReady = false
  this.name = pckg.name
  this.version = pckg.version
  this._splitChar = options.splitChar || null
  this._defaultCollection = options.defaultCollection || 'deepstream_docs'
  this._db = null
  this._collections = {}

  if( !options.connectionString ) {
    throw new Error( 'Missing setting \'connectionString\'' )
  }

  mongoClient.connect( options.connectionString, this._onConnect.bind( this ) )
}

util.inherits( Connector, events.EventEmitter )

/**
 * Writes a value to the cache.
 *
 * @param {String}   key
 * @param {Object}   value
 * @param {Function} callback Should be called with null for successful set operations or with an error message string
 *
 * @private
 * @returns {void}
 */
Connector.prototype.set = function( key, value, callback ) {
  var params = this._getParams( key )

  if( params === null ) {
    callback( 'Invalid key ' + key )
    return
  }

  value = dataTransform.transformValueForStorage( value )
  value.ds_key = params.id
  params.collection.updateOne({ ds_key: params.id }, value, { upsert: true }, callback )
}

/**
 * Retrieves a value from the cache
 *
 * @param {String}   key
 * @param {Function} callback Will be called with null and the stored object
 *                            for successful operations or with an error message string
 *
 * @private
 * @returns {void}
 */
Connector.prototype.get = function( key, callback ) {
  var params = this._getParams( key )

  if( params === null ) {
    callback( 'Invalid key ' + key )
    return
  }

  params.collection.findOne({ ds_key: params.id }, ( err, doc ) => {
    if( err ) {
      callback( err )
    } else {
      if( doc === null ) {
        callback( null, null )
      } else {
        delete doc._id
        delete doc.ds_key
        doc = dataTransform.transformValueFromStorage( doc )
        callback( null, doc )
      }
    }
  })
}

/**
 * Deletes an entry from the cache.
 *
 * @param   {String}   key
 * @param   {Function} callback Will be called with null for successful deletions or with
 *                     an error message string
 *
 * @private
 * @returns {void}
 */
Connector.prototype.delete = function( key, callback ) {
  var params = this._getParams( key )

  if( params === null ) {
    callback( 'Invalid key ' + key )
    return
  }

  params.collection.deleteOne({ ds_key: params.id }, callback )
}

/**
 * Callback for established (or rejected) connections
 *
 * @param {String} error
 * @param {MongoClient} db
 *
 * @private
 * @returns {void}
 */
Connector.prototype._onConnect = function( err, db ) {
  if( err ) {
    this.emit( 'error', err )
    return
  }

  this._db = db
  this.isReady = true
  this.emit( 'ready' )
}

/**
 * Determines the document id and the collection
 * to use based on the provided key
 *
 * Creates the collection if it doesn't exist yet.
 *
 * Since MongoDB ObjecIDs are adhering to a specified format
 * we'll add a new field for the key called ds_key and index the
 * collection based on it
 *
 * @param {String} key
 *
 * @private
 * @returns {Object} {connection: <MongoConnection>, id: <String> }
 */
Connector.prototype._getParams = function( key ) {
  var parts = key.split( this._splitChar ),
    collectionName,
    id

  if( parts.length === 1 ) {
    collectionName = this._defaultCollection
    id = key
  }
  else if( parts.length === 2 ) {
    collectionName = parts[ 0 ]
    id = parts[ 1 ]
  }
  else {
    return null
  }

  if( !this._collections[ collectionName ] ) {
    this._collections[ collectionName ] = this._db.collection( collectionName )
    this._collections[ collectionName ].ensureIndex({ ds_key: 1 })
  }

  return { collection: this._collections[ collectionName ], id: id }
}

module.exports = Connector<commit_msg>only splitting on first splitchar occurence<commit_after>const events = require( 'events' )
const util = require( 'util' )
const pckg = require( '../package.json' )
const mongoClient = require('mongodb').MongoClient
const ObjectID = require('mongodb').ObjectID
const dataTransform = require( './transform-data' )

/**
 * Connects deepstream to MongoDb.
 *
 * Collections, ids and performance
 * --------------------------------------------------
 * Deepstream treats its storage like a simple key value store. But there are a few things
 * we can do to speed it up when using MongoDb. Mainly: using smaller (e.g. more granular) collections and using successive Id's
 *
 *
 * To support multiple collections pass a splitChar setting to this class. This setting specifies a character
 * at which keys will be split and ordered into collections. This sounds a bit complicated, but all that means is the following:
 *
 * Imagine you want to store a few users. Just specify their recordNames as e.g.
 *
 *  user/i4vcg5j1-16n1qrnziuog
 *  user/i4vcg5x9-a2wc3g9pbhmi
 *  user/i4vcg74u-21ufhl1qs8fh
 *
 * and in your options set
 *
 * { splitChar: '/' }
 *
 * This way the MongoDB connector will create a 'user' collection the first time
 * it encounters this recordName and will subsequently store users in it. This will
 * improve the speed of read operations since MongoDb has to look through a smaller
 * amount of datasets to find your record
 *
 * On top of this, it makes sense to use successive ids. MongoDb will optimise collections
 * by putting documents with similar ids next to each other. Fortunately, the build-in getUid()
 * method of the deepstream client already produces semi-succesive ids. Notice how the first bits of the
 * ids (user/i4vcg5) are all the same. These are Base36 encoded timestamps, facilitating almost succesive ordering.
 *
 * @param {Object} options
 *
 * {
 *    // Optional: Collections for items without a splitChar or if no splitChar is specified. Defaults to 'deepstream_docs'
   defaultCollection: <String>,

   // Optional: A char that seperates the collection name from the document id. Defaults to null
   splitChar: <String>,

   // Full connection URL for MongoDb. Format is mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]
   // More details can be found here: http://docs.mongodb.org/manual/reference/connection-string/
   connectionString: <String>
   }
 *
 * @constructor
 */
var Connector = function( options ) {
  this.isReady = false
  this.name = pckg.name
  this.version = pckg.version
  this._splitChar = options.splitChar || null
  this._defaultCollection = options.defaultCollection || 'deepstream_docs'
  this._db = null
  this._collections = {}

  if( !options.connectionString ) {
    throw new Error( 'Missing setting \'connectionString\'' )
  }

  mongoClient.connect( options.connectionString, this._onConnect.bind( this ) )
}

util.inherits( Connector, events.EventEmitter )

/**
 * Writes a value to the cache.
 *
 * @param {String}   key
 * @param {Object}   value
 * @param {Function} callback Should be called with null for successful set operations or with an error message string
 *
 * @private
 * @returns {void}
 */
Connector.prototype.set = function( key, value, callback ) {
  var params = this._getParams( key )

  if( params === null ) {
    callback( 'Invalid key ' + key )
    return
  }

  value = dataTransform.transformValueForStorage( value )
  value.ds_key = params.id
  params.collection.updateOne({ ds_key: params.id }, value, { upsert: true }, callback )
}

/**
 * Retrieves a value from the cache
 *
 * @param {String}   key
 * @param {Function} callback Will be called with null and the stored object
 *                            for successful operations or with an error message string
 *
 * @private
 * @returns {void}
 */
Connector.prototype.get = function( key, callback ) {
  var params = this._getParams( key )

  if( params === null ) {
    callback( 'Invalid key ' + key )
    return
  }

  params.collection.findOne({ ds_key: params.id }, ( err, doc ) => {
    if( err ) {
      callback( err )
    } else {
      if( doc === null ) {
        callback( null, null )
      } else {
        delete doc._id
        delete doc.ds_key
        doc = dataTransform.transformValueFromStorage( doc )
        callback( null, doc )
      }
    }
  })
}

/**
 * Deletes an entry from the cache.
 *
 * @param   {String}   key
 * @param   {Function} callback Will be called with null for successful deletions or with
 *                     an error message string
 *
 * @private
 * @returns {void}
 */
Connector.prototype.delete = function( key, callback ) {
  var params = this._getParams( key )

  if( params === null ) {
    callback( 'Invalid key ' + key )
    return
  }

  params.collection.deleteOne({ ds_key: params.id }, callback )
}

/**
 * Callback for established (or rejected) connections
 *
 * @param {String} error
 * @param {MongoClient} db
 *
 * @private
 * @returns {void}
 */
Connector.prototype._onConnect = function( err, db ) {
  if( err ) {
    this.emit( 'error', err )
    return
  }

  this._db = db
  this.isReady = true
  this.emit( 'ready' )
}

/**
 * Determines the document id and the collection
 * to use based on the provided key
 *
 * Creates the collection if it doesn't exist yet.
 *
 * Since MongoDB ObjecIDs are adhering to a specified format
 * we'll add a new field for the key called ds_key and index the
 * collection based on it
 *
 * @param {String} key
 *
 * @private
 * @returns {Object} {connection: <MongoConnection>, id: <String> }
 */
Connector.prototype._getParams = function( key ) {
  var parts = key.split( this._splitChar, 1 ),
    collectionName,
    id

  if( parts.length === 1 ) {
    collectionName = this._defaultCollection
    id = key
  }
  else if( parts.length === 2 ) {
    collectionName = parts[ 0 ]
    id = parts[ 1 ]
  }
  else {
    return null
  }

  if( !this._collections[ collectionName ] ) {
    this._collections[ collectionName ] = this._db.collection( collectionName )
    this._collections[ collectionName ].ensureIndex({ ds_key: 1 })
  }

  return { collection: this._collections[ collectionName ], id: id }
}

module.exports = Connector
<|endoftext|>


<commit_before>
export const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36'; // eslint-disable-line max-len

/**
 * Exit codes for the act process.
 * The error codes must be in range 1-128, to avoid collision with signal exits
 * and to ensure Docker will handle them correctly!
 */
export const EXIT_CODES = {
    SUCCESS: 0,
    ERROR_USER_FUNCTION_THREW: 91,
    ERROR_UNKNOWN: 92,
};

/**
 * Dictionary of APIFY_XXX environment variable names.
 */
export const ENV_VARS = {
    ACT_ID: 'APIFY_ACT_ID',
    ACT_RUN_ID: 'APIFY_ACT_RUN_ID',
    USER_ID: 'APIFY_USER_ID',
    TOKEN: 'APIFY_TOKEN',
    STARTED_AT: 'APIFY_STARTED_AT',
    TIMEOUT_AT: 'APIFY_TIMEOUT_AT',
    DEFAULT_KEY_VALUE_STORE_ID: 'APIFY_DEFAULT_KEY_VALUE_STORE_ID',
    DEV_KEY_VALUE_STORE_DIR: 'APIFY_DEV_KEY_VALUE_STORE_DIR',
    DEV_KEY_VALUE_STORE_CONTENT_TYPE: 'APIFY_DEV_KEY_VALUE_STORE_CONTENT_TYPE',
    WATCH_FILE: 'APIFY_WATCH_FILE',
    API_BASE_URL: 'APIFY_API_BASE_URL',
    HEADLESS: 'APIFY_HEADLESS',
    INTERNAL_PORT: 'APIFY_INTERNAL_PORT',
    MEMORY_MBYTES: 'APIFY_MEMORY_MBYTES',
};

export const KEY_VALUE_STORE_KEYS = {
    INPUT: 'INPUT',
    OUTPUT: 'OUTPUT',
};

/**
 * Dictionary of possible values for 'status' field of act2Builds or act2Runs collections.
 */
export const ACT_TASK_STATUSES = {
    READY: 'READY', // started but not allocated to any worker yet
    RUNNING: 'RUNNING', // running on worker
    SUCCEEDED: 'SUCCEEDED', // finished and all good
    FAILED: 'FAILED', // run or build failed
    TIMING_OUT: 'TIMING-OUT', // timing out now
    TIMED_OUT: 'TIMED-OUT', // timed out
    ABORTING: 'ABORTING', // being aborted by user
    ABORTED: 'ABORTED', // aborted by user
};

/**
 * An array of act task statuses that are final for the task.
 */
export const ACT_TASK_TERMINAL_STATUSES = [
    ACT_TASK_STATUSES.SUCCEEDED,
    ACT_TASK_STATUSES.FAILED,
    ACT_TASK_STATUSES.TIMED_OUT,
    ACT_TASK_STATUSES.ABORTED,
];
<commit_msg>Updated default user-agent for launchPuppeteer()<commit_after>
export const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36'; // eslint-disable-line max-len

/**
 * Exit codes for the act process.
 * The error codes must be in range 1-128, to avoid collision with signal exits
 * and to ensure Docker will handle them correctly!
 */
export const EXIT_CODES = {
    SUCCESS: 0,
    ERROR_USER_FUNCTION_THREW: 91,
    ERROR_UNKNOWN: 92,
};

/**
 * Dictionary of APIFY_XXX environment variable names.
 */
export const ENV_VARS = {
    ACT_ID: 'APIFY_ACT_ID',
    ACT_RUN_ID: 'APIFY_ACT_RUN_ID',
    USER_ID: 'APIFY_USER_ID',
    TOKEN: 'APIFY_TOKEN',
    STARTED_AT: 'APIFY_STARTED_AT',
    TIMEOUT_AT: 'APIFY_TIMEOUT_AT',
    DEFAULT_KEY_VALUE_STORE_ID: 'APIFY_DEFAULT_KEY_VALUE_STORE_ID',
    DEV_KEY_VALUE_STORE_DIR: 'APIFY_DEV_KEY_VALUE_STORE_DIR',
    DEV_KEY_VALUE_STORE_CONTENT_TYPE: 'APIFY_DEV_KEY_VALUE_STORE_CONTENT_TYPE',
    WATCH_FILE: 'APIFY_WATCH_FILE',
    API_BASE_URL: 'APIFY_API_BASE_URL',
    HEADLESS: 'APIFY_HEADLESS',
    INTERNAL_PORT: 'APIFY_INTERNAL_PORT',
    MEMORY_MBYTES: 'APIFY_MEMORY_MBYTES',
};

export const KEY_VALUE_STORE_KEYS = {
    INPUT: 'INPUT',
    OUTPUT: 'OUTPUT',
};

/**
 * Dictionary of possible values for 'status' field of act2Builds or act2Runs collections.
 */
export const ACT_TASK_STATUSES = {
    READY: 'READY', // started but not allocated to any worker yet
    RUNNING: 'RUNNING', // running on worker
    SUCCEEDED: 'SUCCEEDED', // finished and all good
    FAILED: 'FAILED', // run or build failed
    TIMING_OUT: 'TIMING-OUT', // timing out now
    TIMED_OUT: 'TIMED-OUT', // timed out
    ABORTING: 'ABORTING', // being aborted by user
    ABORTED: 'ABORTED', // aborted by user
};

/**
 * An array of act task statuses that are final for the task.
 */
export const ACT_TASK_TERMINAL_STATUSES = [
    ACT_TASK_STATUSES.SUCCEEDED,
    ACT_TASK_STATUSES.FAILED,
    ACT_TASK_STATUSES.TIMED_OUT,
    ACT_TASK_STATUSES.ABORTED,
];
<|endoftext|>


<commit_before>"use strict";
/**
 * Runtime message from/to plain object converters.
 * @namespace
 */
var converter = exports;

var Enum = require("./enum"),
    util = require("./util");

/**
 * Generates a partial value fromObject conveter.
 * @param {Codegen} gen Codegen instance
 * @param {Field} field Reflected field
 * @param {number} fieldIndex Field index
 * @param {string} prop Property reference
 * @returns {Codegen} Codegen instance
 * @ignore
 */
function genValuePartial_fromObject(gen, field, fieldIndex, prop) {
    /* eslint-disable no-unexpected-multiline, block-scoped-var, no-redeclare */
    if (field.resolvedType) {
        if (field.resolvedType instanceof Enum) { gen
            ("switch(d%s){", prop);
            for (var values = field.resolvedType.values, keys = Object.keys(values), i = 0; i < keys.length; ++i) {
                if (field.repeated && values[keys[i]] === field.typeDefault) gen
                ("default:");
                gen
                ("case%j:", keys[i])
                ("case %i:", values[keys[i]])
                    ("m%s=%j", prop, values[keys[i]])
                    ("break");
            } gen
            ("}");
        } else gen
            ("if(typeof d%s!==\"object\")", prop)
                ("throw TypeError(%j)", field.fullName + ": object expected")
            ("m%s=types[%i].fromObject(d%s)", prop, fieldIndex, prop);
    } else {
        var isUnsigned = false;
        switch (field.type) {
            case "double":
            case "float": gen
                ("m%s=Number(d%s)", prop, prop); // also catches "NaN", "Infinity"
                break;
            case "uint32":
            case "fixed32": gen
                ("m%s=d%s>>>0", prop, prop);
                break;
            case "int32":
            case "sint32":
            case "sfixed32": gen
                ("m%s=d%s|0", prop, prop);
                break;
            case "uint64":
                isUnsigned = true;
                // eslint-disable-line no-fallthrough
            case "int64":
            case "sint64":
            case "fixed64":
            case "sfixed64": gen
                ("if(util.Long)")
                    ("(m%s=util.Long.fromValue(d%s)).unsigned=%j", prop, prop, isUnsigned)
                ("else if(typeof d%s===\"string\")", prop)
                    ("m%s=parseInt(d%s,10)", prop, prop)
                ("else if(typeof d%s===\"number\")", prop)
                    ("m%s=d%s", prop, prop)
                ("else if(typeof d%s===\"object\")", prop)
                    ("m%s=new util.LongBits(d%s.low>>>0,d%s.high>>>0).toNumber(%s)", prop, prop, prop, isUnsigned ? "true" : "");
                break;
            case "bytes": gen
                ("if(typeof d%s===\"string\")", prop)
                    ("util.base64.decode(d%s,m%s=util.newBuffer(util.base64.length(d%s)),0)", prop, prop, prop)
                ("else if(d%s.length)", prop)
                    ("m%s=d%s", prop, prop);
                break;
            case "string": gen
                ("m%s=String(d%s)", prop, prop);
                break;
            case "bool": gen
                ("m%s=Boolean(d%s)", prop, prop);
                break;
            /* default: gen
                ("m%s=d%s", prop, prop);
                break; */
        }
    }
    return gen;
    /* eslint-enable no-unexpected-multiline, block-scoped-var, no-redeclare */
}

/**
 * Generates a plain object to runtime message converter specific to the specified message type.
 * @param {Type} mtype Message type
 * @returns {Codegen} Codegen instance
 */
converter.fromObject = function fromObject(mtype) {
    /* eslint-disable no-unexpected-multiline, block-scoped-var, no-redeclare */
    var fields = mtype.fieldsArray;
    var gen = util.codegen(["d"], mtype.name + "$fromObject")
    ("if(d instanceof this.ctor)")
        ("return d");
    if (!fields.length) return gen
    ("return new this.ctor");
    gen
    ("var m=new this.ctor");
    for (var i = 0; i < fields.length; ++i) {
        var field  = fields[i].resolve(),
            prop   = util.safeProp(field.name);

        // Map fields
        if (field.map) { gen
    ("if(d%s){", prop)
        ("if(typeof d%s!==\"object\")", prop)
            ("throw TypeError(%j)", field.fullName + ": object expected")
        ("m%s={}", prop)
        ("for(var ks=Object.keys(d%s),i=0;i<ks.length;++i){", prop);
            genValuePartial_fromObject(gen, field, /* not sorted */ i, prop + "[ks[i]]")
        ("}")
    ("}");

        // Repeated fields
        } else if (field.repeated) { gen
    ("if(d%s){", prop)
        ("if(!Array.isArray(d%s))", prop)
            ("throw TypeError(%j)", field.fullName + ": array expected")
        ("m%s=[]", prop)
        ("for(var i=0;i<d%s.length;++i){", prop);
            genValuePartial_fromObject(gen, field, /* not sorted */ i, prop + "[i]")
        ("}")
    ("}");

        // Non-repeated fields
        } else {
            if (!(field.resolvedType instanceof Enum)) gen // no need to test for null/undefined if an enum (uses switch)
    ("if(d%s!=null){", prop); // !== undefined && !== null
        genValuePartial_fromObject(gen, field, /* not sorted */ i, prop);
            if (!(field.resolvedType instanceof Enum)) gen
    ("}");
        }
    } return gen
    ("return m");
    /* eslint-enable no-unexpected-multiline, block-scoped-var, no-redeclare */
};

/**
 * Generates a partial value toObject converter.
 * @param {Codegen} gen Codegen instance
 * @param {Field} field Reflected field
 * @param {number} fieldIndex Field index
 * @param {string} prop Property reference
 * @returns {Codegen} Codegen instance
 * @ignore
 */
function genValuePartial_toObject(gen, field, fieldIndex, prop) {
    /* eslint-disable no-unexpected-multiline, block-scoped-var, no-redeclare */
    if (field.resolvedType) {
        if (field.resolvedType instanceof Enum) gen
            ("d%s=o.enums===String?types[%i].values[m%s]:m%s", prop, fieldIndex, prop, prop);
        else gen
            ("d%s=types[%i].toObject(m%s,o)", prop, fieldIndex, prop);
    } else {
        var isUnsigned = false;
        switch (field.type) {
            case "double":
            case "float": gen
            ("d%s=o.json&&!isFinite(m%s)?String(m%s):m%s", prop, prop, prop, prop);
                break;
            case "uint64":
                isUnsigned = true;
                // eslint-disable-line no-fallthrough
            case "int64":
            case "sint64":
            case "fixed64":
            case "sfixed64": gen
            ("if(typeof m%s===\"number\")", prop)
                ("d%s=o.longs===String?String(m%s):m%s", prop, prop, prop)
            ("else") // Long-like
                ("d%s=o.longs===String?util.Long.prototype.toString.call(m%s):o.longs===Number?new util.LongBits(m%s.low>>>0,m%s.high>>>0).toNumber(%s):m%s", prop, prop, prop, prop, isUnsigned ? "true": "", prop);
                break;
            case "bytes": gen
            ("d%s=o.bytes===String?util.base64.encode(m%s,0,m%s.length):o.bytes===Array?Array.prototype.slice.call(m%s):m%s", prop, prop, prop, prop, prop);
                break;
            default: gen
            ("d%s=m%s", prop, prop);
                break;
        }
    }
    return gen;
    /* eslint-enable no-unexpected-multiline, block-scoped-var, no-redeclare */
}

/**
 * Generates a runtime message to plain object converter specific to the specified message type.
 * @param {Type} mtype Message type
 * @returns {Codegen} Codegen instance
 */
converter.toObject = function toObject(mtype) {
    /* eslint-disable no-unexpected-multiline, block-scoped-var, no-redeclare */
    var fields = mtype.fieldsArray.slice().sort(util.compareFieldsById);
    if (!fields.length)
        return util.codegen()("return {}");
    var gen = util.codegen(["m", "o"], mtype.name + "$toObject")
    ("if(!o)")
        ("o={}")
    ("var d={}");

    var repeatedFields = [],
        mapFields = [],
        normalFields = [],
        i = 0;
    for (; i < fields.length; ++i)
        if (!fields[i].partOf)
            ( fields[i].resolve().repeated ? repeatedFields
            : fields[i].map ? mapFields
            : normalFields).push(fields[i]);

    if (repeatedFields.length) { gen
    ("if(o.arrays||o.defaults){");
        for (i = 0; i < repeatedFields.length; ++i) gen
        ("d%s=[]", util.safeProp(repeatedFields[i].name));
        gen
    ("}");
    }

    if (mapFields.length) { gen
    ("if(o.objects||o.defaults){");
        for (i = 0; i < mapFields.length; ++i) gen
        ("d%s={}", util.safeProp(mapFields[i].name));
        gen
    ("}");
    }

    if (normalFields.length) { gen
    ("if(o.defaults){");
        for (i = 0; i < normalFields.length; ++i) {
            var field = normalFields[i],
                prop  = util.safeProp(field.name);
            if (field.resolvedType instanceof Enum) gen
        ("d%s=o.enums===String?%j:%j", prop, field.resolvedType.valuesById[field.typeDefault], field.typeDefault);
            else if (field.long) gen
        ("if(util.Long){")
            ("var n=new util.Long(%i,%i,%j)", field.typeDefault.low, field.typeDefault.high, field.typeDefault.unsigned)
            ("d%s=o.longs===String?n.toString():o.longs===Number?n.toNumber():n", prop)
        ("}else")
            ("d%s=o.longs===String?%j:%i", prop, field.typeDefault.toString(), field.typeDefault.toNumber());
            else if (field.bytes) gen
        ("d%s=o.bytes===String?%j:%s", prop, String.fromCharCode.apply(String, field.typeDefault), "[" + Array.prototype.slice.call(field.typeDefault).join(",") + "]");
            else gen
        ("d%s=%j", prop, field.typeDefault); // also messages (=null)
        } gen
    ("}");
    }
    var hasKs2 = false;
    for (i = 0; i < fields.length; ++i) {
        var field = fields[i],
            index = mtype._fieldsArray.indexOf(field),
            prop  = util.safeProp(field.name);
        if (field.map) {
            if (!hasKs2) { hasKs2 = true; gen
    ("var ks2");
            } gen
    ("if(m%s&&(ks2=Object.keys(m%s)).length){", prop, prop)
        ("d%s={}", prop)
        ("for(var j=0;j<ks2.length;++j){");
            genValuePartial_toObject(gen, field, /* sorted */ index, prop + "[ks2[j]]")
        ("}");
        } else if (field.repeated) { gen
    ("if(m%s&&m%s.length){", prop, prop)
        ("d%s=[]", prop)
        ("for(var j=0;j<m%s.length;++j){", prop);
            genValuePartial_toObject(gen, field, /* sorted */ index, prop + "[j]")
        ("}");
        } else { gen
    ("if(m%s!=null&&m.hasOwnProperty(%j)){", prop, field.name); // !== undefined && !== null
        genValuePartial_toObject(gen, field, /* sorted */ index, prop);
        if (field.partOf) gen
        ("if(o.oneofs)")
            ("d%s=%j", util.safeProp(field.partOf.name), field.name);
        }
        gen
    ("}");
    }
    return gen
    ("return d");
    /* eslint-enable no-unexpected-multiline, block-scoped-var, no-redeclare */
};
<commit_msg>Converter: Fix empty 'bytes' field decoding, now using Buffer where applicable (#1020)<commit_after>"use strict";
/**
 * Runtime message from/to plain object converters.
 * @namespace
 */
var converter = exports;

var Enum = require("./enum"),
    util = require("./util");

/**
 * Generates a partial value fromObject conveter.
 * @param {Codegen} gen Codegen instance
 * @param {Field} field Reflected field
 * @param {number} fieldIndex Field index
 * @param {string} prop Property reference
 * @returns {Codegen} Codegen instance
 * @ignore
 */
function genValuePartial_fromObject(gen, field, fieldIndex, prop) {
    /* eslint-disable no-unexpected-multiline, block-scoped-var, no-redeclare */
    if (field.resolvedType) {
        if (field.resolvedType instanceof Enum) { gen
            ("switch(d%s){", prop);
            for (var values = field.resolvedType.values, keys = Object.keys(values), i = 0; i < keys.length; ++i) {
                if (field.repeated && values[keys[i]] === field.typeDefault) gen
                ("default:");
                gen
                ("case%j:", keys[i])
                ("case %i:", values[keys[i]])
                    ("m%s=%j", prop, values[keys[i]])
                    ("break");
            } gen
            ("}");
        } else gen
            ("if(typeof d%s!==\"object\")", prop)
                ("throw TypeError(%j)", field.fullName + ": object expected")
            ("m%s=types[%i].fromObject(d%s)", prop, fieldIndex, prop);
    } else {
        var isUnsigned = false;
        switch (field.type) {
            case "double":
            case "float": gen
                ("m%s=Number(d%s)", prop, prop); // also catches "NaN", "Infinity"
                break;
            case "uint32":
            case "fixed32": gen
                ("m%s=d%s>>>0", prop, prop);
                break;
            case "int32":
            case "sint32":
            case "sfixed32": gen
                ("m%s=d%s|0", prop, prop);
                break;
            case "uint64":
                isUnsigned = true;
                // eslint-disable-line no-fallthrough
            case "int64":
            case "sint64":
            case "fixed64":
            case "sfixed64": gen
                ("if(util.Long)")
                    ("(m%s=util.Long.fromValue(d%s)).unsigned=%j", prop, prop, isUnsigned)
                ("else if(typeof d%s===\"string\")", prop)
                    ("m%s=parseInt(d%s,10)", prop, prop)
                ("else if(typeof d%s===\"number\")", prop)
                    ("m%s=d%s", prop, prop)
                ("else if(typeof d%s===\"object\")", prop)
                    ("m%s=new util.LongBits(d%s.low>>>0,d%s.high>>>0).toNumber(%s)", prop, prop, prop, isUnsigned ? "true" : "");
                break;
            case "bytes": gen
                ("if(typeof d%s===\"string\")", prop)
                    ("util.base64.decode(d%s,m%s=util.newBuffer(util.base64.length(d%s)),0)", prop, prop, prop)
                ("else if(d%s.length)", prop)
                    ("m%s=d%s", prop, prop);
                break;
            case "string": gen
                ("m%s=String(d%s)", prop, prop);
                break;
            case "bool": gen
                ("m%s=Boolean(d%s)", prop, prop);
                break;
            /* default: gen
                ("m%s=d%s", prop, prop);
                break; */
        }
    }
    return gen;
    /* eslint-enable no-unexpected-multiline, block-scoped-var, no-redeclare */
}

/**
 * Generates a plain object to runtime message converter specific to the specified message type.
 * @param {Type} mtype Message type
 * @returns {Codegen} Codegen instance
 */
converter.fromObject = function fromObject(mtype) {
    /* eslint-disable no-unexpected-multiline, block-scoped-var, no-redeclare */
    var fields = mtype.fieldsArray;
    var gen = util.codegen(["d"], mtype.name + "$fromObject")
    ("if(d instanceof this.ctor)")
        ("return d");
    if (!fields.length) return gen
    ("return new this.ctor");
    gen
    ("var m=new this.ctor");
    for (var i = 0; i < fields.length; ++i) {
        var field  = fields[i].resolve(),
            prop   = util.safeProp(field.name);

        // Map fields
        if (field.map) { gen
    ("if(d%s){", prop)
        ("if(typeof d%s!==\"object\")", prop)
            ("throw TypeError(%j)", field.fullName + ": object expected")
        ("m%s={}", prop)
        ("for(var ks=Object.keys(d%s),i=0;i<ks.length;++i){", prop);
            genValuePartial_fromObject(gen, field, /* not sorted */ i, prop + "[ks[i]]")
        ("}")
    ("}");

        // Repeated fields
        } else if (field.repeated) { gen
    ("if(d%s){", prop)
        ("if(!Array.isArray(d%s))", prop)
            ("throw TypeError(%j)", field.fullName + ": array expected")
        ("m%s=[]", prop)
        ("for(var i=0;i<d%s.length;++i){", prop);
            genValuePartial_fromObject(gen, field, /* not sorted */ i, prop + "[i]")
        ("}")
    ("}");

        // Non-repeated fields
        } else {
            if (!(field.resolvedType instanceof Enum)) gen // no need to test for null/undefined if an enum (uses switch)
    ("if(d%s!=null){", prop); // !== undefined && !== null
        genValuePartial_fromObject(gen, field, /* not sorted */ i, prop);
            if (!(field.resolvedType instanceof Enum)) gen
    ("}");
        }
    } return gen
    ("return m");
    /* eslint-enable no-unexpected-multiline, block-scoped-var, no-redeclare */
};

/**
 * Generates a partial value toObject converter.
 * @param {Codegen} gen Codegen instance
 * @param {Field} field Reflected field
 * @param {number} fieldIndex Field index
 * @param {string} prop Property reference
 * @returns {Codegen} Codegen instance
 * @ignore
 */
function genValuePartial_toObject(gen, field, fieldIndex, prop) {
    /* eslint-disable no-unexpected-multiline, block-scoped-var, no-redeclare */
    if (field.resolvedType) {
        if (field.resolvedType instanceof Enum) gen
            ("d%s=o.enums===String?types[%i].values[m%s]:m%s", prop, fieldIndex, prop, prop);
        else gen
            ("d%s=types[%i].toObject(m%s,o)", prop, fieldIndex, prop);
    } else {
        var isUnsigned = false;
        switch (field.type) {
            case "double":
            case "float": gen
            ("d%s=o.json&&!isFinite(m%s)?String(m%s):m%s", prop, prop, prop, prop);
                break;
            case "uint64":
                isUnsigned = true;
                // eslint-disable-line no-fallthrough
            case "int64":
            case "sint64":
            case "fixed64":
            case "sfixed64": gen
            ("if(typeof m%s===\"number\")", prop)
                ("d%s=o.longs===String?String(m%s):m%s", prop, prop, prop)
            ("else") // Long-like
                ("d%s=o.longs===String?util.Long.prototype.toString.call(m%s):o.longs===Number?new util.LongBits(m%s.low>>>0,m%s.high>>>0).toNumber(%s):m%s", prop, prop, prop, prop, isUnsigned ? "true": "", prop);
                break;
            case "bytes": gen
            ("d%s=o.bytes===String?util.base64.encode(m%s,0,m%s.length):o.bytes===Array?Array.prototype.slice.call(m%s):m%s", prop, prop, prop, prop, prop);
                break;
            default: gen
            ("d%s=m%s", prop, prop);
                break;
        }
    }
    return gen;
    /* eslint-enable no-unexpected-multiline, block-scoped-var, no-redeclare */
}

/**
 * Generates a runtime message to plain object converter specific to the specified message type.
 * @param {Type} mtype Message type
 * @returns {Codegen} Codegen instance
 */
converter.toObject = function toObject(mtype) {
    /* eslint-disable no-unexpected-multiline, block-scoped-var, no-redeclare */
    var fields = mtype.fieldsArray.slice().sort(util.compareFieldsById);
    if (!fields.length)
        return util.codegen()("return {}");
    var gen = util.codegen(["m", "o"], mtype.name + "$toObject")
    ("if(!o)")
        ("o={}")
    ("var d={}");

    var repeatedFields = [],
        mapFields = [],
        normalFields = [],
        i = 0;
    for (; i < fields.length; ++i)
        if (!fields[i].partOf)
            ( fields[i].resolve().repeated ? repeatedFields
            : fields[i].map ? mapFields
            : normalFields).push(fields[i]);

    if (repeatedFields.length) { gen
    ("if(o.arrays||o.defaults){");
        for (i = 0; i < repeatedFields.length; ++i) gen
        ("d%s=[]", util.safeProp(repeatedFields[i].name));
        gen
    ("}");
    }

    if (mapFields.length) { gen
    ("if(o.objects||o.defaults){");
        for (i = 0; i < mapFields.length; ++i) gen
        ("d%s={}", util.safeProp(mapFields[i].name));
        gen
    ("}");
    }

    if (normalFields.length) { gen
    ("if(o.defaults){");
        for (i = 0; i < normalFields.length; ++i) {
            var field = normalFields[i],
                prop  = util.safeProp(field.name);
            if (field.resolvedType instanceof Enum) gen
        ("d%s=o.enums===String?%j:%j", prop, field.resolvedType.valuesById[field.typeDefault], field.typeDefault);
            else if (field.long) gen
        ("if(util.Long){")
            ("var n=new util.Long(%i,%i,%j)", field.typeDefault.low, field.typeDefault.high, field.typeDefault.unsigned)
            ("d%s=o.longs===String?n.toString():o.longs===Number?n.toNumber():n", prop)
        ("}else")
            ("d%s=o.longs===String?%j:%i", prop, field.typeDefault.toString(), field.typeDefault.toNumber());
            else if (field.bytes) {
                var arrayDefault = "[" + Array.prototype.slice.call(field.typeDefault).join(",") + "]";
                gen
        ("if(o.bytes===String)d%s=%j", prop, String.fromCharCode.apply(String, field.typeDefault))
        ("else{")
            ("d%s=%s", prop, arrayDefault)
            ("if(o.bytes!==Array)d%s=util.newBuffer(d%s)", prop, prop)
        ("}")
            } else gen
        ("d%s=%j", prop, field.typeDefault); // also messages (=null)
        } gen
    ("}");
    }
    var hasKs2 = false;
    for (i = 0; i < fields.length; ++i) {
        var field = fields[i],
            index = mtype._fieldsArray.indexOf(field),
            prop  = util.safeProp(field.name);
        if (field.map) {
            if (!hasKs2) { hasKs2 = true; gen
    ("var ks2");
            } gen
    ("if(m%s&&(ks2=Object.keys(m%s)).length){", prop, prop)
        ("d%s={}", prop)
        ("for(var j=0;j<ks2.length;++j){");
            genValuePartial_toObject(gen, field, /* sorted */ index, prop + "[ks2[j]]")
        ("}");
        } else if (field.repeated) { gen
    ("if(m%s&&m%s.length){", prop, prop)
        ("d%s=[]", prop)
        ("for(var j=0;j<m%s.length;++j){", prop);
            genValuePartial_toObject(gen, field, /* sorted */ index, prop + "[j]")
        ("}");
        } else { gen
    ("if(m%s!=null&&m.hasOwnProperty(%j)){", prop, field.name); // !== undefined && !== null
        genValuePartial_toObject(gen, field, /* sorted */ index, prop);
        if (field.partOf) gen
        ("if(o.oneofs)")
            ("d%s=%j", util.safeProp(field.partOf.name), field.name);
        }
        gen
    ("}");
    }
    return gen
    ("return d");
    /* eslint-enable no-unexpected-multiline, block-scoped-var, no-redeclare */
};
<|endoftext|>


<commit_before>/*
 * @license
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * 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
 *
 *     http://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.
 */

/**
  Slots are observable values which can change over time.

  Slots are simple single-value Model-View-Controller Models, but since
  another meaning of 'Model' is already heavily used in FOAM, Slot is
  used to avoid overloading the term.

Types of Slots include:

PropertySlot

ConstantSlot

ExpressionSlot

*/
foam.CLASS({
  package: 'foam.core',
  name: 'Slot', // ???: Rename AbstractSlot or make an Interface
  extends: null,

  methods: [
    /**
      Link two Slots together, setting both to other's value.
      Returns a Destroyable which can be used to break the link.
    */
    function link(other) {
      var sub1 = this.follow(other);
      var sub2 = other.follow(this);

      return {
        destroy: function() {
          sub1 && sub1.destroy();
          sub2 && sub2.destroy();
          sub1 = sub2 = null;
        }
      };
    },

    /**
      Have this Slot dynamically follow other's value.
      Returns a Destroyable which can be used to cancel the binding.
    */
    function follow(other) {
      return other.sub(foam.Function.bind(function() {
        this.set(other.get());
      }, this));
    },

    /**
     * Maps values from one model to another.
     * @param f maps values from srcValue to dstValue
     */
    function mapFrom(other, f) {
      var self = this;
      var l = function() {
        self.set(f(other.get()));
      };
      l();
      return other.sub(l);
    },

    function mapTo(other, f) {
      return other.mapFrom(this, f);
    },

    /**
     * Relate to another Slot.
     * @param f maps from this to other
     * @param fprime maps other to this
     */
    function relate(other, f, fPrime) {
      var self     = this;
      var feedback = false;
      var sub      = foam.core.FObject.create();

      sub.onDestroy(this.sub(function() {
        if ( feedback ) return;
        feedback = true;
        other.set(f(self.get()));
        feedback = false;
      }));

      sub.onDestroy(other.sub(function() {
        if ( feedback ) return;
        feedback = true;
        self.set(fPrime(other.get()));
        feedback = false;
      }));

      return sb;
    }
  ]
});

/**
  PropertySlot represents object properties as Slots.
  Created with calling obj.prop$ or obj.slot('prop').
  For internal use only.
 */
foam.CLASS({
  package: 'foam.core.internal',
  name: 'PropertySlot',
  extends: 'foam.core.Slot',

  methods: [
    function initArgs() { },

    function get() {
      return this.prop.get(this.obj);
    },

    function set(value) {
      return this.prop.set(this.obj, value);
    },

    function getPrev() {
      return this.oldValue;
    },

    function setPrev(value) {
      return this.oldValue = value;
    },

    function sub(l) {
      return this.obj.sub('propertyChange', this.prop.name, l);
    },

    function unsub(l) {
      this.obj.unsub('propertyChange', this.prop.name, l);
    },

    function isDefined() {
      return this.obj.hasOwnProperty(this.prop.name);
    },

    function clear() {
      this.obj.clearProperty(this.prop.name);
    }
  ]
});

/** Tracks dependencies for a dynamic function and invalidates is they change. */
foam.CLASS({
  package: 'foam.core',
  name: 'ConstantSlot',
  implements: [ 'foam.core.Slot' ],

  properties: [
    {
      name: 'value',
      getter: function() { return this.value_; },
      setter: function() {}
    }
  ],

  methods: [
    function initArgs(args) { this.value_ = args && args.value; },

    function get() { return this.value; },

    function set() { /* nop */ },

    function sub(l) { /* nop */ },

    function unsub(l) { /* nop */ }
  ]
});

/**
  Tracks dependencies for a dynamic function and invalidates is they change.

foam.CLASS({name: 'Person', properties: ['fname', 'lname']});
var p = Person.create({fname: 'John', lname: 'Smith'});
var e = foam.core.ExpressionSlot.create({
  args: [ p.fname$, p.lname$ ],
  fn: function(f, l) { return f + ' ' + l; }
});
log(e.get());
e.sub(log);
p.fname = 'Steve';
p.lname = 'Jones';
log(e.get());
Output:
 > John Smith
 > [object Object] propertyChange value [object Object]
 > [object Object] propertyChange value [object Object]
 > Steve Jones

*/
foam.CLASS({
  package: 'foam.core',
  name: 'ExpressionSlot',
  implements: [ 'foam.core.Slot' ],

  properties: [
    'args',
    'fn',
    {
      name: 'value',
      factory: function() {
        return this.fn.apply(this, this.args.map(function(a) {
          return a.get();
        }));
      }
    }
  ],

  methods: [
    function init() {
      for ( var i = 0 ; i < this.args.length ; i++ ) {
        this.onDestroy(this.args[i].sub(this.invalidate));
      }
    },

    function get() {
      return this.value;
    },

    function set() { /* nop */ },

    function sub(l) {
      return this.SUPER('propertyChange', 'value', l);
    },

    function unsub(l) {
      this.SUPER('propertyChange', 'value', l);
    }
  ],

  listeners: [
    function invalidate() { this.clearProperty('value'); }
  ]
});
<commit_msg>Fix bug in Slot.follow(), which was not copying initial value.<commit_after>/*
 * @license
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * 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
 *
 *     http://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.
 */

/**
  Slots are observable values which can change over time.

  Slots are simple single-value Model-View-Controller Models, but since
  another meaning of 'Model' is already heavily used in FOAM, Slot is
  used to avoid overloading the term.

Types of Slots include:

PropertySlot

ConstantSlot

ExpressionSlot

*/
foam.CLASS({
  package: 'foam.core',
  name: 'Slot', // ???: Rename AbstractSlot or make an Interface
  extends: null,

  methods: [
    /**
      Link two Slots together, setting both to other's value.
      Returns a Destroyable which can be used to break the link.
    */
    function link(other) {
      var sub1 = this.follow(other);
      var sub2 = other.follow(this);

      return {
        destroy: function() {
          sub1 && sub1.destroy();
          sub2 && sub2.destroy();
          sub1 = sub2 = null;
        }
      };
    },

    /**
      Have this Slot dynamically follow other's value.
      Returns a Destroyable which can be used to cancel the binding.
    */
    function follow(other) {
      var self = this;
      var l = function() { self.set(other.get()); };
      l();
      return other.sub(l);
    },

    /**
     * Maps values from one model to another.
     * @param f maps values from srcValue to dstValue
     */
    function mapFrom(other, f) {
      var self = this;
      var l = function() { self.set(f(other.get())); };
      l();
      return other.sub(l);
    },

    function mapTo(other, f) {
      return other.mapFrom(this, f);
    },

    /**
     * Relate to another Slot.
     * @param f maps from this to other
     * @param fprime maps other to this
     */
    function relate(other, f, fPrime) {
      var self     = this;
      var feedback = false;
      var sub      = foam.core.FObject.create();

      sub.onDestroy(this.sub(function() {
        if ( feedback ) return;
        feedback = true;
        other.set(f(self.get()));
        feedback = false;
      }));

      sub.onDestroy(other.sub(function() {
        if ( feedback ) return;
        feedback = true;
        self.set(fPrime(other.get()));
        feedback = false;
      }));

      return sb;
    }
  ]
});

/**
  PropertySlot represents object properties as Slots.
  Created with calling obj.prop$ or obj.slot('prop').
  For internal use only.
 */
foam.CLASS({
  package: 'foam.core.internal',
  name: 'PropertySlot',
  extends: 'foam.core.Slot',

  methods: [
    function initArgs() { },

    function get() {
      return this.prop.get(this.obj);
    },

    function set(value) {
      return this.prop.set(this.obj, value);
    },

    function getPrev() {
      return this.oldValue;
    },

    function setPrev(value) {
      return this.oldValue = value;
    },

    function sub(l) {
      return this.obj.sub('propertyChange', this.prop.name, l);
    },

    function unsub(l) {
      this.obj.unsub('propertyChange', this.prop.name, l);
    },

    function isDefined() {
      return this.obj.hasOwnProperty(this.prop.name);
    },

    function clear() {
      this.obj.clearProperty(this.prop.name);
    }
  ]
});

/** Tracks dependencies for a dynamic function and invalidates is they change. */
foam.CLASS({
  package: 'foam.core',
  name: 'ConstantSlot',
  implements: [ 'foam.core.Slot' ],

  properties: [
    {
      name: 'value',
      getter: function() { return this.value_; },
      setter: function() {}
    }
  ],

  methods: [
    function initArgs(args) { this.value_ = args && args.value; },

    function get() { return this.value; },

    function set() { /* nop */ },

    function sub(l) { /* nop */ },

    function unsub(l) { /* nop */ }
  ]
});

/**
  Tracks dependencies for a dynamic function and invalidates is they change.

foam.CLASS({name: 'Person', properties: ['fname', 'lname']});
var p = Person.create({fname: 'John', lname: 'Smith'});
var e = foam.core.ExpressionSlot.create({
  args: [ p.fname$, p.lname$ ],
  fn: function(f, l) { return f + ' ' + l; }
});
log(e.get());
e.sub(log);
p.fname = 'Steve';
p.lname = 'Jones';
log(e.get());
Output:
 > John Smith
 > [object Object] propertyChange value [object Object]
 > [object Object] propertyChange value [object Object]
 > Steve Jones

*/
foam.CLASS({
  package: 'foam.core',
  name: 'ExpressionSlot',
  implements: [ 'foam.core.Slot' ],

  properties: [
    'args',
    'fn',
    {
      name: 'value',
      factory: function() {
        return this.fn.apply(this, this.args.map(function(a) {
          return a.get();
        }));
      }
    }
  ],

  methods: [
    function init() {
      for ( var i = 0 ; i < this.args.length ; i++ ) {
        this.onDestroy(this.args[i].sub(this.invalidate));
      }
    },

    function get() {
      return this.value;
    },

    function set() { /* nop */ },

    function sub(l) {
      return this.SUPER('propertyChange', 'value', l);
    },

    function unsub(l) {
      this.SUPER('propertyChange', 'value', l);
    }
  ],

  listeners: [
    function invalidate() { this.clearProperty('value'); }
  ]
});
<|endoftext|>