text: const fs = require('fs-extra');
  const Promise = require('bluebird');
  const _ = require('lodash');
  const os = require('os');
  const path = require('path');
  const ASTLinter = require('./ast-linter');
  
  const ignore = [
      'node_modules',
      'bower_components',
      '.DS_Store',
      '.git',
      '.svn',
      'Thumbs.db',
      '.yarn-cache'
  ];
  
  const linter = new ASTLinter({
      partials: [],
      helpers: []
  });
  
  const readThemeStructure = function readThemeFiles(themePath, subPath, arr) {
      themePath = path.join(themePath, '.');
      subPath = subPath || '';
  
      var tmpPath = os.tmpdir(),
          inTmp = themePath.substr(0, tmpPath.length) === tmpPath;
  
      arr = arr || [];
  
      var makeResult = function makeResult(result, subFilePath, ext) {
          result.push({
              file: subFilePath,
              ext: ext
          });
          return result;
      };
  
      return fs.readdir(themePath).then(function (files) {
          return Promise.reduce(files, function (result, file) {
              var extMatch = file.match(/.*?(\.[0-9a-z]+$)/i),
                  subFilePath = path.join(subPath, file),
                  newPath = path.join(themePath, file);
  
              /**
               * don't process ignored paths, remove target file
               *
               * @TODO:
               * - gscan extracts the target zip into a tmp directory
               * - if you use gscan with `keepExtractedDir` the caller (Ghost) will use the tmp folder with the deleted ignore files
               * - what we don't support right now is to delete the ignore files from the zip
               */
              if (ignore.indexOf(file) > -1) {
                  return inTmp
                      ? fs.remove(newPath)
                          .then(function () {
                              return result;
                          })
                      : result;
              }
  
              // NOTE: lstat does not follow symlinks
              return fs.lstat(newPath).then(function (statFile) {
                  if (statFile.isDirectory()) {
                      return readThemeStructure(newPath, subFilePath, result);
                  } else {
                      return makeResult(result, subFilePath, extMatch !== null ? extMatch[1] : undefined);
                  }
              });
          }, arr);
      });
  };
  
  /**
   *
   * @param {Theme} theme
   * @returns {Promise<Theme>}
   */
  const readFiles = function readFiles(theme) {
      const themeFilesContent = _.filter(theme.files, function (themeFile) {
          if (themeFile && themeFile.ext) {
              return themeFile.ext.match(/\.hbs|\.css|\.js/ig) || themeFile.file.match(/package.json/i);
          }
      });
  
      // Setup a partials array
      theme.partials = [];
  
      // Setup the helper object
      theme.helpers = {};
  
      // CASE: we need the actual content of all css, hbs files, and package.json for
      // our checks
      return Promise.map(themeFilesContent, function (themeFile) {
          return fs.readFile(path.join(theme.path, themeFile.file), 'utf8').then(function (content) {
              themeFile.content = content;
  
              const partialMatch = themeFile.file.match(/^partials[/\\]+(.*)\.hbs$/);
              if (partialMatch) {
                  theme.partials.push(partialMatch[1]);
              }
  
              const handlebarsMatch = themeFile.file.match(/\.hbs$/);
              if (handlebarsMatch) {
                  themeFile.parsed = ASTLinter.parse(themeFile.content, themeFile.file);
  
                  processHelpers(theme, themeFile);
              }
          });
      }).then(function () {
          return theme;
      });
  };
  
  const processHelpers = function (theme, themeFile) {
      linter.verify({
          parsed: themeFile.parsed,
          rules: [
              require('./ast-linter/rules/mark-used-helpers')
          ],
          source: themeFile.content,
          moduleId: themeFile.file
      });
      for (const helper of linter.helpers) {
          if (!theme.helpers[helper.name]) {
              theme.helpers[helper.name] = [];
          }
          theme.helpers[helper.name].push(themeFile.file);
      }
  };
  
  /**
   * Works only for posts, pages and custom templates at the moment.
   *
   * @TODO:
   * This fn was added for the custom post template feature https://github.com/TryGhost/Ghost/issues/9060.
   * We've decided to extract custom templates in GScan for now, because the read-theme helper already knows which
   * hbs files are part of a theme.
   *
   * As soon as we have another use case e.g. we would like to allow to parse a custom template header with frontmatter,
   * then we need to know which template is custom, which is not. Also, it could be that
   * this function is outsourced in the future, so it can be used by GScan and Ghost. But for now, we don't pre-optimise.
   */
  const extractCustomTemplates = function extractCustomTemplates(allTemplates) {
      var toReturn = [],
          generateName = function generateName(templateName) {
              var name = templateName;
  
              name = name.replace(/^(post-|page-|custom-)/, '');
              name = name.replace(/-/g, ' ');
              name = name.replace(/\b\w/g, function (letter) {
                  return letter.toUpperCase();
              });
  
              return name.trim();
          },
          generateFor = function (templateName) {
              if (templateName.match(/^page-/)) {
                  return ['page'];
              }
  
              if (templateName.match(/^post-/)) {
                  return ['post'];
              }
  
              return ['page', 'post'];
          },
          generateSlug = function (templateName) {
              if (templateName.match(/^custom-/)) {
                  return null;
              }
  
              return templateName.match(/^(page-|post-)(.*)/)[2];
          };
  
      _.each(allTemplates, function (templateName) {
          if (templateName.match(/^(post-|page-|custom-)/) && !templateName.match(/\//)) {
              toReturn.push({
                  filename: templateName,
                  name: generateName(templateName),
                  for: generateFor(templateName),
                  slug: generateSlug(templateName)
              });
          }
      });
  
      return toReturn;
  };
  
  /**
   * Extracts from all theme files the .hbs files.
   */
  const extractTemplates = function extractTemplates(allFiles) {
      return _.reduce(allFiles, function (templates, entry) {
          // CASE: partials are added to `theme.partials`
          if (entry.file.match(/^partials[/\\]+(.*)\.hbs$/)) {
              return templates;
          }
  
          // CASE: we ignore any hbs files in assets/
          if (entry.file.match(/^assets[/\\]+(.*)\.hbs$/)) {
              return templates;
          }
  
          var tplMatch = entry.file.match(/(.*)\.hbs$/);
          if (tplMatch) {
              templates.push(tplMatch[1]);
          }
          return templates;
      }, []);
  };
  
  /**
   *
   * @param {string} themePath - path to the validated theme
   * @returns {Promise<Theme>}
   */
  module.exports = function readTheme(themePath) {
      return readThemeStructure(themePath)
          .then(function (themeFiles) {
              var allTemplates = extractTemplates(themeFiles);
  
              return readFiles({
                  path: themePath,
                  files: themeFiles,
                  templates: {
                      all: allTemplates,
                      custom: extractCustomTemplates(allTemplates)
                  },
                  // @TODO: there's no good reason to mix Object and Array formats.
                  //        They should be unified and use the one that suits best.
                  results: {
                      pass: [],
                      fail: {}
                  }
              });
          });
  };
  
  /**
   * @typedef {Object} Theme
   * @param {string} path
   * @param {string[]} files
   * @param {Object} templates
   * @param {Object[]} templates.all
   * @param {Object[]} templates.custom
   * @param {string[]} [partials]
   * @param {Object} helpers
   * @param {Object} results
   * @param {Object[]} results.pass
   * @param {Object} results.fail
   */
  