Jump To …

paths.js

#
'use strict'

var Trait = require('light-traits').Trait
#

Array shortcuts

,   _join = Array.prototype.join
,   _slice = Array.prototype.slice


/**
 * Creates an object implementing all "Path" manipulation APIs from CommonJS
 * [filesystem](http://wiki.commonjs.org/wiki/Filesystem/A) specification.
 * All the IO operation are meant to be added as a second tire on top.
 * @param {Object} source
 *    Object has to implement `workingDirectory` & `changeWorkingDirectory`
 *    functions since some of the "Path" manipulation functions (for example
 *    `absolute`) depend on them.
 * @param {Function} workingDirectory
 *    Function syncroniusly should return `String` of working directory on
 *    underlaying system, analog to `cwd`.
 * @param {Function} changeWorkingDirectory
 *    Function call should change working directory on underlying system
 */
exports.TextPathTrait = Trait(
{ _separator: Trait.required
  /**
   * Current working directory. Analog to `pwd`.
   * @returns {String}
    */
, workingDirectory: Trait.required
 /**
  * Changes working directory, analog to `cd`.
  * @param {String()} path
  * @returns {String|Promise}
   */
, changeWorkingDirectory: Trait.required
  /**
   * Returns true if path is a windows drive, which has `c:` like form.
   * @param {String} path      top slice from path
   * @returns {Boolean}
   */
, _isDrive: function _isDrive(path) {
    return path.indexOf(':') === --path.length
  }
  /**
   * Returns true if path is absolute.
   * For absolute paths on any operating system,
   * the first path component always determines whether it is relative or
   * absolute.
   * @example
   *        // on Unix, it is empty
   *        ['', 'foo'].join('/') == '/foo'
   *        '/foo'.split('/') == ['', 'foo']
   *        // while on windows it is has form of drive
   *        ['c:', 'foo'].join('\\') == 'c:\\foo'
   *        'c:\\foo'.split('\\') == ['c:', 'foo']
   *        // '' is not absolute.
   *        split('') == []
   *        // '/' is absolute.
   *        split('/') == ['', '']
   * @param {String()}
   * @returns {Boolean}
   */
, _isAbsolute: function _isAbsolute(path) {
    var parts = this.split(path), head = parts[0]
    return (parts.length == 0) ? false : '' === head || this._isDrive(head)
  }
  /**
   * Takes a variadic list of path Strings, joins them on the file system's
   * path separator, and normalizes the result.
   * [details](http://wiki.commonjs.org/wiki/Filesystem/Join)
   * @params {String()}
   * @returns {String()}
    */
, join: function join() {
#

special case for root, helps glob [""] -> "/"

    if (1 === arguments.length && '' === arguments[0]) return this._separator
#

["", ""] -> "/", ["", "a"] -> "/a" ["a"] -> "a"

    return this.normal(_join.call(arguments, this._separator)) || '.'
  }
  /**
   * returns an array of path components. If the path is absolute, the
   * first component will be an indicator of the root of the file system.
   * for file systems with drives (such as Windows), this is the drive
   * identifier with a colon, like `c:`. on Unix, this is an empty string
   * `""`. The intent is that calling `join.apply` with the result of
   * `split` as arguments will reconstruct the path.
   * @param {String()} path
   * @return {String[]}
   */
, split: function split(path) {
    path = String(path)
#

this special case helps isAbsolute distinguish an empty path from an absolute path "" -> [] NOT [""] "a" -> ["a"] "/a" -> ["", "a"]

    return '' === path ? [] : path.split(this._separator)
  }
  /**
   * function like `join` except that it treats each argument as as either
   * an absolute or relative path and, as is the convention with URL's,
   * treats everything up to the final directory separator as a location,
   * and everything afterward as an entry in that directory, even if the
   * entry refers to a directory in the underlying storage. Resolve starts
   * at the location `""` and walks to the locations referenced by each
   * path, and returns the path of the last file. Thus, `resolve(file, "")`
   * idempotently refers to the location containing a file or directory
   * entry, and `resolve(file, neighbor)` always gives the path of a file
   * in the same directory. `resolve` is useful for finding paths in the
   * `neighborhood` of a given file, while gracefully accepting both
   * absolute and relative paths at each stage.
   * [unit tests](http://github.com/kriskowal/narwhal-test/blob/master/src/test/file/resolve.js).
   * @params {String()} path
   * @return {String[]}
   */
, resolve: function resolve() {
    var root = ""
    ,   leaf = ""
    ,   parents = []
    ,   children = []
    ,   _separator = this._separator
    for (var i = 0, ii = arguments.length; i < ii; i++) {
      var path = String(arguments[i])
      if ('' == path) continue
      var parts = path.split(_separator)
      if (this._isAbsolute(path)) {
        root = parts.shift() + _separator
        parents = []
        children = []
      }
      leaf = parts.pop()
      if ('.' === leaf || '..' === leaf) {
        parts.push(leaf)
        leaf = ''
      }
      for (var j = 0, jj = parts.length; j < jj; j++) {
        var part = parts[j]
        if ('..' === part) {
          if (0 !== children.length) children.pop()
          else if (!root) parents.push('..')
        } else if ('.' !== part && '' !== part) children.push(part)
      }
    }
    path = parents.concat(children).join(_separator)
    if (path) leaf = _separator + leaf
    return root + path + leaf
  }
  /**
   * removes `'.'` path components and simplifies `'..'` paths, if
   * possible, for a given path.
   */
, normal: function() { return this.resolve.apply(this, arguments) }
  /**
   * returns the path of a file's containing directory, albeit the parent
   * directory if the file is a directory. A terminal directory separator
   * is ignored.
   */
, directory: function directory(path) {
    var parts = this.split(path)
    if ('' === parts.pop()) if ('' === parts.pop()) parts.push('')
    if (0 !== parts.length) parts.push('')
    return this.join.apply(this, parts) || '.'
  }
  /**
   * returns the part of the path that is after the last directory
   * separator. If an extension is provided and is equal to the file's
   * extension, the extension is removed from the result.
   * @param {String()} path
   * @param {String()} extension
   * @returns {String}
   */
, base: function base(path, extension) {
    var basename = this.split(path).pop() || ''
    if (undefined !== extension) {
      var l = basename.length - extension.length
      if (extension === basename.substr(l)) basename = basename.substr(0, l)
    }
    return basename
  }
  /**
   * returns the extension of a file. The extension of a file is the last
   * dot (excluding any number of initial dots) followed by one or more
   * non-dot characters. Returns an empty string if no valid extension
   * exists.
   * [unit test](http://github.com/kriskowal/narwhal-test/blob/master/src/test/file/extension.js).
   * @param {String()} path
   * @returns {String}
   */
, extension: function extension(path) {
    var basename = this.base(path).replace(/^\.+/, '')
    var index = basename.lastIndexOf('.')
    return 0 >= index ? '' : basename.substring(index)
  }
  /**
   * returns the absolute path, starting with the root of this file
   * system object, for the given path, resolved from the current
   * working directory. If the file system supports home directory
   * aliases, absolute resolves those from the root of the file system.
   * The resulting path is in normal form. On most systems, this is
   * equivalent to expanding any user directory alias, joining the path
   * to the current working directory, and normalizing the result.
   * "absolute" can be implemented in terms of "workingDirectory",
   * "join", and "normal".
   * @param {String()} path
   * @returns {String}
   */
, absolute: function absolute(path) {
    return this.resolve(this.join(this.workingDirectory(), ''), path)
  }
  /*
   * returns the canonical path to a given abstract path. Canonical paths
   * are both absolute and intrinsic, such that all paths that refer to a
   * given file (whether it exists or not) have the same corresponding
   * canonical path. This function is equivalent to joining the given path
   * to the current working directory (if the path is relative), joining
   * all symbolic links along the path, and normalizing the result to
   * remove relative path (. or ..) references.
   * When the underlying implementation is built on a Unicode-aware file
   * system, Unicode normalization must also be performed on the path using
   * the same normal form as the underlying file system.
   * * It is not required that paths whose directories do not exist have a
   * canonical representation. Such paths will be canonicalized as
   * "undefined". Note: this point has caused some argument, and the exact
   * behaviour in this case needs to be determined.
   * @path {String()} path
   * @returns {String}
   */
, canonical: function canonical(path) {
    var paths = [this.workingDirectory(), path]
    ,   outs = []
    ,   prev
    for (var i = 0, ii = paths.length; i < ii; i++) {
      var path = paths[i], ins = this.split(path)
      if (this._isDrive(ins[0])) outs = [ins.shift()]
      while (ins.length) {
        var leaf = ins.shift()
        , consider = this.join.apply(this, outs.concat([leaf]))
#

cycle breaker; does not throw an error since every invalid path must also have an intrinsic canonical name.

        if (consider == prev) {
          ins.unshift.apply(ins, split(link))
          break
        }
        prev = consider
        try {
          var link = basename.readlink(consider)
        } catch (e) { link = undefined }
        if (undefined !== link) ins.unshift.apply(ins, split(link))
        else outs.push(leaf)
      }
    }
    return this.join.apply(this, outs)
  }
  /**
   * returns the relative path from one path to another using only ".." to
   * traverse up to the two paths' common ancestor. If the target is
   * omitted, returns the path to the source from the current working
   * directory
   * @param {String()} source
   * @param {String()} target
   * @returns {String}
   */
, relative: function relative(source, target) {
    var _separator = this._separator
    if (!target) {
      target = source
      source = this.workingDirectory() + _separator
    }
    source = this.absolute(source)
    target = this.absolute(target)
    source = this.split(source)
    target = this.split(target)
    source.pop()
    while (
      0 !== source.length &&
      0 !== target.length &&
      target[0] === source[0]
    ) {
      source.shift()
      target.shift()
    }
    while (0 !== source.length) {
      source.shift()
      target.unshift('..')
    }
    return target.join(_separator)
  }
})

var PathTypeTrait = Trait(
  { _textPath: Trait.required
  , toString: Trait.required
  , constructor: Trait.required

  , valueOf: function valueOf() {
    return this.toString()
  }
  /**
   * Path instances inherit from the String prototype.
   * returns a Path object. Path is a chainable shorthand for working with
   * paths. Path objects have no more or less authority to manipulate the
   * file system that produces them. If multiple arguments are passed to
   * the Path constructor, they are joined to construct the corresponding
   * path. If an argument is an Array, as ascertained by Array.isArray, it
   * must conform to normal output of split, meaning that if the first
   * value is a drive or root, the components are joined absolutely, and
   * otherwise they are joined relatively.
   */
, absolute: function absolute() {
    return this.constructor(this._textPath.absolute(this.toString()))
  }
, join: function join(/*paths...*/) {
    var paths = _slice.call(arguments)
    paths.unshift(this.toString())
    return this.constructor(this._textPath.join.apply(this._textPath, paths))
  }
, split: function split() {
    return this._textPath.split(this.toString())
  }
, resolve: function resolve(/*paths...*/) {
    var paths = _slice.call(arguments)
    paths.unshift(this.toString())
    return this.constructor(this._textPath.resolve.apply(this._textPath, paths))
  }
, to: function to(target) {
    return this.constructor(this._textPath.relative(this.toString(), target))
  }
, from: function from(target) {
    return this.constructor(this._textPath.relative(target, this.toString()))
  }
, base: function base(extension) {
    return this.constructor(this._textPath.base(this.toString(), extension))
  }
, extension: function extension() {
    return this.constructor(this._textPath.extension(this.toString()))
  }
, canonical: function canonical(path) {
    return this.constructor(this._textPath.canonical(this.toString(), path))
  }
, directory: function directory() {
    return this.constructor(this._textPath.directory(this.toString()))
  }
, normal: function normal() {
    return this.constructor(this._textPath.normal(this.toString()))
  }
, relative: function relative(target) {
    return this.constructor(this._textPath.relative(this.toString(), target))
  }
})

exports.ObjectPathTrait = Trait(
{ workingDirectory: Trait.required
, workingDirectoryPath: function workingDirectoryPath() {
    return this.Path(this.workingDirectory())
  }
, Path: function Path(paths/*, ...*/) {
    var path = ('string' === typeof paths) ? paths
      : (1 === paths.length && '' === paths[0]) ? ''
      : this.join.apply(this, paths) || '.'

    return PathTypeTrait.create(
    Object.create(String.prototype,
      { _textPath: { value: this }
      , toString: { value: path.toString.bind(path) }
      , constructor: { value: Path }
      })
    )
  }
, path: function(paths/*, ...*/) {
    return this.Path.apply(this, arguments)
  }
})

exports.FSPathsTrait = Trait(exports.TextPathTrait, exports.ObjectPathTrait)