text: /*!
   * serve-lindex
   * Copyright(c) 2011 Sencha Inc.
   * Copyright(c) 2011 TJ Holowaychuk
   * Copyright(c) 2014-2015 Douglas Christopher Wilson
   * Copyright(c) 2017 Nigel Daniels
   * MIT Licensed
   */
  
  'use strict';
  
  /**
   * Module dependencies.
   * @private
   */
  
  var accepts = require('accepts');
  var createError = require('http-errors');
  var debug = require('debug')('serve-lindex');
  var escapeHtml = require('escape-html');
  var fs = require('fs')
    , path = require('path')
    , normalize = path.normalize
    , sep = path.sep
    , extname = path.extname
    , join = path.join;
  var Batch = require('batch');
  var mime = require('mime-types');
  var parseUrl = require('parseurl');
  var resolve = require('path').resolve;
  
  /**
   * Module exports.
   * @public
   */
  
  module.exports = serveIndex;
  
  /*!
   * Icon cache.
   */
  
  var cache = {};
  
  /*!
   * Default template.
   */
  
  var defaultTemplate = join(__dirname, 'public', 'directory.html');
  
  /*!
   * Stylesheet.
   */
  
  var defaultStylesheet = join(__dirname, 'public', 'style.css');
  
  /**
   * Media types and the map for content negotiation.
   */
  
  var mediaTypes = [
    'text/html',
    'text/plain',
    'application/json'
  ];
  
  var mediaType = {
    'text/html': 'html',
    'text/plain': 'plain',
    'application/json': 'json'
  };
  
  /**
   * Serve directory listings with the given `root` path.
   *
   * See Readme.md for documentation of options.
   *
   * @param {String} root
   * @param {Object} options
   * @return {Function} middleware
   * @public
   */
  
  function serveIndex(root, options) {
    var opts = options || {};
  
    // root required
    if (!root) {
      throw new TypeError('serveIndex() root path required');
    }
  
    // resolve root to absolute and normalize
    var rootPath = normalize(resolve(root) + sep);
  
    var filter = opts.filter;
    var hidden = opts.hidden;
    var icons = opts.icons;
    var stylesheet = opts.stylesheet || defaultStylesheet;
    var template = opts.template || defaultTemplate;
    var view = opts.view || 'tiles';
    var long = opts.long;
  
    return function (req, res, next) {
      if (req.method !== 'GET' && req.method !== 'HEAD') {
        res.statusCode = 'OPTIONS' === req.method ? 200 : 405;
        res.setHeader('Allow', 'GET, HEAD, OPTIONS');
        res.setHeader('Content-Length', '0');
        res.end();
        return;
      }
  
      // parse URLs
      var url = parseUrl(req);
      var originalUrl = parseUrl.original(req);
      var dir = decodeURIComponent(url.pathname);
      var originalDir = decodeURIComponent(originalUrl.pathname);
  
      // join / normalize from root dir
      var path = normalize(join(rootPath, dir));
  
      // null byte(s), bad request
      if (~path.indexOf('\0')) return next(createError(400));
  
      // malicious path
      if ((path + sep).substr(0, rootPath.length) !== rootPath) {
        debug('malicious path "%s"', path);
        return next(createError(403));
      }
  
      // determine ".." display
      var showUp = normalize(resolve(path) + sep) !== rootPath;
  
      // check if we have a directory
      debug('stat "%s"', path);
      fs.stat(path, function(err, stat){
        if (err && err.code === 'ENOENT') {
          return next();
        }
  
        if (err) {
          err.status = err.code === 'ENAMETOOLONG'
            ? 414
            : 500;
          return next(err);
        }
  
        if (!stat.isDirectory()) return next();
  
        // fetch files
        debug('readdir "%s"', path);
        fs.readdir(path, function(err, files){
          if (err) return next(err);
          if (!hidden) files = removeHidden(files);
          if (filter) files = files.filter(function(filename, index, list) {
            return filter(filename, index, list, path);
          });
          files.sort();
  
          var longFiles = [];
  
          if (long) {
              for (var file of files) {
  
                  var stats = fs.lstatSync(path + '/' + file);
  
                  stats.name = file;
                  stats.ext = extname(file);
                  stats.path = dir;
                  stats.type = stats.isDirectory() ? 'dir' : 'file';
                  longFiles.push(stats);
                  }
              }
  
          // content-negotiation
          var accept = accepts(req);
          var type = accept.type(mediaTypes);
  
          // not acceptable
          if (!type) return next(createError(406));
          serveIndex[mediaType[type]](req, res, files, long, longFiles, next, originalDir, showUp, icons, path, view, template, stylesheet);
        });
      });
    };
  };
  
  /**
   * Respond with text/html.
   */
  
  serveIndex.html = function _html(req, res, files, long, longFiles, next, dir, showUp, icons, path, view, template, stylesheet) {
    var render = typeof template !== 'function'
      ? createHtmlRender(template)
      : template
  
    if (showUp) {
      files.unshift('..');
    }
  
    // stat all files
    stat(path, files, function (err, stats) {
      if (err) return next(err);
  
      // combine the stats into the file list
      var fileList = files.map(function (file, i) {
        return { name: file, stat: stats[i] };
      });
  
      // sort file list
      fileList.sort(fileSort);
  
      // read stylesheet
      fs.readFile(stylesheet, 'utf8', function (err, style) {
        if (err) return next(err);
  
        // create locals for rendering
        var locals = {
          directory: dir,
          displayIcons: Boolean(icons),
          fileList: fileList,
          path: path,
          style: style,
          viewName: view
        };
  
        // render html
        render(locals, function (err, body) {
          if (err) return next(err);
  
          var buf = new Buffer(body, 'utf8');
          res.setHeader('Content-Type', 'text/html; charset=utf-8');
          res.setHeader('Content-Length', buf.length);
          res.end(buf);
        });
      });
    });
  };
  
  /**
   * Respond with application/json.
   */
  
  serveIndex.json = function _json(req, res, files, long, longFiles) {
  
    var body = long ? JSON.stringify(longFiles) : JSON.stringify(files);
    var buf = new Buffer(body, 'utf8');
  
    res.setHeader('Content-Type', 'application/json; charset=utf-8');
    res.setHeader('Content-Length', buf.length);
    res.end(buf);
  };
  
  /**
   * Respond with text/plain.
   */
  
  serveIndex.plain = function _plain(req, res, files, long, longFiles) {
    var body = '';
  
    if (long) {
      for (var file of longFiles) {
          body + 'file name: ' + file.name + ', type: ' + file.type + ', size: ' + file.size + ', created: ' + file.birthtime + ', modified: ' + file.mtime + '\n';
          }
      body + '\n';
    } else {
      body = files.join('\n') + '\n';
      }
  
    var buf = new Buffer(body, 'utf8');
  
    res.setHeader('Content-Type', 'text/plain; charset=utf-8');
    res.setHeader('Content-Length', buf.length);
    res.end(buf);
  };
  
  /**
   * Map html `files`, returning an html unordered list.
   * @private
   */
  
  function createHtmlFileList(files, dir, useIcons, view) {
    var html = '<ul id="files" class="view-' + escapeHtml(view) + '">'
      + (view == 'details' ? (
        '<li class="header">'
        + '<span class="name">Name</span>'
        + '<span class="size">Size</span>'
        + '<span class="date">Modified</span>'
        + '</li>') : '');
  
    html += files.map(function (file) {
      var classes = [];
      var isDir = file.stat && file.stat.isDirectory();
      var path = dir.split('/').map(function (c) { return encodeURIComponent(c); });
  
      if (useIcons) {
        classes.push('icon');
  
        if (isDir) {
          classes.push('icon-directory');
        } else {
          var ext = extname(file.name);
          var icon = iconLookup(file.name);
  
          classes.push('icon');
          classes.push('icon-' + ext.substring(1));
  
          if (classes.indexOf(icon.className) === -1) {
            classes.push(icon.className);
          }
        }
      }
  
      path.push(encodeURIComponent(file.name));
  
      var date = file.stat && file.name !== '..'
        ? file.stat.mtime.toLocaleDateString() + ' ' + file.stat.mtime.toLocaleTimeString()
        : '';
      var size = file.stat && !isDir
        ? file.stat.size
        : '';
  
      return '<li><a href="'
        + escapeHtml(normalizeSlashes(normalize(path.join('/'))))
        + '" class="' + escapeHtml(classes.join(' ')) + '"'
        + ' title="' + escapeHtml(file.name) + '">'
        + '<span class="name">' + escapeHtml(file.name) + '</span>'
        + '<span class="size">' + escapeHtml(size) + '</span>'
        + '<span class="date">' + escapeHtml(date) + '</span>'
        + '</a></li>';
    }).join('\n');
  
    html += '</ul>';
  
    return html;
  }
  
  /**
   * Create function to render html.
   */
  
  function createHtmlRender(template) {
    return function render(locals, callback) {
      // read template
      fs.readFile(template, 'utf8', function (err, str) {
        if (err) return callback(err);
  
        var body = str
          .replace(/\{style\}/g, locals.style.concat(iconStyle(locals.fileList, locals.displayIcons)))
          .replace(/\{files\}/g, createHtmlFileList(locals.fileList, locals.directory, locals.displayIcons, locals.viewName))
          .replace(/\{directory\}/g, escapeHtml(locals.directory))
          .replace(/\{linked-path\}/g, htmlPath(locals.directory));
  
        callback(null, body);
      });
    };
  }
  
  /**
   * Sort function for with directories first.
   */
  
  function fileSort(a, b) {
    // sort ".." to the top
    if (a.name === '..' || b.name === '..') {
      return a.name === b.name ? 0
        : a.name === '..' ? -1 : 1;
    }
  
    return Number(b.stat && b.stat.isDirectory()) - Number(a.stat && a.stat.isDirectory()) ||
      String(a.name).toLocaleLowerCase().localeCompare(String(b.name).toLocaleLowerCase());
  }
  
  /**
   * Map html `dir`, returning a linked path.
   */
  
  function htmlPath(dir) {
    var parts = dir.split('/');
    var crumb = new Array(parts.length);
  
    for (var i = 0; i < parts.length; i++) {
      var part = parts[i];
  
      if (part) {
        parts[i] = encodeURIComponent(part);
        crumb[i] = '<a href="' + escapeHtml(parts.slice(0, i + 1).join('/')) + '">' + escapeHtml(part) + '</a>';
      }
    }
  
    return crumb.join(' / ');
  }
  
  /**
   * Get the icon data for the file name.
   */
  
  function iconLookup(filename) {
    var ext = extname(filename);
  
    // try by extension
    if (icons[ext]) {
      return {
        className: 'icon-' + ext.substring(1),
        fileName: icons[ext]
      };
    }
  
    var mimetype = mime.lookup(ext);
  
    // default if no mime type
    if (mimetype === false) {
      return {
        className: 'icon-default',
        fileName: icons.default
      };
    }
  
    // try by mime type
    if (icons[mimetype]) {
      return {
        className: 'icon-' + mimetype.replace('/', '-'),
        fileName: icons[mimetype]
      };
    }
  
    var suffix = mimetype.split('+')[1];
  
    if (suffix && icons['+' + suffix]) {
      return {
        className: 'icon-' + suffix,
        fileName: icons['+' + suffix]
      };
    }
  
    var type = mimetype.split('/')[0];
  
    // try by type only
    if (icons[type]) {
      return {
        className: 'icon-' + type,
        fileName: icons[type]
      };
    }
  
    return {
      className: 'icon-default',
      fileName: icons.default
    };
  }
  
  /**
   * Load icon images, return css string.
   */
  
  function iconStyle(files, useIcons) {
    if (!useIcons) return '';
    var className;
    var i;
    var iconName;
    var list = [];
    var rules = {};
    var selector;
    var selectors = {};
    var style = '';
  
    for (i = 0; i < files.length; i++) {
      var file = files[i];
  
      var isDir = file.stat && file.stat.isDirectory();
      var icon = isDir
        ? { className: 'icon-directory', fileName: icons.folder }
        : iconLookup(file.name);
      var iconName = icon.fileName;
  
      selector = '#files .' + icon.className + ' .name';
  
      if (!rules[iconName]) {
        rules[iconName] = 'background-image: url(data:image/png;base64,' + load(iconName) + ');'
        selectors[iconName] = [];
        list.push(iconName);
      }
  
      if (selectors[iconName].indexOf(selector) === -1) {
        selectors[iconName].push(selector);
      }
    }
  
    for (i = 0; i < list.length; i++) {
      iconName = list[i];
      style += selectors[iconName].join(',\n') + ' {\n  ' + rules[iconName] + '\n}\n';
    }
  
    return style;
  }
  
  /**
   * Load and cache the given `icon`.
   *
   * @param {String} icon
   * @return {String}
   * @api private
   */
  
  function load(icon) {
    if (cache[icon]) return cache[icon];
    return cache[icon] = fs.readFileSync(__dirname + '/public/icons/' + icon, 'base64');
  }
  
  /**
   * Normalizes the path separator from system separator
   * to URL separator, aka `/`.
   *
   * @param {String} path
   * @return {String}
   * @api private
   */
  
  function normalizeSlashes(path) {
    return path.split(sep).join('/');
  };
  
  /**
   * Filter "hidden" `files`, aka files
   * beginning with a `.`.
   *
   * @param {Array} files
   * @return {Array}
   * @api private
   */
  
  function removeHidden(files) {
    return files.filter(function(file){
      return '.' != file[0];
    });
  }
  
  /**
   * Stat all files and return array of stat
   * in same order.
   */
  
  function stat(dir, files, cb) {
    var batch = new Batch();
  
    batch.concurrency(10);
  
    files.forEach(function(file){
      batch.push(function(done){
        fs.stat(join(dir, file), function(err, stat){
          if (err && err.code !== 'ENOENT') return done(err);
  
          // pass ENOENT as null stat, not error
          done(null, stat || null);
        });
      });
    });
  
    batch.end(cb);
  }
  
  /**
   * Icon map.
   */
  
  var icons = {
    // base icons
    'default': 'page_white.png',
    'folder': 'folder.png',
  
    // generic mime type icons
    'image': 'image.png',
    'text': 'page_white_text.png',
    'video': 'film.png',
  
    // generic mime suffix icons
    '+json': 'page_white_code.png',
    '+xml': 'page_white_code.png',
    '+zip': 'box.png',
  
    // specific mime type icons
    'application/font-woff': 'font.png',
    'application/javascript': 'page_white_code_red.png',
    'application/json': 'page_white_code.png',
    'application/msword': 'page_white_word.png',
    'application/pdf': 'page_white_acrobat.png',
    'application/postscript': 'page_white_vector.png',
    'application/rtf': 'page_white_word.png',
    'application/vnd.ms-excel': 'page_white_excel.png',
    'application/vnd.ms-powerpoint': 'page_white_powerpoint.png',
    'application/vnd.oasis.opendocument.presentation': 'page_white_powerpoint.png',
    'application/vnd.oasis.opendocument.spreadsheet': 'page_white_excel.png',
    'application/vnd.oasis.opendocument.text': 'page_white_word.png',
    'application/x-7z-compressed': 'box.png',
    'application/x-sh': 'application_xp_terminal.png',
    'application/x-font-ttf': 'font.png',
    'application/x-msaccess': 'page_white_database.png',
    'application/x-shockwave-flash': 'page_white_flash.png',
    'application/x-sql': 'page_white_database.png',
    'application/x-tar': 'box.png',
    'application/x-xz': 'box.png',
    'application/xml': 'page_white_code.png',
    'application/zip': 'box.png',
    'image/svg+xml': 'page_white_vector.png',
    'text/css': 'page_white_code.png',
    'text/html': 'page_white_code.png',
    'text/less': 'page_white_code.png',
  
    // other, extension-specific icons
    '.accdb': 'page_white_database.png',
    '.apk': 'box.png',
    '.app': 'application_xp.png',
    '.as': 'page_white_actionscript.png',
    '.asp': 'page_white_code.png',
    '.aspx': 'page_white_code.png',
    '.bat': 'application_xp_terminal.png',
    '.bz2': 'box.png',
    '.c': 'page_white_c.png',
    '.cab': 'box.png',
    '.cfm': 'page_white_coldfusion.png',
    '.clj': 'page_white_code.png',
    '.cc': 'page_white_cplusplus.png',
    '.cgi': 'application_xp_terminal.png',
    '.cpp': 'page_white_cplusplus.png',
    '.cs': 'page_white_csharp.png',
    '.db': 'page_white_database.png',
    '.dbf': 'page_white_database.png',
    '.deb': 'box.png',
    '.dll': 'page_white_gear.png',
    '.dmg': 'drive.png',
    '.docx': 'page_white_word.png',
    '.erb': 'page_white_ruby.png',
    '.exe': 'application_xp.png',
    '.fnt': 'font.png',
    '.gam': 'controller.png',
    '.gz': 'box.png',
    '.h': 'page_white_h.png',
    '.ini': 'page_white_gear.png',
    '.iso': 'cd.png',
    '.jar': 'box.png',
    '.java': 'page_white_cup.png',
    '.jsp': 'page_white_cup.png',
    '.lua': 'page_white_code.png',
    '.lz': 'box.png',
    '.lzma': 'box.png',
    '.m': 'page_white_code.png',
    '.map': 'map.png',
    '.msi': 'box.png',
    '.mv4': 'film.png',
    '.otf': 'font.png',
    '.pdb': 'page_white_database.png',
    '.php': 'page_white_php.png',
    '.pl': 'page_white_code.png',
    '.pkg': 'box.png',
    '.pptx': 'page_white_powerpoint.png',
    '.psd': 'page_white_picture.png',
    '.py': 'page_white_code.png',
    '.rar': 'box.png',
    '.rb': 'page_white_ruby.png',
    '.rm': 'film.png',
    '.rom': 'controller.png',
    '.rpm': 'box.png',
    '.sass': 'page_white_code.png',
    '.sav': 'controller.png',
    '.scss': 'page_white_code.png',
    '.srt': 'page_white_text.png',
    '.tbz2': 'box.png',
    '.tgz': 'box.png',
    '.tlz': 'box.png',
    '.vb': 'page_white_code.png',
    '.vbs': 'page_white_code.png',
    '.xcf': 'page_white_picture.png',
    '.xlsx': 'page_white_excel.png',
    '.yaws': 'page_white_code.png'
  };
  