<commit_before>/*
 * systems.js: Commands related to working with system configuration
 *
 * (C) 2010, Nodejitsu Inc.
 *
 */

var fs = require('fs'),
    path = require('path'),
    spawn = require('child_process').spawn,
    exec = require('child_process').exec,
    archy = require('archy'),
    fstream = require('fstream'),
    semver = require('semver'),
    quill = require('../../quill'),
    async = require('flatiron').common.async,
    composer = quill.composer;

var systems = exports;

systems.usage = [
  '`quill systems *` commands allow you to run lifecycle scripts',
  'update and configure systems on target machines',
  '',
  'quill publish   <dir>',
  'quill unpublish <system>+',
  'quill pack      <dir>',
  '',
  'quill systems list',
  'quill systems publish   <dir>',
  'quill systems unpublish <system>+',
  'quill systems package   <dir>',
  'quill systems view      <system>',
  '',
  'quill systems bump [<release>]'
];

//
// Helper function which defines a shorthand alias for
// `quill systems lifecycle <script>` to `quill <script>`.
//
function lifecycleCommand(script) {
  //
  // Define the command as a pass-thru to `systems.lifecycle`.
  //
  systems[script] = function () {
    return systems.lifecycle.apply(
      null,
      [script].concat(Array.prototype.slice.call(arguments))
    );
  };

  //
  // Usage for `quill systems <script>`.
  //
  systems[script].usage = [
    'Runs the <script> lifecycle script for the specified `targets`',
    'on the current machine.',
    '',
    'quill <script>         <system>+',
    'quill systems <script> <system>+'
  ].map(function (line) {
    return line.replace('<script>', script);
  });
}

//
// Define commands for the core lifecycle scripts:
// `install, configure, update, uninstall`.
//
var lifecycle = [
  'install',
  'configure',
  'start',
  'update',
  'uninstall'
];

lifecycle.forEach(lifecycleCommand);

//
// ### function lifecycle (script, targets, ...callback)
// #### @script {string} Name of the lifecycle script to run.
// #### @targets {Array} List of systems to run the lifecycle `script` on.
// #### @callback {function} Continuation to respond to when complete.
//
// Runs the lifecycle `script` for the specified `targets` on the current machine.
//
systems.lifecycle = function (script) {
  var systems = Array.prototype.slice.call(arguments, 1),
      callback = systems.pop(),
      lastLine = '';

  if (lifecycle.indexOf(script) === -1) {
    return callback(new Error([
      'Invalid action:',
      script.magenta + '.',
      'Valid actions are:',
      lifecycle.join(' ').yellow + '.'
    ].join(' ')), true);
  }

  quill.log.info('Executing lifecycle action ' + script.yellow);

  //
  // Helper function which outputs the results from the
  // lifecycle script on all systems.
  //
  function onData(system, data) {
    data = '' + data;
    data.split('\n').forEach(function (line) {
      line = line.trim();
      if ((!line || !line.length)
          && (lastLine || lastLine.length)) {
        lastLine = line;
        return;
      }

      quill.log.data(line);
    });
  }

  quill.on(['run', 'stdout'], onData);
  quill.on(['run', 'stderr'], onData);

  composer[script](systems, function (err) {
    quill.off(['run', 'stdout'], onData);
    quill.off(['run', 'stderr'], onData);
    return callback(err);
  });
};

//
// Usage for `quill systems lifecycle`.
//
systems.lifecycle.usage = [
  'Runs an arbitrary lifecycle script for the specified `targets`',
  'on the current machine.',
  '',
  'quill systems lifecycle <name> <system>+'
];

//
// ### function publish (target, callback)
// #### @target {string} **Optional** Directory or tarball to publish system from.
// #### @callback {function} Continuation to respond to when complete.
// Publishes the target `dir` to the registry so that it can be installed by name.
//
systems.publish = function (target, callback) {
  var responded;

  target = target || process.cwd();
  target = path.resolve(target);

  //
  // Helper function for responding to `end` or
  // `error` events on streams.
  //
  function respond(err) {
    if (!responded) {
      responded = true;
      callback(err);
    }
  }

  //
  // Helper function for bumping current system version.
  //
  function tryBump(dir, next) {
    if (!quill.argv.bump) {
      return next();
    }

    if (typeof quill.argv.bump === 'boolean') {
      quill.argv.bump = typeof quill.config.get('bump') === 'string'
        ? quill.config.get('bump')
        : 'patch';
    }

    systems.bump(quill.argv.bump, dir, next);
  }

  //
  // Helper function to publish a given directory
  //
  function publishDir(dir, next) {
    tryBump(dir, function (err) {
      if (err) {
        return next(err);
      }

      quill.log.info('Reading system.json in ' + dir.yellow);
      composer.readJson(dir, function (err, system) {
        if (err) {
          return next(err);
        }

        var name = [system.name, system.version].join('@').magenta;
        quill.log.info('Packaging system: ' + name);

        //
        // Remark: This could be cleaned if we could pipe a multi-part
        // stream to conservatory.
        //
        composer.pack(dir)
          .pipe(composer.publish(system))
          .on('upload:start', function () {
            quill.log.info('Publishing system: ' + name);
          })
          .on('error', respond)
          .on('upload:end', respond);
      });
    });
  }

  //
  // Helper function to publish all systems within a
  // given directory
  //
  function publishDirRecursive() {
    fs.readdir(target, function (err, files) {
      if (err) {
        return callback(err);
      }

      async.forEach(
        files.map(function (file) {
          return path.join(target, file);
        }),
        function (dir, next) {
          publishDir(dir, next)
        },
        callback
      );
    });
  }

  fs.stat(target, function (err, stats) {
    if (err) {
      return callback(err);
    }
    else if (stats.isDirectory()) {
      return !(quill.argv.r || quill.argv.recursive)
        ? publishDir(target, callback)
        : publishDirRecursive();
    }

    //
    // TODO: Add capability to publish from tarball
    //
    return callback(new Error('Not implemented'));
  });
};

//
// Usage for `quill systems publish`.
//
systems.publish.usage = [
  'Publishes a package to the registry so that it can be installed by name.',
  '',
  'quill publish <tarball|folder>',
  'quill systems publish <tarball|folder>'
];

//
// ### function unpublish (system+, callback)
// #### @system+ {string} System(s) to unpublish.
// #### @callback {function} Continuation to respond to.
//
// Unpublishes system(s).
//
systems.unpublish = function () {
  var args = Array.apply(null, arguments),
      callback = args.pop();

  if (args.length === 0) {
    return callback(new Error('At least one system name is required'), true);
  }

  async.forEach(
    args,
    quill.systems.destroy.bind(quill.systems),
    callback
  );
};

//
// Usage for `quill unpublish`.
//
systems.unpublish.usage = [
  'Unpublishes systems.',
  '',
  'quill unpublish <system>+',
  'quill systems unpublish <system>+'
];

//
// ### function pack (dir, callback)
// #### @dir {string} **Optional** Directory to pack files from
// #### @callback {function} Continuation to respond to when complete.
// Packages the target `dir` into a tarball ready for publication
//
systems.pack = function (dir, callback) {
  dir = dir || process.cwd();
  dir = path.resolve(dir);

  var responded;

  //
  // Helper function for responding to `end` or
  // `error` events on streams.
  //
  function respond(err) {
    if (!responded) {
      responded = true;
      callback(err);
    }
  }

  quill.log.info('Reading system.json in ' + dir.yellow);
  composer.readJson(dir, function (err, system) {
    if (err) {
      return callback(err);
    }

    var target = path.join(
      process.cwd(),
      [system.name, system.version].join('-') + '.tgz'
    );

    quill.log.info('Packaging system into ' + target.yellow);
    composer.pack(dir)
      .on('error', respond)
      .pipe(fstream.Writer({ type: 'File', path: target }))
      .on('error', respond)
      .on('close', respond);
  });
};

//
// Usage for `quill systems pack`.
//
systems.pack.usage = [
  'Packages the target directory into a tarball ready for publication',
  '',
  'quill pack <dir>',
  'quill systems pack <dir>'
];

//
// ### function list (callback)
// #### @callback {function} Continuation to respond to when complete.
// Lists all systems in the registry.
//
systems.list = function (callback) {
  quill.systems.list(function (err, systems) {
    if (err) {
      return callback(err);
    }

    var rows = [['name', 'latest', 'description']],
        colors = ['underline', 'yellow', 'grey'];

    systems.forEach(function (system) {
      rows.push([
        system.name,
        semver.maxSatisfying(Object.keys(system.versions), '*'),
        system.description
      ]);
    });

    quill.inspect.putRows('data', rows, colors);
    callback(null, systems);
  });
};

//
// Usage for `quill systems list`.
//
systems.list.usage = [
  'Lists all systems in the registry.',
  '',
  'quill list',
  'quill systems list'
];

//
// ### function list (name, callback)
// #### @name {string} Name of the system to view.
// #### @callback {function} Continuation to respond to when complete.
//
// Views details for the system with the specified `name`.
//
// TODO: Parse additional arguments. e.g. `name@version`.
//
systems.view = function (name, callback) {
  if (!name) {
    return callback(new Error('Name is required.'), true);
  }

  quill.systems.get(name, function (err, system) {
    if (err) {
      return callback(err);
    }

    //
    // TODO: Format the system object.
    //
    quill.inspect.putObject(system);
    callback();
  });
};

//
// Usage for `quill systems view`.
//
systems.view.usage = [
  'Views details for the system with the specified <name>.',
  '',
  'quill view <name>',
  'quill systems view <name>'
];

//
// ### function view (release, callback)
// #### @release {string} Release type. Defaults to "patch".
// #### @callback {function} Continuation to respond to when complete.
//
// Bump system version in the current directory.
//
systems.bump = function (release, dir, callback) {
  release = release || quill.config.get('bump') || 'patch';
  dir     = dir ? path.resolve(dir) : process.cwd();

  //
  // Use `fs` functions instead of `composer.readJson` due to changes in the
  // `system.json` introduced by `readJson`.
  //
  var systemJson = path.join(dir, 'system.json');

  quill.log.info('Reading system.json in ' + dir.yellow);
  fs.readFile(systemJson, 'utf8', function (err, data) {
    if (err) {
      return callback(err);
    }

    var oldVersion;

    try {
      data = JSON.parse(data);
    }
    catch (ex) {
      return callback(ex);
    }

    oldVersion = data.version;
    data.version = semver.inc(data.version, release);
    quill.log.info('Version ' + oldVersion.grey + ' bumped to ' + data.version.grey);
    fs.writeFile(systemJson, JSON.stringify(data, null, 2), 'utf8', callback);
  });
};

//
// Usage for `quill systems bump`.
//
systems.bump.usage = [
  'Bumps version string for the system in the current directory.',
  'Possible values for `<release>` are: "major", "minor", "patch" and "build".',
  'Default value is "patch".',
  '',
  'quill systems bump [<release>]',
];

//
// ### function installed (callback)
// #### @callback {function} Continuation to respond to.
// Lists all systems installed on the current machine.
//
systems.installed = function (callback) {
  composer.installed.list(function (err, list) {
    if (err) {
      return callback(err);
    }

    var names = Object.keys(list);

    if (!names || !names.length) {
      quill.log.warn('No systems installed');
    }
    else {
      archy(composer.hierarchy(
        quill.config.get('directories:install'),
        names.reduce(function (systems, name) {
          systems[name] = list[name].system;
          return systems;
        }, {})
      ))
      .split('\n')
      .filter(Boolean)
      .forEach(function (line) {
        quill.log.data(line);
      });
    }

    callback();
  });
};<commit_msg>[fix] Add help for `quill [systems]? installed`<commit_after>/*
 * systems.js: Commands related to working with system configuration
 *
 * (C) 2010, Nodejitsu Inc.
 *
 */

var fs = require('fs'),
    path = require('path'),
    spawn = require('child_process').spawn,
    exec = require('child_process').exec,
    archy = require('archy'),
    fstream = require('fstream'),
    semver = require('semver'),
    quill = require('../../quill'),
    async = require('flatiron').common.async,
    composer = quill.composer;

var systems = exports;

systems.usage = [
  '`quill systems *` commands allow you to run lifecycle scripts',
  'update and configure systems on target machines',
  '',
  'quill publish   <dir>',
  'quill unpublish <system>+',
  'quill pack      <dir>',
  '',
  'quill systems list',
  'quill systems publish   <dir>',
  'quill systems unpublish <system>+',
  'quill systems package   <dir>',
  'quill systems view      <system>',
  '',
  'quill systems bump [<release>]'
];

//
// Helper function which defines a shorthand alias for
// `quill systems lifecycle <script>` to `quill <script>`.
//
function lifecycleCommand(script) {
  //
  // Define the command as a pass-thru to `systems.lifecycle`.
  //
  systems[script] = function () {
    return systems.lifecycle.apply(
      null,
      [script].concat(Array.prototype.slice.call(arguments))
    );
  };

  //
  // Usage for `quill systems <script>`.
  //
  systems[script].usage = [
    'Runs the <script> lifecycle script for the specified `targets`',
    'on the current machine.',
    '',
    'quill <script>         <system>+',
    'quill systems <script> <system>+'
  ].map(function (line) {
    return line.replace('<script>', script);
  });
}

//
// Define commands for the core lifecycle scripts:
// `install, configure, update, uninstall`.
//
var lifecycle = [
  'install',
  'configure',
  'start',
  'update',
  'uninstall'
];

lifecycle.forEach(lifecycleCommand);

//
// ### function lifecycle (script, targets, ...callback)
// #### @script {string} Name of the lifecycle script to run.
// #### @targets {Array} List of systems to run the lifecycle `script` on.
// #### @callback {function} Continuation to respond to when complete.
//
// Runs the lifecycle `script` for the specified `targets` on the current machine.
//
systems.lifecycle = function (script) {
  var systems = Array.prototype.slice.call(arguments, 1),
      callback = systems.pop(),
      lastLine = '';

  if (lifecycle.indexOf(script) === -1) {
    return callback(new Error([
      'Invalid action:',
      script.magenta + '.',
      'Valid actions are:',
      lifecycle.join(' ').yellow + '.'
    ].join(' ')), true);
  }

  quill.log.info('Executing lifecycle action ' + script.yellow);

  //
  // Helper function which outputs the results from the
  // lifecycle script on all systems.
  //
  function onData(system, data) {
    data = '' + data;
    data.split('\n').forEach(function (line) {
      line = line.trim();
      if ((!line || !line.length)
          && (lastLine || lastLine.length)) {
        lastLine = line;
        return;
      }

      quill.log.data(line);
    });
  }

  quill.on(['run', 'stdout'], onData);
  quill.on(['run', 'stderr'], onData);

  composer[script](systems, function (err) {
    quill.off(['run', 'stdout'], onData);
    quill.off(['run', 'stderr'], onData);
    return callback(err);
  });
};

//
// Usage for `quill systems lifecycle`.
//
systems.lifecycle.usage = [
  'Runs an arbitrary lifecycle script for the specified `targets`',
  'on the current machine.',
  '',
  'quill systems lifecycle <name> <system>+'
];

//
// ### function publish (target, callback)
// #### @target {string} **Optional** Directory or tarball to publish system from.
// #### @callback {function} Continuation to respond to when complete.
// Publishes the target `dir` to the registry so that it can be installed by name.
//
systems.publish = function (target, callback) {
  var responded;

  target = target || process.cwd();
  target = path.resolve(target);

  //
  // Helper function for responding to `end` or
  // `error` events on streams.
  //
  function respond(err) {
    if (!responded) {
      responded = true;
      callback(err);
    }
  }

  //
  // Helper function for bumping current system version.
  //
  function tryBump(dir, next) {
    if (!quill.argv.bump) {
      return next();
    }

    if (typeof quill.argv.bump === 'boolean') {
      quill.argv.bump = typeof quill.config.get('bump') === 'string'
        ? quill.config.get('bump')
        : 'patch';
    }

    systems.bump(quill.argv.bump, dir, next);
  }

  //
  // Helper function to publish a given directory
  //
  function publishDir(dir, next) {
    tryBump(dir, function (err) {
      if (err) {
        return next(err);
      }

      quill.log.info('Reading system.json in ' + dir.yellow);
      composer.readJson(dir, function (err, system) {
        if (err) {
          return next(err);
        }

        var name = [system.name, system.version].join('@').magenta;
        quill.log.info('Packaging system: ' + name);

        //
        // Remark: This could be cleaned if we could pipe a multi-part
        // stream to conservatory.
        //
        composer.pack(dir)
          .pipe(composer.publish(system))
          .on('upload:start', function () {
            quill.log.info('Publishing system: ' + name);
          })
          .on('error', respond)
          .on('upload:end', respond);
      });
    });
  }

  //
  // Helper function to publish all systems within a
  // given directory
  //
  function publishDirRecursive() {
    fs.readdir(target, function (err, files) {
      if (err) {
        return callback(err);
      }

      async.forEach(
        files.map(function (file) {
          return path.join(target, file);
        }),
        function (dir, next) {
          publishDir(dir, next)
        },
        callback
      );
    });
  }

  fs.stat(target, function (err, stats) {
    if (err) {
      return callback(err);
    }
    else if (stats.isDirectory()) {
      return !(quill.argv.r || quill.argv.recursive)
        ? publishDir(target, callback)
        : publishDirRecursive();
    }

    //
    // TODO: Add capability to publish from tarball
    //
    return callback(new Error('Not implemented'));
  });
};

//
// Usage for `quill systems publish`.
//
systems.publish.usage = [
  'Publishes a package to the registry so that it can be installed by name.',
  '',
  'quill publish <tarball|folder>',
  'quill systems publish <tarball|folder>'
];

//
// ### function unpublish (system+, callback)
// #### @system+ {string} System(s) to unpublish.
// #### @callback {function} Continuation to respond to.
//
// Unpublishes system(s).
//
systems.unpublish = function () {
  var args = Array.apply(null, arguments),
      callback = args.pop();

  if (args.length === 0) {
    return callback(new Error('At least one system name is required'), true);
  }

  async.forEach(
    args,
    quill.systems.destroy.bind(quill.systems),
    callback
  );
};

//
// Usage for `quill unpublish`.
//
systems.unpublish.usage = [
  'Unpublishes systems.',
  '',
  'quill unpublish <system>+',
  'quill systems unpublish <system>+'
];

//
// ### function pack (dir, callback)
// #### @dir {string} **Optional** Directory to pack files from
// #### @callback {function} Continuation to respond to when complete.
// Packages the target `dir` into a tarball ready for publication
//
systems.pack = function (dir, callback) {
  dir = dir || process.cwd();
  dir = path.resolve(dir);

  var responded;

  //
  // Helper function for responding to `end` or
  // `error` events on streams.
  //
  function respond(err) {
    if (!responded) {
      responded = true;
      callback(err);
    }
  }

  quill.log.info('Reading system.json in ' + dir.yellow);
  composer.readJson(dir, function (err, system) {
    if (err) {
      return callback(err);
    }

    var target = path.join(
      process.cwd(),
      [system.name, system.version].join('-') + '.tgz'
    );

    quill.log.info('Packaging system into ' + target.yellow);
    composer.pack(dir)
      .on('error', respond)
      .pipe(fstream.Writer({ type: 'File', path: target }))
      .on('error', respond)
      .on('close', respond);
  });
};

//
// Usage for `quill systems pack`.
//
systems.pack.usage = [
  'Packages the target directory into a tarball ready for publication',
  '',
  'quill pack <dir>',
  'quill systems pack <dir>'
];

//
// ### function list (callback)
// #### @callback {function} Continuation to respond to when complete.
// Lists all systems in the registry.
//
systems.list = function (callback) {
  quill.systems.list(function (err, systems) {
    if (err) {
      return callback(err);
    }

    var rows = [['name', 'latest', 'description']],
        colors = ['underline', 'yellow', 'grey'];

    systems.forEach(function (system) {
      rows.push([
        system.name,
        semver.maxSatisfying(Object.keys(system.versions), '*'),
        system.description
      ]);
    });

    quill.inspect.putRows('data', rows, colors);
    callback(null, systems);
  });
};

//
// Usage for `quill systems list`.
//
systems.list.usage = [
  'Lists all systems in the registry.',
  '',
  'quill list',
  'quill systems list'
];

//
// ### function list (name, callback)
// #### @name {string} Name of the system to view.
// #### @callback {function} Continuation to respond to when complete.
//
// Views details for the system with the specified `name`.
//
// TODO: Parse additional arguments. e.g. `name@version`.
//
systems.view = function (name, callback) {
  if (!name) {
    return callback(new Error('Name is required.'), true);
  }

  quill.systems.get(name, function (err, system) {
    if (err) {
      return callback(err);
    }

    //
    // TODO: Format the system object.
    //
    quill.inspect.putObject(system);
    callback();
  });
};

//
// Usage for `quill systems view`.
//
systems.view.usage = [
  'Views details for the system with the specified <name>.',
  '',
  'quill view <name>',
  'quill systems view <name>'
];

//
// ### function view (release, callback)
// #### @release {string} Release type. Defaults to "patch".
// #### @callback {function} Continuation to respond to when complete.
//
// Bump system version in the current directory.
//
systems.bump = function (release, dir, callback) {
  release = release || quill.config.get('bump') || 'patch';
  dir     = dir ? path.resolve(dir) : process.cwd();

  //
  // Use `fs` functions instead of `composer.readJson` due to changes in the
  // `system.json` introduced by `readJson`.
  //
  var systemJson = path.join(dir, 'system.json');

  quill.log.info('Reading system.json in ' + dir.yellow);
  fs.readFile(systemJson, 'utf8', function (err, data) {
    if (err) {
      return callback(err);
    }

    var oldVersion;

    try {
      data = JSON.parse(data);
    }
    catch (ex) {
      return callback(ex);
    }

    oldVersion = data.version;
    data.version = semver.inc(data.version, release);
    quill.log.info('Version ' + oldVersion.grey + ' bumped to ' + data.version.grey);
    fs.writeFile(systemJson, JSON.stringify(data, null, 2), 'utf8', callback);
  });
};

//
// Usage for `quill systems bump`.
//
systems.bump.usage = [
  'Bumps version string for the system in the current directory.',
  'Possible values for `<release>` are: "major", "minor", "patch" and "build".',
  'Default value is "patch".',
  '',
  'quill systems bump [<release>]',
];

//
// ### function installed (callback)
// #### @callback {function} Continuation to respond to.
// Lists all systems installed on the current machine.
//
systems.installed = function (callback) {
  composer.installed.list(function (err, list) {
    if (err) {
      return callback(err);
    }

    var names = Object.keys(list);

    if (!names || !names.length) {
      quill.log.warn('No systems installed');
    }
    else {
      archy(composer.hierarchy(
        quill.config.get('directories:install'),
        names.reduce(function (systems, name) {
          systems[name] = list[name].system;
          return systems;
        }, {})
      ))
      .split('\n')
      .filter(Boolean)
      .forEach(function (line) {
        quill.log.data(line);
      });
    }

    callback();
  });
};

//
// Usage for `quill systems installed`.
//
systems.installed.usage = [
  'Lists all systems installed on the current machine.',
  '',
  'quill systems installed',
  'quill installed'
];<|endoftext|>


<commit_before>import Writer from './Writer';
import LiteralWriter from './LiteralWriter';
import defaults from 'lodash.defaults';
import JSDocBlockWriter from './JSDocBlockWriter';

export default class JSDocParamWriter extends Writer {
  constructor(options) {
    super(options);

    defaults(options, {
      defaultParamType: '*',
      paramTag: 'param',
      paramDescription: name => `The ${name}`,
      objectPatternName: 'config'
    });

    this.literalWriter = new LiteralWriter(options);
  }

  RestElement(node) {
    return `${this.writeTag()} {...${this.options.defaultParamType}} ${node.argument.name} - ${this.options.paramDescription(node.argument.name)}`;
  }

  AssignmentPattern(node) {
    return `${this.writeTag()} {${this.options.defaultParamType}} [${node.left.name}=${this.literalWriter.write(node.right)}] - ${this.options.paramDescription(node.left.name)}.`;
  }

  ObjectPattern(node, meta = {}) {
    if (!meta.path) {
      meta.path = this.options.objectPatternName;
    }

    let result = `${this.writeTag()} {Object} ${meta.path} - ${this.options.paramDescription(this.options.objectPatternName)}`;

    for (let property of node.properties) {
      result += `\n${this.write(property, meta)}`;
    }

    return result;
  }

  ObjectProperty(node, meta = {}) {
    if (node.value.type === 'ObjectPattern') {
      meta.path = `${meta.path}.${node.key.name}`;
      return this.write(node.value, meta);
    }

    return `${this.writeTag()} {${this.options.defaultParamType}} ${meta.path}.${node.key.name} - ${this.options.paramDescription(node.key.name)}`;
  }

  defaultWriter(node) {
    return `${this.writeTag()} {${this.options.defaultParamType}} ${node.name} - ${this.options.paramDescription(node.name)}.`;
  }

  writeTag() {
    return JSDocBlockWriter.createTag(this.options.paramTag);
  }
}
<commit_msg>add correct nesting syntax<commit_after>import Writer from './Writer';
import LiteralWriter from './LiteralWriter';
import defaults from 'lodash.defaults';
import JSDocBlockWriter from './JSDocBlockWriter';

export default class JSDocParamWriter extends Writer {
  constructor(options) {
    super(options);

    defaults(options, {
      defaultParamType: '*',
      paramTag: 'param',
      paramDescription: name => `The ${name}`
    });

    this.literalWriter = new LiteralWriter(options);
  }

  RestElement(node) {
    return `${this.writeTag()} {...${this.options.defaultParamType}} ${node.argument.name} - ${this.options.paramDescription(node.argument.name)}`;
  }

  AssignmentPattern(node) {
    return `${this.writeTag()} {${this.options.defaultParamType}} [${node.left.name}=${this.literalWriter.write(node.right)}] - ${this.options.paramDescription(node.left.name)}.`;
  }

  ObjectPattern(node, meta = {}) {
    if (!meta.path) {
      meta.path = [];
      meta.depth = -1;
    }

    meta.depth++;
    meta.path.push('$' + meta.depth);

    let result = `${this.writeTag()} {Object} ${meta.path.join('.')}}`;

    for (let property of node.properties) {
      result += `\n${this.write(property, meta)}`;
    }

    return result;
  }

  ObjectProperty(node, meta = {}) {
    if (node.value.type === 'ObjectPattern') {
      meta.path.push(node.key.name);
      return this.write(node.value, meta);
    }

    return `${this.writeTag()} {${this.options.defaultParamType}} ${meta.path.join('.')}.${node.key.name} - ${this.options.paramDescription(node.key.name)}`;
  }

  defaultWriter(node) {
    return `${this.writeTag()} {${this.options.defaultParamType}} ${node.name} - ${this.options.paramDescription(node.name)}.`;
  }

  writeTag() {
    return JSDocBlockWriter.createTag(this.options.paramTag);
  }
}
<|endoftext|>


<commit_before>import Ember from 'ember';
import Model, {attr, belongsTo, hasMany} from '@ember-data/model';
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
import boundOneWay from 'ghost-admin/utils/bound-one-way';
import moment from 'moment';
import {compare, isBlank} from '@ember/utils';
// eslint-disable-next-line ghost/ember/no-observers
import {BLANK_DOC} from 'koenig-editor/components/koenig-editor';
import {computed, observer} from '@ember/object';
import {equal, filterBy, reads} from '@ember/object/computed';
import {on} from '@ember/object/evented';
import {inject as service} from '@ember/service';

// ember-cli-shims doesn't export these so we must get them manually
const {Comparable} = Ember;

function statusCompare(postA, postB) {
    let status1 = postA.get('status');
    let status2 = postB.get('status');

    // if any of those is empty
    if (!status1 && !status2) {
        return 0;
    }

    if (!status1 && status2) {
        return -1;
    }

    if (!status2 && status1) {
        return 1;
    }

    // We have to make sure, that scheduled posts will be listed first
    // after that, draft and published will be sorted alphabetically and don't need
    // any manual comparison.

    if (status1 === 'scheduled' && (status2 === 'draft' || status2 === 'published')) {
        return -1;
    }

    if (status2 === 'scheduled' && (status1 === 'draft' || status1 === 'published')) {
        return 1;
    }

    return compare(status1.valueOf(), status2.valueOf());
}

function publishedAtCompare(postA, postB) {
    let published1 = postA.get('publishedAtUTC');
    let published2 = postB.get('publishedAtUTC');

    if (!published1 && !published2) {
        return 0;
    }

    if (!published1 && published2) {
        return -1;
    }

    if (!published2 && published1) {
        return 1;
    }

    return compare(published1.valueOf(), published2.valueOf());
}

export default Model.extend(Comparable, ValidationEngine, {
    config: service(),
    feature: service(),
    ghostPaths: service(),
    clock: service(),
    settings: service(),

    displayName: 'post',
    validationType: 'post',

    createdAtUTC: attr('moment-utc'),
    excerpt: attr('string'),
    customExcerpt: attr('string'),
    featured: attr('boolean', {defaultValue: false}),
    canonicalUrl: attr('string'),
    codeinjectionFoot: attr('string', {defaultValue: ''}),
    codeinjectionHead: attr('string', {defaultValue: ''}),
    customTemplate: attr('string'),
    ogImage: attr('string'),
    ogTitle: attr('string'),
    ogDescription: attr('string'),
    twitterImage: attr('string'),
    twitterTitle: attr('string'),
    twitterDescription: attr('string'),
    emailSubject: attr('string'),
    html: attr('string'),
    visibility: attr('string'),
    metaDescription: attr('string'),
    metaTitle: attr('string'),
    mobiledoc: attr('json-string', {defaultValue: () => JSON.parse(JSON.stringify(BLANK_DOC))}),
    plaintext: attr('string'),
    publishedAtUTC: attr('moment-utc'),
    slug: attr('string'),
    status: attr('string', {defaultValue: 'draft'}),
    title: attr('string', {defaultValue: ''}),
    updatedAtUTC: attr('moment-utc'),
    updatedBy: attr('number'),
    url: attr('string'),
    uuid: attr('string'),
    emailRecipientFilter: attr('members-segment-string', {defaultValue: null}),
    emailOnly: attr('boolean', {defaultValue: false}),

    featureImage: attr('string'),
    featureImageAlt: attr('string'),
    featureImageCaption: attr('string'),

    authors: hasMany('user', {embedded: 'always', async: false}),
    createdBy: belongsTo('user', {async: true}),
    email: belongsTo('email', {async: false}),
    newsletter: belongsTo('newsletter', {embedded: 'always', async: false}),
    publishedBy: belongsTo('user', {async: true}),
    tags: hasMany('tag', {embedded: 'always', async: false}),

    primaryAuthor: reads('authors.firstObject'),
    primaryTag: reads('tags.firstObject'),

    scratch: null,
    titleScratch: null,

    // HACK: used for validation so that date/time can be validated based on
    // eventual status rather than current status
    statusScratch: null,

    // For use by date/time pickers - will be validated then converted to UTC
    // on save. Updated by an observer whenever publishedAtUTC changes.
    // Everything that revolves around publishedAtUTC only cares about the saved
    // value so this should be almost entirely internal
    publishedAtBlogDate: '',
    publishedAtBlogTime: '',

    canonicalUrlScratch: boundOneWay('canonicalUrl'),
    customExcerptScratch: boundOneWay('customExcerpt'),
    codeinjectionFootScratch: boundOneWay('codeinjectionFoot'),
    codeinjectionHeadScratch: boundOneWay('codeinjectionHead'),
    metaDescriptionScratch: boundOneWay('metaDescription'),
    metaTitleScratch: boundOneWay('metaTitle'),
    ogDescriptionScratch: boundOneWay('ogDescription'),
    ogTitleScratch: boundOneWay('ogTitle'),
    twitterDescriptionScratch: boundOneWay('twitterDescription'),
    twitterTitleScratch: boundOneWay('twitterTitle'),
    tiers: attr('member-tier'),
    emailSubjectScratch: boundOneWay('emailSubject'),

    isPublished: equal('status', 'published'),
    isDraft: equal('status', 'draft'),
    internalTags: filterBy('tags', 'isInternal', true),
    isScheduled: equal('status', 'scheduled'),
    isSent: equal('status', 'sent'),

    isPost: equal('displayName', 'post'),
    isPage: equal('displayName', 'page'),

    hasEmail: computed('email', 'emailOnly', function () {
        return this.email !== null || this.emailOnly;
    }),
    willEmail: computed('emailRecipientFilter', function () {
        return this.emailRecipientFilter !== null;
    }),

    previewUrl: computed('uuid', 'ghostPaths.url', 'config.blogUrl', function () {
        let blogUrl = this.get('config.blogUrl');
        let uuid = this.uuid;
        // routeKeywords.preview: 'p'
        let previewKeyword = 'p';
        // New posts don't have a preview
        if (!uuid) {
            return '';
        }
        return this.get('ghostPaths.url').join(blogUrl, previewKeyword, uuid);
    }),

    isPublic: computed('visibility', function () {
        return this.visibility === 'public' ? true : false;
    }),

    visibilitySegment: computed('visibility', 'isPublic', 'tiers', function () {
        if (this.isPublic) {
            return this.settings.get('defaultContentVisibility') === 'paid' ? 'status:-free' : 'status:free,status:-free';
        } else {
            if (this.visibility === 'members') {
                return 'status:free,status:-free';
            }
            if (this.visibility === 'paid') {
                return 'status:-free';
            }
            if (this.visibility === 'tiers' && this.tiers) {
                let filter = this.tiers.map((tier) => {
                    return `tier:${tier.slug}`;
                }).join(',');
                return filter;
            }
            return this.visibility;
        }
    }),

    fullRecipientFilter: computed('newsletter.recipientFilter', 'emailRecipientFilter', function () {
        if (!this.newsletter) {
            return this.emailRecipientFilter;
        }

        return `${this.newsletter.recipientFilter}+(${this.emailRecipientFilter})`;
    }),

    // check every second to see if we're past the scheduled time
    // will only re-compute if this property is being observed elsewhere
    pastScheduledTime: computed('isScheduled', 'publishedAtUTC', 'clock.second', function () {
        if (this.isScheduled) {
            let now = moment.utc();
            let publishedAtUTC = this.publishedAtUTC || now;
            let pastScheduledTime = publishedAtUTC.diff(now, 'hours', true) < 0;

            // force a recompute
            this.get('clock.second');

            return pastScheduledTime;
        } else {
            return false;
        }
    }),

    publishedAtBlogTZ: computed('publishedAtBlogDate', 'publishedAtBlogTime', 'settings.timezone', {
        get() {
            return this._getPublishedAtBlogTZ();
        },
        set(key, value) {
            let momentValue = value ? moment(value) : null;
            this._setPublishedAtBlogStrings(momentValue);
            return this._getPublishedAtBlogTZ();
        }
    }),

    _getPublishedAtBlogTZ() {
        let publishedAtUTC = this.publishedAtUTC;
        let publishedAtBlogDate = this.publishedAtBlogDate;
        let publishedAtBlogTime = this.publishedAtBlogTime;
        let blogTimezone = this.get('settings.timezone');

        if (!publishedAtUTC && isBlank(publishedAtBlogDate) && isBlank(publishedAtBlogTime)) {
            return null;
        }

        if (publishedAtBlogDate && publishedAtBlogTime) {
            let publishedAtBlog = moment.tz(`${publishedAtBlogDate} ${publishedAtBlogTime}`, blogTimezone);

            /**
             * Note:
             * If you create a post and publish it, we send seconds to the database.
             * If you edit the post afterwards, ember would send the date without seconds, because
             * the `publishedAtUTC` is based on `publishedAtBlogTime`, which is only in seconds.
             * The date time picker doesn't use seconds.
             *
             * This condition prevents the case:
             *   - you edit a post, but you don't change the published_at time
             *   - we keep the original date with seconds
             *
             * See https://github.com/TryGhost/Ghost/issues/8603#issuecomment-309538395.
             */
            if (publishedAtUTC && publishedAtBlog.diff(publishedAtUTC.clone().startOf('minutes')) === 0) {
                return publishedAtUTC;
            }

            return publishedAtBlog;
        } else {
            return moment.tz(this.publishedAtUTC, blogTimezone);
        }
    },

    // TODO: is there a better way to handle this?
    // eslint-disable-next-line ghost/ember/no-observers
    _setPublishedAtBlogTZ: on('init', observer('publishedAtUTC', 'settings.timezone', function () {
        let publishedAtUTC = this.publishedAtUTC;
        this._setPublishedAtBlogStrings(publishedAtUTC);
    })),

    _setPublishedAtBlogStrings(momentDate) {
        if (momentDate) {
            let blogTimezone = this.get('settings.timezone');
            let publishedAtBlog = moment.tz(momentDate, blogTimezone);

            this.set('publishedAtBlogDate', publishedAtBlog.format('YYYY-MM-DD'));
            this.set('publishedAtBlogTime', publishedAtBlog.format('HH:mm'));
        } else {
            this.set('publishedAtBlogDate', '');
            this.set('publishedAtBlogTime', '');
        }
    },

    // remove client-generated tags, which have `id: null`.
    // Ember Data won't recognize/update them automatically
    // when returned from the server with ids.
    // https://github.com/emberjs/data/issues/1829
    updateTags() {
        let tags = this.tags;
        let oldTags = tags.filterBy('id', null);

        tags.removeObjects(oldTags);
        oldTags.invoke('deleteRecord');
    },

    isAuthoredByUser(user) {
        return this.authors.includes(user);
    },

    // a custom sort function is needed in order to sort the posts list the same way the server would:
    //     status: scheduled, draft, published
    //     publishedAt: DESC
    //     updatedAt: DESC
    //     id: DESC
    compare(postA, postB) {
        let updated1 = postA.get('updatedAtUTC');
        let updated2 = postB.get('updatedAtUTC');
        let idResult,
            publishedAtResult,
            statusResult,
            updatedAtResult;

        // when `updatedAt` is undefined, the model is still
        // being written to with the results from the server
        if (postA.get('isNew') || !updated1) {
            return -1;
        }

        if (postB.get('isNew') || !updated2) {
            return 1;
        }

        // TODO: revisit the ID sorting because we no longer have auto-incrementing IDs
        idResult = compare(postA.get('id'), postB.get('id'));
        statusResult = statusCompare(postA, postB);
        updatedAtResult = compare(updated1.valueOf(), updated2.valueOf());
        publishedAtResult = publishedAtCompare(postA, postB);

        if (statusResult === 0) {
            if (publishedAtResult === 0) {
                if (updatedAtResult === 0) {
                    // This should be DESC
                    return idResult * -1;
                }
                // This should be DESC
                return updatedAtResult * -1;
            }
            // This should be DESC
            return publishedAtResult * -1;
        }

        return statusResult;
    },

    // this is a hook added by the ValidationEngine mixin and is called after
    // successful validation and before this.save()
    //
    // the publishedAtBlog{Date/Time} strings are set separately so they can be
    // validated, grab that time if it exists and set the publishedAtUTC
    beforeSave() {
        let publishedAtBlogTZ = this.publishedAtBlogTZ;
        let publishedAtUTC = publishedAtBlogTZ ? publishedAtBlogTZ.utc() : null;
        this.set('publishedAtUTC', publishedAtUTC);
    }
});
<commit_msg>Fixed "and sent" appearing on publish complete step when scheduling already-sent post<commit_after>import Ember from 'ember';
import Model, {attr, belongsTo, hasMany} from '@ember-data/model';
import ValidationEngine from 'ghost-admin/mixins/validation-engine';
import boundOneWay from 'ghost-admin/utils/bound-one-way';
import moment from 'moment';
import {compare, isBlank} from '@ember/utils';
// eslint-disable-next-line ghost/ember/no-observers
import {BLANK_DOC} from 'koenig-editor/components/koenig-editor';
import {computed, observer} from '@ember/object';
import {equal, filterBy, reads} from '@ember/object/computed';
import {on} from '@ember/object/evented';
import {inject as service} from '@ember/service';

// ember-cli-shims doesn't export these so we must get them manually
const {Comparable} = Ember;

function statusCompare(postA, postB) {
    let status1 = postA.get('status');
    let status2 = postB.get('status');

    // if any of those is empty
    if (!status1 && !status2) {
        return 0;
    }

    if (!status1 && status2) {
        return -1;
    }

    if (!status2 && status1) {
        return 1;
    }

    // We have to make sure, that scheduled posts will be listed first
    // after that, draft and published will be sorted alphabetically and don't need
    // any manual comparison.

    if (status1 === 'scheduled' && (status2 === 'draft' || status2 === 'published')) {
        return -1;
    }

    if (status2 === 'scheduled' && (status1 === 'draft' || status1 === 'published')) {
        return 1;
    }

    return compare(status1.valueOf(), status2.valueOf());
}

function publishedAtCompare(postA, postB) {
    let published1 = postA.get('publishedAtUTC');
    let published2 = postB.get('publishedAtUTC');

    if (!published1 && !published2) {
        return 0;
    }

    if (!published1 && published2) {
        return -1;
    }

    if (!published2 && published1) {
        return 1;
    }

    return compare(published1.valueOf(), published2.valueOf());
}

export default Model.extend(Comparable, ValidationEngine, {
    config: service(),
    feature: service(),
    ghostPaths: service(),
    clock: service(),
    settings: service(),

    displayName: 'post',
    validationType: 'post',

    createdAtUTC: attr('moment-utc'),
    excerpt: attr('string'),
    customExcerpt: attr('string'),
    featured: attr('boolean', {defaultValue: false}),
    canonicalUrl: attr('string'),
    codeinjectionFoot: attr('string', {defaultValue: ''}),
    codeinjectionHead: attr('string', {defaultValue: ''}),
    customTemplate: attr('string'),
    ogImage: attr('string'),
    ogTitle: attr('string'),
    ogDescription: attr('string'),
    twitterImage: attr('string'),
    twitterTitle: attr('string'),
    twitterDescription: attr('string'),
    emailSubject: attr('string'),
    html: attr('string'),
    visibility: attr('string'),
    metaDescription: attr('string'),
    metaTitle: attr('string'),
    mobiledoc: attr('json-string', {defaultValue: () => JSON.parse(JSON.stringify(BLANK_DOC))}),
    plaintext: attr('string'),
    publishedAtUTC: attr('moment-utc'),
    slug: attr('string'),
    status: attr('string', {defaultValue: 'draft'}),
    title: attr('string', {defaultValue: ''}),
    updatedAtUTC: attr('moment-utc'),
    updatedBy: attr('number'),
    url: attr('string'),
    uuid: attr('string'),
    emailRecipientFilter: attr('members-segment-string', {defaultValue: null}),
    emailOnly: attr('boolean', {defaultValue: false}),

    featureImage: attr('string'),
    featureImageAlt: attr('string'),
    featureImageCaption: attr('string'),

    authors: hasMany('user', {embedded: 'always', async: false}),
    createdBy: belongsTo('user', {async: true}),
    email: belongsTo('email', {async: false}),
    newsletter: belongsTo('newsletter', {embedded: 'always', async: false}),
    publishedBy: belongsTo('user', {async: true}),
    tags: hasMany('tag', {embedded: 'always', async: false}),

    primaryAuthor: reads('authors.firstObject'),
    primaryTag: reads('tags.firstObject'),

    scratch: null,
    titleScratch: null,

    // HACK: used for validation so that date/time can be validated based on
    // eventual status rather than current status
    statusScratch: null,

    // For use by date/time pickers - will be validated then converted to UTC
    // on save. Updated by an observer whenever publishedAtUTC changes.
    // Everything that revolves around publishedAtUTC only cares about the saved
    // value so this should be almost entirely internal
    publishedAtBlogDate: '',
    publishedAtBlogTime: '',

    canonicalUrlScratch: boundOneWay('canonicalUrl'),
    customExcerptScratch: boundOneWay('customExcerpt'),
    codeinjectionFootScratch: boundOneWay('codeinjectionFoot'),
    codeinjectionHeadScratch: boundOneWay('codeinjectionHead'),
    metaDescriptionScratch: boundOneWay('metaDescription'),
    metaTitleScratch: boundOneWay('metaTitle'),
    ogDescriptionScratch: boundOneWay('ogDescription'),
    ogTitleScratch: boundOneWay('ogTitle'),
    twitterDescriptionScratch: boundOneWay('twitterDescription'),
    twitterTitleScratch: boundOneWay('twitterTitle'),
    tiers: attr('member-tier'),
    emailSubjectScratch: boundOneWay('emailSubject'),

    isPublished: equal('status', 'published'),
    isDraft: equal('status', 'draft'),
    internalTags: filterBy('tags', 'isInternal', true),
    isScheduled: equal('status', 'scheduled'),
    isSent: equal('status', 'sent'),

    isPost: equal('displayName', 'post'),
    isPage: equal('displayName', 'page'),

    hasEmail: computed('email', 'emailOnly', function () {
        return this.email !== null || this.emailOnly;
    }),

    willEmail: computed('emailRecipientFilter', 'email', function () {
        return this.emailRecipientFilter !== null && !this.email;
    }),

    previewUrl: computed('uuid', 'ghostPaths.url', 'config.blogUrl', function () {
        let blogUrl = this.get('config.blogUrl');
        let uuid = this.uuid;
        // routeKeywords.preview: 'p'
        let previewKeyword = 'p';
        // New posts don't have a preview
        if (!uuid) {
            return '';
        }
        return this.get('ghostPaths.url').join(blogUrl, previewKeyword, uuid);
    }),

    isPublic: computed('visibility', function () {
        return this.visibility === 'public' ? true : false;
    }),

    visibilitySegment: computed('visibility', 'isPublic', 'tiers', function () {
        if (this.isPublic) {
            return this.settings.get('defaultContentVisibility') === 'paid' ? 'status:-free' : 'status:free,status:-free';
        } else {
            if (this.visibility === 'members') {
                return 'status:free,status:-free';
            }
            if (this.visibility === 'paid') {
                return 'status:-free';
            }
            if (this.visibility === 'tiers' && this.tiers) {
                let filter = this.tiers.map((tier) => {
                    return `tier:${tier.slug}`;
                }).join(',');
                return filter;
            }
            return this.visibility;
        }
    }),

    fullRecipientFilter: computed('newsletter.recipientFilter', 'emailRecipientFilter', function () {
        if (!this.newsletter) {
            return this.emailRecipientFilter;
        }

        return `${this.newsletter.recipientFilter}+(${this.emailRecipientFilter})`;
    }),

    // check every second to see if we're past the scheduled time
    // will only re-compute if this property is being observed elsewhere
    pastScheduledTime: computed('isScheduled', 'publishedAtUTC', 'clock.second', function () {
        if (this.isScheduled) {
            let now = moment.utc();
            let publishedAtUTC = this.publishedAtUTC || now;
            let pastScheduledTime = publishedAtUTC.diff(now, 'hours', true) < 0;

            // force a recompute
            this.get('clock.second');

            return pastScheduledTime;
        } else {
            return false;
        }
    }),

    publishedAtBlogTZ: computed('publishedAtBlogDate', 'publishedAtBlogTime', 'settings.timezone', {
        get() {
            return this._getPublishedAtBlogTZ();
        },
        set(key, value) {
            let momentValue = value ? moment(value) : null;
            this._setPublishedAtBlogStrings(momentValue);
            return this._getPublishedAtBlogTZ();
        }
    }),

    _getPublishedAtBlogTZ() {
        let publishedAtUTC = this.publishedAtUTC;
        let publishedAtBlogDate = this.publishedAtBlogDate;
        let publishedAtBlogTime = this.publishedAtBlogTime;
        let blogTimezone = this.get('settings.timezone');

        if (!publishedAtUTC && isBlank(publishedAtBlogDate) && isBlank(publishedAtBlogTime)) {
            return null;
        }

        if (publishedAtBlogDate && publishedAtBlogTime) {
            let publishedAtBlog = moment.tz(`${publishedAtBlogDate} ${publishedAtBlogTime}`, blogTimezone);

            /**
             * Note:
             * If you create a post and publish it, we send seconds to the database.
             * If you edit the post afterwards, ember would send the date without seconds, because
             * the `publishedAtUTC` is based on `publishedAtBlogTime`, which is only in seconds.
             * The date time picker doesn't use seconds.
             *
             * This condition prevents the case:
             *   - you edit a post, but you don't change the published_at time
             *   - we keep the original date with seconds
             *
             * See https://github.com/TryGhost/Ghost/issues/8603#issuecomment-309538395.
             */
            if (publishedAtUTC && publishedAtBlog.diff(publishedAtUTC.clone().startOf('minutes')) === 0) {
                return publishedAtUTC;
            }

            return publishedAtBlog;
        } else {
            return moment.tz(this.publishedAtUTC, blogTimezone);
        }
    },

    // TODO: is there a better way to handle this?
    // eslint-disable-next-line ghost/ember/no-observers
    _setPublishedAtBlogTZ: on('init', observer('publishedAtUTC', 'settings.timezone', function () {
        let publishedAtUTC = this.publishedAtUTC;
        this._setPublishedAtBlogStrings(publishedAtUTC);
    })),

    _setPublishedAtBlogStrings(momentDate) {
        if (momentDate) {
            let blogTimezone = this.get('settings.timezone');
            let publishedAtBlog = moment.tz(momentDate, blogTimezone);

            this.set('publishedAtBlogDate', publishedAtBlog.format('YYYY-MM-DD'));
            this.set('publishedAtBlogTime', publishedAtBlog.format('HH:mm'));
        } else {
            this.set('publishedAtBlogDate', '');
            this.set('publishedAtBlogTime', '');
        }
    },

    // remove client-generated tags, which have `id: null`.
    // Ember Data won't recognize/update them automatically
    // when returned from the server with ids.
    // https://github.com/emberjs/data/issues/1829
    updateTags() {
        let tags = this.tags;
        let oldTags = tags.filterBy('id', null);

        tags.removeObjects(oldTags);
        oldTags.invoke('deleteRecord');
    },

    isAuthoredByUser(user) {
        return this.authors.includes(user);
    },

    // a custom sort function is needed in order to sort the posts list the same way the server would:
    //     status: scheduled, draft, published
    //     publishedAt: DESC
    //     updatedAt: DESC
    //     id: DESC
    compare(postA, postB) {
        let updated1 = postA.get('updatedAtUTC');
        let updated2 = postB.get('updatedAtUTC');
        let idResult,
            publishedAtResult,
            statusResult,
            updatedAtResult;

        // when `updatedAt` is undefined, the model is still
        // being written to with the results from the server
        if (postA.get('isNew') || !updated1) {
            return -1;
        }

        if (postB.get('isNew') || !updated2) {
            return 1;
        }

        // TODO: revisit the ID sorting because we no longer have auto-incrementing IDs
        idResult = compare(postA.get('id'), postB.get('id'));
        statusResult = statusCompare(postA, postB);
        updatedAtResult = compare(updated1.valueOf(), updated2.valueOf());
        publishedAtResult = publishedAtCompare(postA, postB);

        if (statusResult === 0) {
            if (publishedAtResult === 0) {
                if (updatedAtResult === 0) {
                    // This should be DESC
                    return idResult * -1;
                }
                // This should be DESC
                return updatedAtResult * -1;
            }
            // This should be DESC
            return publishedAtResult * -1;
        }

        return statusResult;
    },

    // this is a hook added by the ValidationEngine mixin and is called after
    // successful validation and before this.save()
    //
    // the publishedAtBlog{Date/Time} strings are set separately so they can be
    // validated, grab that time if it exists and set the publishedAtUTC
    beforeSave() {
        let publishedAtBlogTZ = this.publishedAtBlogTZ;
        let publishedAtUTC = publishedAtBlogTZ ? publishedAtBlogTZ.utc() : null;
        this.set('publishedAtUTC', publishedAtUTC);
    }
});
<|endoftext|>


<commit_before>/*
 * Paper.js
 *
 * This file is part of Paper.js, a JavaScript Vector Graphics Library,
 * based on Scriptographer.org and designed to be largely API compatible.
 * http://paperjs.org/
 * http://scriptographer.org/
 *
 * Copyright (c) 2011, Juerg Lehni & Jonathan Puckey
 * http://lehni.org/ & http://jonathanpuckey.com/
 *
 * Distributed under the MIT license. See LICENSE file for details.
 *
 * All rights reserved.
 */

/**
 * @name Project
 *
 * @class A Project object in Paper.js is what usually is referred to as the
 * document: The top level object that holds all the items contained in the
 * scene graph. As the term document is already taken in the browser context,
 * it is called Project.
 *
 * Projects allow the manipluation of the styles that are applied to all newly
 * created items, give access to the selected items, and will in future versions
 * offer ways to query for items in the scene graph defining specific
 * requirements, and means to persist and load from different formats, such as
 * SVG and PDF.
 *
 * The currently active project can be accessed through the
 * {@link PaperScope#project} variable.
 *
 * An array of all open projects is accessible through the
 * {@link PaperScope#projects} variable.
 */
var Project = this.Project = PaperScopeItem.extend(/** @lends Project# */{
	_list: 'projects',
	_reference: 'project',

	// TODO: Add arguments to define pages
	/**
	 * Creates a Paper.js project.
	 *
	 * When working with PaperScript, a project is automatically created for us
	 * and the {@link PaperScope#project} variable points to it.
	 *
	 * @param {View|HTMLCanvasElement} view Either a view object or an HTML
	 * Canvas element that should be wrapped in a newly created view.
	 */
	initialize: function(view) {
		// Activate straight away by passing true to base(), so paper.project is
		// set, as required by Layer and DoumentView constructors.
		this.base(true);
		this.layers = [];
		this.symbols = [];
		this.activeLayer = new Layer();
		if (view)
			this.view = view instanceof View ? view : View.create(view);
		this._currentStyle = new PathStyle();
		this._selectedItems = {};
		this._selectedItemCount = 0;
		// Change tracking, not in use for now. Activate once required:
		// this._changes = [];
		// this._changesById = {};
	},

	_needsRedraw: function() {
		if (this.view)
			this.view._redrawNeeded = true;
	},

	/**
	 * Activates this project, so all newly created items will be placed
	 * in it.
	 *
	 * @name Project#activate
	 * @function
	 */

	/**
	 * Removes this project from the {@link PaperScope#projects} list, and also
	 * removes its view, if one was defined.
	 */
	remove: function() {
		if (!this.base())
			return false;
		if (this.view)
			this.view.remove();
		return true;
	},

	/**
	 * The reference to the project's view.
	 * @name Project#view
	 * @type View
	 */

	/**
	 * The currently active path style. All selected items and newly
	 * created items will be styled with this style.
	 *
	 * @type PathStyle
	 * @bean
	 *
	 * @example {@paperscript}
	 * project.currentStyle = {
	 * 	fillColor: 'red',
	 * 	strokeColor: 'black',
	 * 	strokeWidth: 5
	 * }
	 *
	 * // The following paths will take over all style properties of
	 * // the current style:
	 * var path = new Path.Circle(new Point(75, 50), 30);
	 * var path2 = new Path.Circle(new Point(175, 50), 20);
	 *
	 * @example {@paperscript}
	 * project.currentStyle.fillColor = 'red';
	 *
	 * // The following path will take over the fill color we just set:
	 * var path = new Path.Circle(new Point(75, 50), 30);
	 * var path2 = new Path.Circle(new Point(175, 50), 20);
	 */
	getCurrentStyle: function() {
		return this._currentStyle;
	},

	setCurrentStyle: function(style) {
		// TODO: Style selected items with the style:
		this._currentStyle.initialize(style);
	},

	/**
	 * The index of the project in the {@link PaperScope#projects} list.
	 *
	 * @type Number
	 * @bean
	 */
	getIndex: function() {
		return this._index;
	},

	/**
	 * The selected items contained within the project.
	 *
	 * @type Item[]
	 * @bean
	 */
	getSelectedItems: function() {
		// TODO: Return groups if their children are all selected,
		// and filter out their children from the list.
		// TODO: The order of these items should be that of their
		// drawing order.
		var items = [];
		Base.each(this._selectedItems, function(item) {
			items.push(item);
		});
		return items;
	},

	// TODO: Implement setSelectedItems?

	_updateSelection: function(item) {
		if (item._selected) {
			this._selectedItemCount++;
			this._selectedItems[item._id] = item;
		} else {
			this._selectedItemCount--;
			delete this._selectedItems[item._id];
		}
	},

	/**
	 * Selects all items in the project.
	 */
	selectAll: function() {
		for (var i = 0, l = this.layers.length; i < l; i++)
			this.layers[i].setSelected(true);
	},

	/**
	 * Deselects all selected items in the project.
	 */
	deselectAll: function() {
		for (var i in this._selectedItems)
			this._selectedItems[i].setSelected(false);
	},

	/**
	 * Perform a hit test on the items contained within the project at the
	 * location of the specified point.
	 *
	 * The optional options object allows you to control the specifics of the
	 * hit test and may contain a combination of the following values:
	 * options.tolerance: {@code Number} - The tolerance of the hit test
	 * in points.
	 * options.type: Only hit test again a certain item
	 * type: {@link PathItem}, {@link Raster}, {@link TextItem}, etc.
	 * options.fill: {@code Boolean} - Hit test the fill of items.
	 * options.stroke: {@code Boolean} - Hit test the curves of path
	 * items, taking into account stroke width.
	 * options.segments: {@code Boolean} - Hit test for
	 * {@link Segment#point} of {@link Path} items.
	 * options.handles: {@code Boolean} - Hit test for the handles
	 * ({@link Segment#handleIn} / {@link Segment#handleOut}) of path segments.
	 * options.ends: {@code Boolean} - Only hit test for the first or
	 * last segment points of open path items.
	 * options.bounds: {@code Boolean} - Hit test the corners and
	 * side-centers of the bounding rectangle of items ({@link Item#bounds}).
	 * options.center: {@code Boolean} - Hit test the
	 * {@link Rectangle#center} of the bounding rectangle of items
	 * ({@link Item#bounds}).
	 * options.guide: {@code Boolean} - Hit test items that have
	 * {@link Item#guide} set to {@code true}.
	 * options.selected: {@code Boolean} - Only hit selected items.
	 *
	 * @param {Point} point The point where the hit test should be performed
	 * @param {Object} [options={ fill: true, stroke: true, segments: true,
	 * tolerance: true }]
	 * @return {HitResult} A hit result object that contains more
	 * information about what exactly was hit or {@code null} if nothing was
	 * hit.
	 */
	hitTest: function(point, options) {
		// We don't need to do this here, but it speeds up things since we won't
		// repeatetly convert in Item#hitTest() then.
		point = Point.read(arguments);
		options = HitResult.getOptions(Base.readValue(arguments));
		// Loop backwards, so layers that get drawn last are tested first
		for (var i = this.layers.length - 1; i >= 0; i--) {
			var res = this.layers[i].hitTest(point, options);
			if (res) return res;
		}
		return null;
	},

	/**
	 * {@grouptitle Project Hierarchy}
	 *
	 * The layers contained within the project.
	 *
	 * @name Project#layers
	 * @type Layer[]
	 */

	/**
	 * The layer which is currently active. New items will be created on this
	 * layer by default.
	 *
	 * @name Project#activeLayer
	 * @type Layer
	 */

	/**
	 * The symbols contained within the project.
	 *
	 * @name Project#symbols
	 * @type Symbol[]
	 */

	draw: function(ctx, matrix) {
		ctx.save();
		if (!matrix.isIdentity())
			matrix.applyToContext(ctx);
		var param = { offset: new Point(0, 0) };
		for (var i = 0, l = this.layers.length; i < l; i++)
			Item.draw(this.layers[i], ctx, param);
		ctx.restore();

		// Draw the selection of the selected items in the project:
		if (this._selectedItemCount > 0) {
			ctx.save();
			ctx.strokeWidth = 1;
			// TODO: use Layer#color
			ctx.strokeStyle = ctx.fillStyle = '#009dec';
			// Create a local lookup table for hierarchically concatenated
			// matrices by item id, to speed up drawing by eliminating repeated
			// concatenation of parent's matrices through caching.
			var matrices = {};
			// Descriptionf of the paramters to getGlobalMatrix():
			// mx is the container for the final concatenated matrix, passed
			// to getGlobalMatrix() on the initial call.
			// cached defines wether the result of the concatenation should be
			// cached, only used for parents of items that this is called for.
			function getGlobalMatrix(item, mx, cached) {
				var cache = cached && matrices[item._id];
				if (cache) {
					// Found a cached version, copy over the values and return
					mx.concatenate(cache);
					return mx;
				}
				if (item._parent) {
					// Get concatenated matrix from all the parents, using
					// local caching (passing true for cached):
					getGlobalMatrix(item._parent, mx, true);
					// No need to concatenate if it's the identity matrix
					if (!item._matrix.isIdentity())
						mx.concatenate(item._matrix);
				} else {
					// Simply copy over the item's matrix, since it's the root
					mx.initialize(item._matrix);
				}
				// If the result needs to be cached, create a copy since matrix
				// might be further modified through recursive calls
				if (cached)
					matrices[item._id] = mx.clone();
				return mx;
			}
			for (var id in this._selectedItems) {
				var item = this._selectedItems[id];
				item.drawSelected(ctx, getGlobalMatrix(item, matrix.clone()));
			}
			ctx.restore();
		}
	}
});
<commit_msg>Handle scroll matrices correctly when drawing selections.<commit_after>/*
 * Paper.js
 *
 * This file is part of Paper.js, a JavaScript Vector Graphics Library,
 * based on Scriptographer.org and designed to be largely API compatible.
 * http://paperjs.org/
 * http://scriptographer.org/
 *
 * Copyright (c) 2011, Juerg Lehni & Jonathan Puckey
 * http://lehni.org/ & http://jonathanpuckey.com/
 *
 * Distributed under the MIT license. See LICENSE file for details.
 *
 * All rights reserved.
 */

/**
 * @name Project
 *
 * @class A Project object in Paper.js is what usually is referred to as the
 * document: The top level object that holds all the items contained in the
 * scene graph. As the term document is already taken in the browser context,
 * it is called Project.
 *
 * Projects allow the manipluation of the styles that are applied to all newly
 * created items, give access to the selected items, and will in future versions
 * offer ways to query for items in the scene graph defining specific
 * requirements, and means to persist and load from different formats, such as
 * SVG and PDF.
 *
 * The currently active project can be accessed through the
 * {@link PaperScope#project} variable.
 *
 * An array of all open projects is accessible through the
 * {@link PaperScope#projects} variable.
 */
var Project = this.Project = PaperScopeItem.extend(/** @lends Project# */{
	_list: 'projects',
	_reference: 'project',

	// TODO: Add arguments to define pages
	/**
	 * Creates a Paper.js project.
	 *
	 * When working with PaperScript, a project is automatically created for us
	 * and the {@link PaperScope#project} variable points to it.
	 *
	 * @param {View|HTMLCanvasElement} view Either a view object or an HTML
	 * Canvas element that should be wrapped in a newly created view.
	 */
	initialize: function(view) {
		// Activate straight away by passing true to base(), so paper.project is
		// set, as required by Layer and DoumentView constructors.
		this.base(true);
		this.layers = [];
		this.symbols = [];
		this.activeLayer = new Layer();
		if (view)
			this.view = view instanceof View ? view : View.create(view);
		this._currentStyle = new PathStyle();
		this._selectedItems = {};
		this._selectedItemCount = 0;
		// Change tracking, not in use for now. Activate once required:
		// this._changes = [];
		// this._changesById = {};
	},

	_needsRedraw: function() {
		if (this.view)
			this.view._redrawNeeded = true;
	},

	/**
	 * Activates this project, so all newly created items will be placed
	 * in it.
	 *
	 * @name Project#activate
	 * @function
	 */

	/**
	 * Removes this project from the {@link PaperScope#projects} list, and also
	 * removes its view, if one was defined.
	 */
	remove: function() {
		if (!this.base())
			return false;
		if (this.view)
			this.view.remove();
		return true;
	},

	/**
	 * The reference to the project's view.
	 * @name Project#view
	 * @type View
	 */

	/**
	 * The currently active path style. All selected items and newly
	 * created items will be styled with this style.
	 *
	 * @type PathStyle
	 * @bean
	 *
	 * @example {@paperscript}
	 * project.currentStyle = {
	 * 	fillColor: 'red',
	 * 	strokeColor: 'black',
	 * 	strokeWidth: 5
	 * }
	 *
	 * // The following paths will take over all style properties of
	 * // the current style:
	 * var path = new Path.Circle(new Point(75, 50), 30);
	 * var path2 = new Path.Circle(new Point(175, 50), 20);
	 *
	 * @example {@paperscript}
	 * project.currentStyle.fillColor = 'red';
	 *
	 * // The following path will take over the fill color we just set:
	 * var path = new Path.Circle(new Point(75, 50), 30);
	 * var path2 = new Path.Circle(new Point(175, 50), 20);
	 */
	getCurrentStyle: function() {
		return this._currentStyle;
	},

	setCurrentStyle: function(style) {
		// TODO: Style selected items with the style:
		this._currentStyle.initialize(style);
	},

	/**
	 * The index of the project in the {@link PaperScope#projects} list.
	 *
	 * @type Number
	 * @bean
	 */
	getIndex: function() {
		return this._index;
	},

	/**
	 * The selected items contained within the project.
	 *
	 * @type Item[]
	 * @bean
	 */
	getSelectedItems: function() {
		// TODO: Return groups if their children are all selected,
		// and filter out their children from the list.
		// TODO: The order of these items should be that of their
		// drawing order.
		var items = [];
		Base.each(this._selectedItems, function(item) {
			items.push(item);
		});
		return items;
	},

	// TODO: Implement setSelectedItems?

	_updateSelection: function(item) {
		if (item._selected) {
			this._selectedItemCount++;
			this._selectedItems[item._id] = item;
		} else {
			this._selectedItemCount--;
			delete this._selectedItems[item._id];
		}
	},

	/**
	 * Selects all items in the project.
	 */
	selectAll: function() {
		for (var i = 0, l = this.layers.length; i < l; i++)
			this.layers[i].setSelected(true);
	},

	/**
	 * Deselects all selected items in the project.
	 */
	deselectAll: function() {
		for (var i in this._selectedItems)
			this._selectedItems[i].setSelected(false);
	},

	/**
	 * Perform a hit test on the items contained within the project at the
	 * location of the specified point.
	 *
	 * The optional options object allows you to control the specifics of the
	 * hit test and may contain a combination of the following values:
	 * options.tolerance: {@code Number} - The tolerance of the hit test
	 * in points.
	 * options.type: Only hit test again a certain item
	 * type: {@link PathItem}, {@link Raster}, {@link TextItem}, etc.
	 * options.fill: {@code Boolean} - Hit test the fill of items.
	 * options.stroke: {@code Boolean} - Hit test the curves of path
	 * items, taking into account stroke width.
	 * options.segments: {@code Boolean} - Hit test for
	 * {@link Segment#point} of {@link Path} items.
	 * options.handles: {@code Boolean} - Hit test for the handles
	 * ({@link Segment#handleIn} / {@link Segment#handleOut}) of path segments.
	 * options.ends: {@code Boolean} - Only hit test for the first or
	 * last segment points of open path items.
	 * options.bounds: {@code Boolean} - Hit test the corners and
	 * side-centers of the bounding rectangle of items ({@link Item#bounds}).
	 * options.center: {@code Boolean} - Hit test the
	 * {@link Rectangle#center} of the bounding rectangle of items
	 * ({@link Item#bounds}).
	 * options.guide: {@code Boolean} - Hit test items that have
	 * {@link Item#guide} set to {@code true}.
	 * options.selected: {@code Boolean} - Only hit selected items.
	 *
	 * @param {Point} point The point where the hit test should be performed
	 * @param {Object} [options={ fill: true, stroke: true, segments: true,
	 * tolerance: true }]
	 * @return {HitResult} A hit result object that contains more
	 * information about what exactly was hit or {@code null} if nothing was
	 * hit.
	 */
	hitTest: function(point, options) {
		// We don't need to do this here, but it speeds up things since we won't
		// repeatetly convert in Item#hitTest() then.
		point = Point.read(arguments);
		options = HitResult.getOptions(Base.readValue(arguments));
		// Loop backwards, so layers that get drawn last are tested first
		for (var i = this.layers.length - 1; i >= 0; i--) {
			var res = this.layers[i].hitTest(point, options);
			if (res) return res;
		}
		return null;
	},

	/**
	 * {@grouptitle Project Hierarchy}
	 *
	 * The layers contained within the project.
	 *
	 * @name Project#layers
	 * @type Layer[]
	 */

	/**
	 * The layer which is currently active. New items will be created on this
	 * layer by default.
	 *
	 * @name Project#activeLayer
	 * @type Layer
	 */

	/**
	 * The symbols contained within the project.
	 *
	 * @name Project#symbols
	 * @type Symbol[]
	 */

	draw: function(ctx, matrix) {
		ctx.save();
		if (!matrix.isIdentity())
			matrix.applyToContext(ctx);
		var param = { offset: new Point(0, 0) };
		for (var i = 0, l = this.layers.length; i < l; i++)
			Item.draw(this.layers[i], ctx, param);
		ctx.restore();

		// Draw the selection of the selected items in the project:
		if (this._selectedItemCount > 0) {
			ctx.save();
			ctx.strokeWidth = 1;
			// TODO: use Layer#color
			ctx.strokeStyle = ctx.fillStyle = '#009dec';
			// Create a local lookup table for hierarchically concatenated
			// matrices by item id, to speed up drawing by eliminating repeated
			// concatenation of parent's matrices through caching.
			var matrices = {};
			// Description of the paramters to getGlobalMatrix():
			// mx is the container for the final concatenated matrix, passed
			// to getGlobalMatrix() on the initial call.
			// cached defines wether the result of the concatenation should be
			// cached, only used for parents of items that this is called for.
			function getGlobalMatrix(item, mx, cached) {
				var cache = cached && matrices[item._id];
				if (cache) {
					// Found a cached version, copy over the values and return
					mx.concatenate(cache);
					return mx;
				}
				if (item._parent) {
					// Get concatenated matrix from all the parents, using
					// local caching (passing true for cached):
					getGlobalMatrix(item._parent, mx, true);
					// No need to concatenate if it's the identity matrix
					if (!item._matrix.isIdentity())
						mx.concatenate(item._matrix);
				} else {
					// Simply copy over the item's matrix, since it's the root
					mx.initialize(item._matrix);
				}
				// If the result needs to be cached, create a copy since matrix
				// might be further modified through recursive calls
				if (cached)
					matrices[item._id] = mx.clone();
				return mx;
			}
			for (var id in this._selectedItems) {
				var item = this._selectedItems[id];
				item.drawSelected(ctx,
					getGlobalMatrix(item, new Matrix()).preConcatenate(matrix));
			}
			ctx.restore();
		}
	}
});
<|endoftext|>


<commit_before>/*!
 * wq.app 0.5.1-dev - pages.js
 * Dynamically generate jQuery Mobile pages for specified URLs
 * (c) 2012-2013, S. Andrew Sheppard
 * http://wq.io/license
 */

define(['jquery', 'jquery.mobile', './router', './template'],
function($, jqm, router, tmpl) {

// Exported module object
var pages = {
    'slug': '([^/?#]+)',
    'query': '(?:[?](.*))?(?:[#](.*))?$'
};

// Configuration
pages.init = function(baseurl, opts) {
    // Define baseurl (without trailing slash) if it is not /
    if (baseurl)
        pages.info.base_url = baseurl;

    if (!opts)
        return;

    // Define tmpl404 if there is not a template named '404'
    if (opts.tmpl404)
        _404 = opts.tmpl404;

    // Re-use rendered templates
    if (opts.injectOnce)
        _injectOnce = opts.injectOnce;
};

// Register URL patterns to override default JQM behavior and inject pages
// Callback fn should call pages.go() with desired template
pages.register = function(path, fn, obj, prevent) {
    var events = "bC";
    if (!fn) {
        fn = function(match, ui, params) {
            // Assume there is a template with the same name
            pages.go(path, path, params, ui);
        };
    }
    if (prevent === undefined) {
        prevent = function(match, ui, params) {
            /* jshint unused: false */
            // By default, prevent default changePage behavior
            // (unless this is a form post and is not being handled by app.js)
            if (ui && ui.options && ui.options.data && ui.options.fromPage) {
                var $form = ui.options.fromPage.find('form');
                if ($form.data('json') !== undefined && !$form.data('json'))
                    return false;
            }
            return true;
        };
    }
    var callback = function(match, ui, params, hash, evt, $page) {
        var curpath = jqm.activePage && jqm.activePage.jqmData('url');

        // Capture URLs only, not completed pages
        if (typeof ui.toPage !== "string")
            return;

        // Avoid interfering with hash updates when popups open & close
        if (curpath == match[0] || curpath + '#' + hash == match[0])
            return;

        // Prevent default changePage behavior?
        if (typeof prevent === 'function' && prevent(match, ui, params))
            evt.preventDefault();
        else if (prevent)
            evt.preventDefault();

        fn = (typeof fn == "string" ? obj[fn] : fn);
        fn(match, ui, params, hash, evt, $page);
    };
    pages.addRoute(path, events, callback);
};

// Wrapper for router.add - adds URL base and parameter regex to path
pages.addRoute = function(path, events, fn, obj) {
    var rt = {};
    path = path.replace(/<slug>/g, pages.slug);
    var url = '^' +  pages.info.base_url + '/' + path + pages.query;
    var callback = function(etype, match, ui, page, evt) {
        var hash = match.pop();
        var params = router.getParams(match.pop());
        fn = (typeof fn == "string" ? obj[fn] : fn);
        fn(match, ui, params, hash, evt, $(page));
    };
    rt[url] = {
        'events': events,
        'handler': callback,
        'step': 'all'
    };
    router.add(rt);
};

// Render page and inject it into DOM (replace existing page if it exists)
pages.inject = function(path, template, context, pageid) {
    _updateInfo(path);
    var html  = tmpl.render(template, context);
    if (!html.match(/<div/))
        throw "No content found in template '" + template + "'!";
    var title = html.split(/<\/?title>/)[1];
    var body  = html.split(/<\/?body[^>?]*>/)[1];
    if (body)
        html = body;
    var $page = $(html.trim());
    var role = $page.jqmData('role');
    var url   = pages.info.full_path;
    var $oldpage;
    if (pageid) {
        if (pageid === true)
            pageid = template + '-page';
        $oldpage = $('#' + pageid);
    } else {
        $oldpage = $(":jqmData(url='" + url + "')");
    }
    if (role == 'popup' || role == 'panel') {
        $page.appendTo(jqm.activePage[0]);
        if (role == 'popup')
            $page.popup();
        else
            $page.panel();
        $page.trigger('create');
    } else if ($oldpage.length) {
        $oldpage.jqmData('title', title);
        if (pageid)
            $oldpage.jqmData('url', url);

        var header = ":jqmData(role='header')";
        var $header = $page.find(header).find("h1,h2,h3");
        var $oldheader = $oldpage.find(header).find("h1,h2,h3");
        $oldheader.html($header.html());

        var content = ":jqmData(role='content')";
        var $content = $page.find(content);
        var $oldcontent = $oldpage.find(content);
        $oldcontent.html($content.html());

        $oldpage.trigger('create');
        $page = $oldpage;
    } else {
        $page.attr("data-" + jqm.ns + "url", url);
        $page.attr("data-" + jqm.ns + "title", title);
        if (pageid)
            $page.attr('id', pageid);
        $page.appendTo(jqm.pageContainer);
        $page.page();
    }
    return $page;
};

// Render template only once
pages.injectOnce = function(path, template, context, id) {
    if(!id)
        id = template + "-page";
    var $page = $('#' + id);
    if (!$page.length) {
        // Initial render, use context if available
        $page = pages.inject(path, template, context);
        $page.attr("id", id);
    } else {
        // Template was already rendered; ignore context but update URL
        // - it is up to the caller to update the DOM
        _updateInfo(path);
        $page.attr("data-" + jqm.ns + "url", pages.info.full_path);
        $page.jqmData("url", pages.info.full_path);
    }
    return $page;
};

// Inject and display page
pages.go = function(path, template, context, ui, once, pageid) {
    var $page, role, options;
    once = once || _injectOnce;
    if (once)
        // Only render the template once
        $page = pages.injectOnce(path, template, context, pageid);
    else
        // Default: render the template every time the page is loaded
        $page = pages.inject(path, template, context, pageid);

    role = $page.jqmData('role');
    if (role == 'page') {
        options = ui && ui.options || {};
        options._jqmrouter_bC = true;
        if (once || _injectOnce)
            options.allowSamePageTransition = true;
        jqm.changePage($page, options);
    } else if (role == 'popup') {
        options = {};
        if (ui && ui.options) {
            options.transition = ui.options.transition;
            if (ui.options.link && ui.options.link.jqmData('position-to'))
                options.positionTo = ui.options.link.jqmData('position-to');
            else
                options.positionTo = $page.jqmData('position-to');
            // 'origin' won't work since we're opening the popup manually
            if (!options.positionTo || options.positionTo == 'origin')
                options.positionTo = ui.options.link[0];
            // Remove link highlight *after* popup is closed
            $page.bind('popupafterclose.resetlink', function() {
                ui.options.link.removeClass('ui-btn-active');
                $(this).unbind('popupafterclose.resetlink');
            });
        }
        $page.popup('open', options);
    } else if (role == 'panel') {
        $page.panel('open');
    }
    return $page;
};

// Simple 404 page helper
pages.notFound = function(url, ui) {
    var context  = {'url': url};
    pages.go(url, _404, context, ui);
};

// Context variable (accessible in templates via pages_info)
pages.info = {
    'base_url': ""
};

function _updateInfo(path) {
    pages.info.prev_path = pages.info.path;
    pages.info.path = path;
    pages.info.full_path = pages.info.base_url + "/" + path;
    tmpl.setDefault('pages_info', pages.info);
}

var _404  = "404";
var _injectOnce = false;

return pages;

});
<commit_msg>fix case when prevent function returns false<commit_after>/*!
 * wq.app 0.5.1-dev - pages.js
 * Dynamically generate jQuery Mobile pages for specified URLs
 * (c) 2012-2013, S. Andrew Sheppard
 * http://wq.io/license
 */

define(['jquery', 'jquery.mobile', './router', './template'],
function($, jqm, router, tmpl) {

// Exported module object
var pages = {
    'slug': '([^/?#]+)',
    'query': '(?:[?](.*))?(?:[#](.*))?$'
};

// Configuration
pages.init = function(baseurl, opts) {
    // Define baseurl (without trailing slash) if it is not /
    if (baseurl)
        pages.info.base_url = baseurl;

    if (!opts)
        return;

    // Define tmpl404 if there is not a template named '404'
    if (opts.tmpl404)
        _404 = opts.tmpl404;

    // Re-use rendered templates
    if (opts.injectOnce)
        _injectOnce = opts.injectOnce;
};

// Register URL patterns to override default JQM behavior and inject pages
// Callback fn should call pages.go() with desired template
pages.register = function(path, fn, obj, prevent) {
    var events = "bC";
    if (!fn) {
        fn = function(match, ui, params) {
            // Assume there is a template with the same name
            pages.go(path, path, params, ui);
        };
    }
    if (prevent === undefined) {
        prevent = function(match, ui, params) {
            /* jshint unused: false */
            // By default, prevent default changePage behavior
            // (unless this is a form post and is not being handled by app.js)
            if (ui && ui.options && ui.options.data && ui.options.fromPage) {
                var $form = ui.options.fromPage.find('form');
                if ($form.data('json') !== undefined && !$form.data('json'))
                    return false;
            }
            return true;
        };
    }
    var callback = function(match, ui, params, hash, evt, $page) {
        var curpath = jqm.activePage && jqm.activePage.jqmData('url');

        // Capture URLs only, not completed pages
        if (typeof ui.toPage !== "string")
            return;

        // Avoid interfering with hash updates when popups open & close
        if (curpath == match[0] || curpath + '#' + hash == match[0])
            return;

        // Prevent default changePage behavior?
        if (typeof prevent === 'function' && prevent(match, ui, params))
            evt.preventDefault();
        else if (typeof prevent !== 'function' && prevent)
            evt.preventDefault();

        fn = (typeof fn == "string" ? obj[fn] : fn);
        fn(match, ui, params, hash, evt, $page);
    };
    pages.addRoute(path, events, callback);
};

// Wrapper for router.add - adds URL base and parameter regex to path
pages.addRoute = function(path, events, fn, obj) {
    var rt = {};
    path = path.replace(/<slug>/g, pages.slug);
    var url = '^' +  pages.info.base_url + '/' + path + pages.query;
    var callback = function(etype, match, ui, page, evt) {
        var hash = match.pop();
        var params = router.getParams(match.pop());
        fn = (typeof fn == "string" ? obj[fn] : fn);
        fn(match, ui, params, hash, evt, $(page));
    };
    rt[url] = {
        'events': events,
        'handler': callback,
        'step': 'all'
    };
    router.add(rt);
};

// Render page and inject it into DOM (replace existing page if it exists)
pages.inject = function(path, template, context, pageid) {
    _updateInfo(path);
    var html  = tmpl.render(template, context);
    if (!html.match(/<div/))
        throw "No content found in template '" + template + "'!";
    var title = html.split(/<\/?title>/)[1];
    var body  = html.split(/<\/?body[^>?]*>/)[1];
    if (body)
        html = body;
    var $page = $(html.trim());
    var role = $page.jqmData('role');
    var url   = pages.info.full_path;
    var $oldpage;
    if (pageid) {
        if (pageid === true)
            pageid = template + '-page';
        $oldpage = $('#' + pageid);
    } else {
        $oldpage = $(":jqmData(url='" + url + "')");
    }
    if (role == 'popup' || role == 'panel') {
        $page.appendTo(jqm.activePage[0]);
        if (role == 'popup')
            $page.popup();
        else
            $page.panel();
        $page.trigger('create');
    } else if ($oldpage.length) {
        $oldpage.jqmData('title', title);
        if (pageid)
            $oldpage.jqmData('url', url);

        var header = ":jqmData(role='header')";
        var $header = $page.find(header).find("h1,h2,h3");
        var $oldheader = $oldpage.find(header).find("h1,h2,h3");
        $oldheader.html($header.html());

        var content = ":jqmData(role='content')";
        var $content = $page.find(content);
        var $oldcontent = $oldpage.find(content);
        $oldcontent.html($content.html());

        $oldpage.trigger('create');
        $page = $oldpage;
    } else {
        $page.attr("data-" + jqm.ns + "url", url);
        $page.attr("data-" + jqm.ns + "title", title);
        if (pageid)
            $page.attr('id', pageid);
        $page.appendTo(jqm.pageContainer);
        $page.page();
    }
    return $page;
};

// Render template only once
pages.injectOnce = function(path, template, context, id) {
    if(!id)
        id = template + "-page";
    var $page = $('#' + id);
    if (!$page.length) {
        // Initial render, use context if available
        $page = pages.inject(path, template, context);
        $page.attr("id", id);
    } else {
        // Template was already rendered; ignore context but update URL
        // - it is up to the caller to update the DOM
        _updateInfo(path);
        $page.attr("data-" + jqm.ns + "url", pages.info.full_path);
        $page.jqmData("url", pages.info.full_path);
    }
    return $page;
};

// Inject and display page
pages.go = function(path, template, context, ui, once, pageid) {
    var $page, role, options;
    once = once || _injectOnce;
    if (once)
        // Only render the template once
        $page = pages.injectOnce(path, template, context, pageid);
    else
        // Default: render the template every time the page is loaded
        $page = pages.inject(path, template, context, pageid);

    role = $page.jqmData('role');
    if (role == 'page') {
        options = ui && ui.options || {};
        options._jqmrouter_bC = true;
        if (once || _injectOnce)
            options.allowSamePageTransition = true;
        jqm.changePage($page, options);
    } else if (role == 'popup') {
        options = {};
        if (ui && ui.options) {
            options.transition = ui.options.transition;
            if (ui.options.link && ui.options.link.jqmData('position-to'))
                options.positionTo = ui.options.link.jqmData('position-to');
            else
                options.positionTo = $page.jqmData('position-to');
            // 'origin' won't work since we're opening the popup manually
            if (!options.positionTo || options.positionTo == 'origin')
                options.positionTo = ui.options.link[0];
            // Remove link highlight *after* popup is closed
            $page.bind('popupafterclose.resetlink', function() {
                ui.options.link.removeClass('ui-btn-active');
                $(this).unbind('popupafterclose.resetlink');
            });
        }
        $page.popup('open', options);
    } else if (role == 'panel') {
        $page.panel('open');
    }
    return $page;
};

// Simple 404 page helper
pages.notFound = function(url, ui) {
    var context  = {'url': url};
    pages.go(url, _404, context, ui);
};

// Context variable (accessible in templates via pages_info)
pages.info = {
    'base_url': ""
};

function _updateInfo(path) {
    pages.info.prev_path = pages.info.path;
    pages.info.path = path;
    pages.info.full_path = pages.info.base_url + "/" + path;
    tmpl.setDefault('pages_info', pages.info);
}

var _404  = "404";
var _injectOnce = false;

return pages;

});
<|endoftext|>