compile.js

'use strict';

var CACHE_DIR;
var _ = require('./util.js');
var rType = /\s+type\s*=\s*(['"]?)((?:text|application)\/(.*?))\1/i;
var memoryCache = {};
var compileStack = [];
var path = require('path');

// 待编译完成后,清空内存缓存
fis.on('release:end', function() {
  memoryCache = {};
  compileStack = [];
});
fis.on('release:error', function() {
  memoryCache = {};
  compileStack = [];
});

function revertFromMemory(file, inCache) {
  file.revertFromCacheData(inCache.getCacheData());
  file.cache = inCache.cache;
  file.setContent(inCache.getContent());
};

/**
 * 编译相关函数入口, 输入为 文件对象,没有输出,直接修改文件对象内容。
 *
 * 默认文件编译会尝试从缓存中读取,如果缓存有效,将跳过编译。
 *
 * @example
 * var file = fis.file(filepath);
 * fis.compile(file);
 * console.log(file.getContent());
 *
 * @function
 * @param {String} file 文件对象
 * @namespace fis.compile
 */
var exports = module.exports = function(file, context) {
  if (!CACHE_DIR) {
    fis.log.error('uninitialized compile cache directory.');
  }
  file = fis.file.wrap(file);

  if (!file.realpath) {
    error('unable to compile [' + file.subpath + ']: Invalid file realpath.');
  }

  if (file.useCache && memoryCache[file.realpath]) {
    revertFromMemory(file, memoryCache[file.realpath]);
    return file;
  }

  // 只有启用 cache 的时候才存入 memoryCache
  file.useCache && (memoryCache[file.realpath] = file);

  // lint 置前,不使用文件缓存
  if (file.isText() && exports.settings.useLint) {
    pipe(file, 'lint', true);
  }

  adjust(file);

  fis.log.debug('compile [' + file.realpath + '] start');
  fis.emit('compile:start', file);
  ~compileStack.indexOf(file.realpath) || compileStack.push(file.realpath);
  if (file.isFile()) {
    if (file.useCompile && file.ext && file.ext !== '.') {
      var cache = file.cache = fis.cache(file.realpath, CACHE_DIR),
        revertObj = {};
      if (file.useCache && cache.revert(revertObj)) {
        exports.settings.beforeCacheRevert(file);
        file.revertFromCacheData(revertObj.info);
        if (file.isText()) {
          revertObj.content = revertObj.content.toString('utf8');
        }
        file.setContent(revertObj.content);
        exports.settings.afterCacheRevert(file);
      } else {
        exports.settings.beforeCompile(file);
        file.setContent(fis.util.read(file.realpath));
        process(file);
        exports.settings.afterCompile(file);
        fis.log.debug('Save cache [%s] start', file.subpath);
        file.useCache && cache.save(file.getContent(), file.getCacheData());
        fis.log.debug('Save cache [%s] end', file.subpath);
      }
    } else {
      file.setContent(file.isText() ? fis.util.read(file.realpath) : fis.util.fs.readFileSync(file.realpath));
    }
  } else if (file.useCompile && file.ext && file.ext !== '.') {
    process(file, context);
  }
  if (file.useHash) {
    file.getHash();
  }
  file.compiled = true;
  fis.log.debug('compile [' + file.realpath + '] end');
  fis.emit('compile:end', file);

  // 是文件变化触发的重新编译,不进入相关文件编译。
  context && context.fromWatch || file.links.forEach(function(subpath) {
    var f = fis.file.wrap(fis.project.getProjectPath() + subpath);

    if (f.exists() && !~compileStack.indexOf(f.realpath)) {
      compileStack.push(f.realpath);
      fis.emit('compile:add', f);
    }
  });

  compileStack.pop();
  return file;
};

/**
 * fis 编译默认的配置项
 * @property {Boolean} debug 如果设置成了 true, 那么编译缓存将会使用 debug 文件夹来存储缓存。
 * @property {Function} beforeCacheRevert 当文件从缓存中还原回来前执行。
 * @property {Function} afterCacheRevert 当文件从缓存中还原回来后执行。
 * @property {Function} beforeCompile 当文件开始编译前执行。
 * @property {Function} afterCompile 当文件开始编译后执行。
 * @name settings
 * @memberOf fis.compile
 */
exports.settings = {
  debug: false,
  useLint: false,
  beforeCacheRevert: function() {},
  afterCacheRevert: function() {},
  beforeCompile: function() {},
  afterCompile: function() {}
};

/**
 * 在编译前,初始化配置项。关于配置项,请查看 {@link fis.compile.settings}
 * @param  {Object} opt
 * @return {String}     缓存文件夹路径
 * @memberOf fis.compile
 * @name setup
 * @function
 */
exports.setup = function(opt) {
  var settings = exports.settings;
  if (opt) {
    fis.util.map(settings, function(key) {
      if (typeof opt[key] !== 'undefined') {
        settings[key] = opt[key];
      }
    });
  }

  CACHE_DIR = 'compile/';
  if (settings.unique) {
    CACHE_DIR += Date.now() + '-' + Math.random();
  } else {
    CACHE_DIR += '' + (settings.debug ? 'debug' : 'release') + '-' + fis.project.currentMedia();
  }

  return CACHE_DIR;
};

/**
 * 清除缓存
 * @param  {String} name 想要清除的缓存目录,缺省为清除默认缓存或全部缓存
 * @memberOf fis.compile
 * @name clean
 * @function
 */
exports.clean = function(name) {
  if (name) {
    fis.cache.clean('compile/' + name);
  } else if (CACHE_DIR) {
    fis.cache.clean(CACHE_DIR);
  } else {
    fis.cache.clean('compile');
  }
};


/**
 * fis 中间码管理器。
 * @namespace fis.compile.lang
 */
var map = exports.lang = (function() {
  var keywords = [];
  var delim = '\u001F'; // Unit Separator
  var rdelim = '\\u001F';
  var slice = [].slice;
  var regInvalid = false;
  var reg = null;
  var map = {

    /**
     * 添加其他中间码类型。
     * @param {String} type 类型
     * @function add
     * @memberOf fis.compile.lang
     */
    add: function(type) {
      if (~keywords.indexOf(type)) {
        return this;
      }
      var stack = [];
      keywords.push(type);
      regInvalid = true;
      map[type] = {
        wrap: function(value) {
          return this.ld + slice.call(arguments, 0).join(delim) + this.rd;
        }
      };

      // 定义map.ld
      Object.defineProperty(map[type], 'ld', {
        get: function() {
          var depth = stack.length;
          stack.push(depth);
          return delim + type + depth + delim;
        }
      });

      // 定义map.rd
      Object.defineProperty(map[type], 'rd', {
        get: function() {
          return delim + stack.pop() + type + delim;
        }
      });
    }
  };

  /**
   * 获取能识别中间码的正则
   * @name reg
   * @type {RegExp}
   * @memberOf fis.compile.lang
   */
  Object.defineProperty(map, 'reg', {
    get: function() {
      if (regInvalid || !reg) {
        reg = new RegExp(
          rdelim + '(' + keywords.join('|') + ')(\\d+?)' + rdelim + '([^' + rdelim + ']*?)(?:' + rdelim + '([^' + rdelim + ']*?))?' + rdelim + '\\2\\1' + rdelim,
          'g'
        );
        regInvalid = false;
      }

      return reg;
    }
  });

  // 默认支持的中间码
  [
    'require', // 同步依赖文件。
    'jsRequire', // 同步 js 依赖
    'embed', // 内嵌其他文件
    'jsEmbed', // 内嵌 js 文件内容
    'async', // 异步依赖
    'jsAsync', // js 异步依赖
    'uri', // 替换成目标文件的 url
    'dep', // 简单的标记依赖
    'id', // 替换成目标文件的 id
    'hash', // 替换成目标文件的 md5 戳。
    'moduleId', // 替换成目标文件的 moduleId
    'xlang', // 用来内嵌其他语言
    'inlineStyle', // 内联样式
    'sourceMap',
    'info' // 能用来包括其他中间码,包裹后可以起到其他中间码的作用,但是不会修改代码源码。
  ].forEach(map.add);

  return map;
})();

/**
 * 判断info.query是否为inline
 *
 * - `abc?__inline` return true
 * - `abc?__inlinee` return false
 * - `abc?a=1&__inline'` return true
 * - `abc?a=1&__inline=` return true
 * - `abc?a=1&__inline&` return true
 * - `abc?a=1&__inline` return true
 * @param {Object} info
 * @memberOf fis.compile
 */
function isInline(info) {
  return /[?&]__inline(?:[=&'"]|$)/.test(info.query);
}

/**
 * 分析注释中依赖用法。
 * @param {String} comment 注释内容
 * @param {Callback} [callback] 可以通过此参数来替换原有替换回调函数。
 * @memberOf fis.compile
 */
function analyseComment(comment, callback) {
  var reg = /(@(require|async|require\.async)\s+)('[^']+'|"[^"]+"|[^\s;!@#%^&*()]+)/g;
  callback = callback || function(m, prefix, type, value) {
    type = type === 'require' ? type : 'async';

    return prefix + map[type].wrap(value);
  };

  return comment.replace(reg, callback).replace(/(?:@|#)\s+sourceMappingURL=([^\s]+)/g, function(_, value) {
    return '# sourceMappingURL=' + map.sourceMap.wrap(value);
  });
}

/**
 * 标准化处理 javascript 内容, 识别 __inline、__uri 和 __require 的用法,并将其转换成中间码。
 *
 * - [@require id] in comment to require resource
 * - __inline(path) to embedd resource content or base64 encodings
 * - __uri(path) to locate resource
 * - require(path) to require resource
 *
 * @param {String} content js 内容
 * @param {Callback} callback 正则替换回调函数,如果不想替换,请传入 null.
 * @param {File} file js 内容所在文件。
 * @memberOf fis.compile
 */
function extJs(content, callback, file) {
  var reg = /"(?:[^\\"\r\n\f]|\\[\s\S])*"|'(?:[^\\'\n\r\f]|\\[\s\S])*'|(\/\/[^\r\n\f]+|\/\*[\s\S]*?(?:\*\/|$))|\b(__inline|__uri|__require|__id|__moduleId|__hash)\s*\(\s*("(?:[^\\"\r\n\f]|\\[\s\S])*"|'(?:[^\\'\n\r\f]|\\[\s\S])*')\s*\)/g;
  callback = callback || function(m, comment, type, value) {
    if (type) {
      switch (type) {
        case '__inline':
          m = map.jsEmbed.wrap(value);
          break;
        case '__uri':
          m = map.uri.wrap(value);
          break;
        case '__id':
          m = map.id.wrap(value);
          break;
        case '__moduleId':
          m = map.moduleId.wrap(value);
          break;
        case '__require':
          m = 'require(' + map.jsRequire.wrap(value) + ')';
          break;
        case '__hash':
          m = map.hash.wrap(value);
          break;
      }
    } else if (comment) {
      m = analyseComment(comment);
    }
    return m;
  };
  content = content.replace(reg, callback);
  var info = {
    file: file,
    content: content
  };

  fis.emit('standard:js', info);
  return info.content;
}

/**
 * 标准化处理 css 内容, 识别各种外链用法,并将其转换成中间码。
 *
 * - [@require id] in comment to require resource
 * - [@import url(path?__inline)] to embed resource content
 * - url(path) to locate resource
 * - url(path?__inline) to embed resource content or base64 encodings
 * - src=path to locate resource
 *
 * @param {String} content css 内容。
 * @param {Callback} callback 正则替换回调函数,如果不想替换,请传入 null.
 * @param {File} file js 内容所在文件。
 * @memberOf fis.compile
 */
function extCss(content, callback, file) {
  var reg = /(\/\*[\s\S]*?(?:\*\/|$))|(?:@import\s+)?\burl\s*\(\s*("(?:[^\\"\r\n\f]|\\[\s\S])*"|'(?:[^\\'\n\r\f]|\\[\s\S])*'|[^)}\s]+)\s*\)(\s*;?)|\bsrc\s*=\s*("(?:[^\\"\r\n\f]|\\[\s\S])*"|'(?:[^\\'\n\r\f]|\\[\s\S])*'|[^\s}]+)/g;
  callback = callback || function(m, comment, url, last, filter) {
    if (url) {
      var key = isInline(fis.util.query(url)) ? 'embed' : 'uri';
      if (m.indexOf('@') === 0) {
        if (key === 'embed') {
          m = map.embed.wrap(url) + last.replace(/;$/, '');
        } else {
          m = '@import url(' + map.uri.wrap(url) + ')' + last;
        }
      } else {
        m = 'url(' + map[key].wrap(url) + ')' + last;
      }
    } else if (filter) {
      m = 'src=' + map.uri.wrap(filter);
    } else if (comment) {
      m = analyseComment(comment);
    }
    return m;
  };
  content = content.replace(reg, callback);

  var info = {
    file: file,
    content: content
  };

  fis.emit('standard:css', info);

  return info.content;
}

/**
 * 标准化处理 html 内容, 识别各种语法,并将其转换成中间码。
 *
 * - `<!--inline[path]-->` to embed resource content
 * - `<img|embed|audio|video|link|object ... (data-)?src="path"/>` to locate resource
 * - `<img|embed|audio|video|link|object ... (data-)?src="path?__inline"/>` to embed resource content
 * - `<script|style ... src="path"></script|style>` to locate js|css resource
 * - `<script|style ... src="path?__inline"></script|style>` to embed js|css resource
 * - `<script|style ...>...</script|style>` to analyse as js|css
 *
 * @param {String} content html 内容。
 * @param {Callback} callback 正则替换回调函数,如果不想替换,请传入 null.
 * @param {File} file js 内容所在文件。
 * @memberOf fis.compile
 */
function extHtml(content, callback, file) {
  var reg = /(<script(?:(?=\s)[\s\S]*?["'\s\w\/\-]>|>))([\s\S]*?)(?=<\/script\s*>|$)|(<style(?:(?=\s)[\s\S]*?["'\s\w\/\-]>|>))([\s\S]*?)(?=<\/style\s*>|$)|<(img|embed|audio|video|link|object|source)\s+[\s\S]*?["'\s\w\/\-](?:>|$)|<!--inline\[([^\]]+)\]-->|(<!(?:--)?\[[^>]+>)|<!--(?!\[|>)([\s\S]*?)(-->|$)|\bstyle\s*=\s*("(?:[^\\"\r\n\f]|\\[\s\S])+"|'(?:[^\\'\n\r\f]|\\[\s\S])+')/ig;
  callback = callback || function(m, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10) {
    if ($1) { //<script>
      var embed = '';
      $1 = $1.replace(/(\s(?:data-)?src\s*=\s*)('[^']+'|"[^"]+"|[^\s\/>]+)/ig, function(m, prefix, value) {
        if (isInline(fis.util.query(value))) {
          embed += map.embed.wrap(value);
          return '';
        } else {
          return prefix + map.uri.wrap(value);
        }
      });
      if (embed) {
        //embed file
        m = $1 + embed;
      } else {
        m = xLang($1, $2, file, rType.test($1) ? (RegExp.$3 === 'javascript' ? 'js' : 'html') : 'js');
      }
    } else if ($3) { //<style>
      m = xLang($3, $4, file, 'css');
    } else if ($5) { //<img|embed|audio|video|link|object|source>
      var tag = $5.toLowerCase();
      if (tag === 'link') {
        var inline = '',
          isCssLink = false,
          isImportLink = false;
        var result = m.match(/\srel\s*=\s*('[^']+'|"[^"]+"|[^\s\/>]+)/i);
        if (result && result[1]) {
          var rel = result[1].replace(/^['"]|['"]$/g, '').toLowerCase();
          isCssLink = rel === 'stylesheet';
          isImportLink = rel === 'import';
        }
        m = m.replace(/(\s(?:data-)?href\s*=\s*)('[^']+'|"[^"]+"|[^\s\/>]+)/ig, function(_, prefix, value) {
          if ((isCssLink || isImportLink) && isInline(fis.util.query(value))) {
            if (isCssLink) {
              inline += '<style' + m.substring(5).replace(/\/(?=>$)/, '').replace(/\s+(?:charset|href|data-href|hreflang|rel|rev|sizes|target)\s*=\s*(?:'[^']+'|"[^"]+"|[^\s\/>]+)/ig, '');
            }

            inline += map.embed.wrap(value, m.replace(/^<link\b|\/?>$|\b(?:rel|href)='[^']*'|\b(?:rel|href)="[^"]*"/g, '').trim());
            if (isCssLink) {
              inline += '</style>';
            }
            return '';
          } else {
            return prefix + map.uri.wrap(value);
          }
        });
        m = inline || m;
      } else if (tag === 'object') {
        m = m.replace(/(\sdata\s*=\s*)('[^']+'|"[^"]+"|[^\s\/>]+)/ig, function(m, prefix, value) {
          return prefix + map.uri.wrap(value);
        });
      } else {
        m = m.replace(/(\s(?:(?:data-)?src(?:set)?|poster)\s*=\s*)('[^']+'|"[^"]+"|[^\s\/>]+)/ig, function(m, prefix, value) {
          var key = isInline(fis.util.query(value)) ? 'embed' : 'uri';
          if (prefix.indexOf('srcset') != -1) {
            //support srcset
            var info = fis.util.stringQuote(value);
            var srcset = [];
            info.rest.split(',').forEach(function(item) {
              var p;
              item = item.trim();
              if ((p = item.indexOf(' ')) == -1) {
                srcset.push(item);
                return;
              }
              srcset.push(map['uri'].wrap(item.substr(0, p)) + item.substr(p));
            });
            return prefix + info.quote + srcset.join(', ') + info.quote;
          }
          return prefix + map[key].wrap(value);
        });
      }
    } else if ($6) {
      m = map.embed.wrap($6);
    } else if ($8) {
      m = '<!--' + analyseComment($8) + $9;
    } else if ($10) {
      var quote = $10[0];
      m = 'style=' + quote + map.inlineStyle.wrap($10.substring(1, $10.length - 1)) + quote;
    }
    return m;
  };
  content = content.replace(reg, callback);

  var info = {
    file: file,
    content: content
  };

  fis.emit('standard:html', info);

  return info.content;
}

/**
 * 处理type类型为 `x-**` 的block标签。
 *
 * ```css
 * <head>
 *   <style type="x-scss">
 *    &commat;import "compass/css3";
 *
 *    #border-radius {
 *      &commat;include border-radius(25px);
 *    }
 *   </style>
 * </head>
 * ```
 * @param  {String} tag        标签
 * @param  {String} content    the content of file
 * @param  {File} file         fis.file instance
 * @param  {String} defaultExt what is ?
 * @return {String}
 * @function
 * @memberOf fis.compile
 */
function xLang(tag, content, file, defaultExt) {
  var ext = defaultExt;

  if (file.pipeEmbed === false) {
    switch (ext) {
      case 'html':
        content = extHtml(content, null, file);
        break;

      case 'js':
        content = extJs(content, null, file);
        break;

      case 'css':
        content = extCss(content, null, file);
        break;
    }

    return tag + content;
  } else {
    var isXLang = false;

    var m = rType.exec(tag);
    if (m) {
      var lang = m[3].toLowerCase();

      switch (lang) {
        case 'javascript':
          ext = 'js';
          break;

        case 'css':
          ext = 'css';
          break;

        default:
          if (lang.substring(0, 2) === 'x-') {
            ext = lang.substring(2);
            isXLang = true;
          }
          break;
      }
    }

    if (isXLang) {
      var mime = _.getMimeType(ext);
      mime && (mime !== 'application/x-' + ext) && (tag = tag.replace(rType, function(all, quote) {
        return ' type=' + quote + mime + quote;
      }));
    }
  }

  return tag + map.xlang.wrap(content, ext);
}

/**
 * 单文件编译入口,与 {@link fis.compile} 不同的是,此方法内部不进行缓存判断。
 * @param  {File} file 文件对象
 * @memberOf fis.compile
 * @name process
 * @function
 */
function process(file, context) {
  fis.emit('process:start', file);
  pipe(file, 'parser');
  pipe(file, 'preprocessor');
  pipe(file, 'standard');
  postStandard(file, context);
  pipe(file, 'postprocessor');
  pipe(file, 'optimizer');
  fis.emit('process:end', file);
}

/**
 * 让文件像管道一样经过某个流程处理。注意,跟 stream 的 pipe 不同,此方法不支持异步,而是同步的处理。
 * @memberOf fis.compile
 * @inner
 * @name pipe
 * @function
 * @param {File} file 文件对象
 * @param {String} type 类型
 * @param {Boolean} [keep] 是否保留文件内容。
 */
function pipe(file, type, keep) {
  var processors = [];
  var prop = file[type];

  if (type === 'standard' && 'undefined' === typeof prop) {
    processors.push('builtin');
  }

  if (prop) {
    var typeOf = typeof prop;
    if (typeOf === 'string') {
      prop = prop.trim().split(/\s*,\s*/);
    } else if (!Array.isArray(prop)) {
      prop = [prop];
    }

    processors = processors.concat(prop);
  }

  fis.emit('compile:' + type, file);

  if (processors.length) {

    // 过滤掉同名的插件, 没必要重复操作。
    processors = processors.filter(function(item, idx, arr) {
      item = item.__name || item;

      return idx === _.findIndex(arr, function(target) {
        target = target.__name || target;

        return target === item;
      });
    });

    var callback = function(processor, settings, key) {
      settings.filename = file.realpath;
      var content = file.getContent();
      try {
        fis.log.debug('pipe [' + key + '] start');
        var result = processor(content, file, settings);
        fis.log.debug('pipe [' + key + '] end');
        if (keep) {
          file.setContent(content);
        } else if (typeof result === 'undefined') {
          fis.log.warning('invalid content return of pipe [' + key + ']');
        } else {
          file.setContent(result);
        }
      } catch (e) {
        if (typeof e === 'string') {
          e = new Error(e);
        }

        //log error
        fis.log.debug('pipe [' + key + '] fail');
        var msg = key + ': ' + String(e.message || e.msg || e).trim() + ' [' + (e.filename || file.realpath);
        if (e.hasOwnProperty('line')) {
          msg += ':' + e.line;
          if (e.hasOwnProperty('col')) {
            msg += ':' + e.col;
          } else if (e.hasOwnProperty('column')) {
            msg += ':' + e.column;
          }
        }
        msg += ']';
        e.message = msg;
        error(e);
      }
    };

    processors.forEach(function(processor, index) {
      var typeOf = typeof processor,
        key, options;

      if (typeOf === 'object' && processor.__name) {
        options = processor;
        processor = processor.__name;
        typeOf = typeof processor;
      }

      if (typeOf === 'string') {
        key = type + '.' + processor;
        processor = (type === 'standard' && processor === 'builtin') ? builtinStandard : fis.require(type, processor);
      } else {
        key = type + '.' + index;
      }

      if (typeof processor === 'function') {
        var settings = {};
        _.assign(settings, processor.defaultOptions || processor.options || {});
        _.assign(settings, fis.media().get('settings.' + key, {}));
        _.assign(settings, options || {});

        // 删除隐藏配置
        delete settings.__name;
        delete settings.__plugin;
        delete settings.__pos;
        delete settings.__isPlugin;

        callback(processor, settings, key);
      } else {
        fis.log.warning('invalid processor [modules.' + key + ']');
      }
    });
  }
}

var lockedMap = {};

/*
 * error收集&输出
 * @param  {String} msg 输出的
 */
function error(msg) {
  //for watching, unable to exit
  lockedMap = {};
  fis.log.error(msg);
}

/*
 * 检查依赖是否存在闭环
 * @param  {String} from
 * @param  {String} to
 * @return {Boolean}
 */
function lockedCheck(from, to) {
  from = fis.file.wrap(from).realpath;
  to = fis.file.wrap(to).realpath;
  if (from === to) {
    return true;
  } else if (lockedMap[to]) {
    var prev = from;
    var msg = [];

    do {
      msg.unshift(prev);
      prev = lockedMap[prev];
    } while (prev);

    prev && msg.unshift(prev);
    msg.push(to);
    return msg;
  }
  return false;
}

/*
 * 设置 lockedMap 的值,用来 check.
 */
function lock(from, to) {
  from = fis.file.wrap(from).realpath;
  to = fis.file.wrap(to).realpath;

  lockedMap[to] = from;
}

/*
 * 删除的对应的 lockedMap 的值
 * @param  {Object} file
 */
function unlock(to) {
  to = fis.file.wrap(to).realpath;
  delete lockedMap[to];
}

/*
 * 添加依赖
 * @param {Object} a
 * @param {Object} b
 */
function addDeps(a, b) {
  if (a && a.cache && b) {
    if (b.cache) {
      a.cache.mergeDeps(b.cache);
    }
    a.cache.addDeps(b.realpath || b);
  }
}

function addMissingDeps(file, value) {
  if (file && file.cache && value) {
    if (value[0] === '"' || value[0] === "'") {
      value = value.substring(1, value.length - 1);
    }

    var filepath = _.isAbsolute(value) ? value : path.join(file.dirname, value), value;

    file.cache.addMissingDeps(filepath, value);
  }
}

/**
 * 内置的标准化处理函数,外部可以覆写此过程。
 *
 * - 对 html 文件进行 {@link fis.compile.extHtml} 处理。
 * - 对 js 文件进行 {@link fis.compile.extjs} 处理。
 * - 对 css 文件进行 {@link fis.compile.extCss} 处理。
 *
 * @param  {String} content 文件内容
 * @param  {File} file    文件对象
 * @param  {Object} conf    标准化配置项
 * @memberOf fis.compile
 * @inner
 */
function builtinStandard(content, file, conf) {
  if (typeof content === 'string') {
    fis.log.debug('builtin standard for [%s] start', file.realpath);
    var type;
    if (conf.type && conf.type !== 'auto') {
      type = conf.type;
    } else {
      type = file.isHtmlLike ? 'html' : (file.isJsLike ? 'js' : (file.isCssLike ? 'css' : ''));
    }

    switch (type) {
      case 'html':
        content = extHtml(content, null, file);
        break;

      case 'js':
        content = extJs(content, null, file);
        break;

      case 'css':
        content = extCss(content, null, file);
        break;

      default:
        // unrecognized.
        break;
    }
    fis.log.debug('builtin standard for [%s] end', file.realpath);
  }
  return content;
}

/**
 * 将中间码还原成源码。
 *
 * 中间码说明:(待补充)
 *
 * @inner
 * @memberOf fis.compile
 * @param  {file} file 文件对象
 */
function postStandard(file, context) {
  fis.emit('standard:restore:start', file);
  var content = file.getContent();

  if (typeof content !== 'string') {
    return;
  }

  fis.log.debug('postStandard start');

  var reg = map.reg;
  // 因为处理过程中可能新生成中间码,所以要拉个判断。
  while (reg.test(content)) {
    reg.lastIndex = 0; // 重置 regexp
    content = content.replace(reg, function(all, type, depth, value, extra) {
      var ret = '',
        info, id;
      try {
        switch (type) {
          case 'id':
            info = fis.project.lookup(value, file);
            ret = info.quote + info.id + info.quote;

            if (info.file && info.file.isFile()) {
              file.addLink(info.file.subpath);
            }
            break;
          case 'moduleId':
            info = fis.project.lookup(value, file);
            ret = info.quote + info.moduleId + info.quote;

            if (info.file && info.file.isFile()) {
              file.addLink(info.file.subpath);
            }
            break;
          case 'hash':
            info = fis.project.lookup(value, file);
            if (info.file && info.file.isFile()) {

              file.addLink(info.file.subpath);

              var locked = lockedCheck(file, info.file);
              if (!locked) {
                lock(file, info.file);
                exports(info.file, context);
                unlock(info.file);
                addDeps(file, info.file);
              }

              var md5 = info.file.getHash();
              ret = info.quote + md5 + info.quote;
            } else {
              ret = value;
              addMissingDeps(file, value);
            }
            break;
          case 'require':
          case 'jsRequire':
            info = fis.project.lookup(value, file);
            file.addRequire(info.id);

            if (type === 'jsRequire' && info.moduleId) {
              ret = info.quote + info.moduleId + info.quote;
            } else {
              ret = info.quote + info.id + info.quote;
            }

            if (info.file && info.file.isFile()) {
              file.addLink(info.file.subpath);
            }
            break;

          case 'async':
          case 'jsAsync':
            info = fis.project.lookup(value, file);
            file.addAsyncRequire(info.id);

            if (type === 'jsAsync' && info.moduleId) {
              ret = info.quote + info.moduleId + info.quote;
            } else {
              ret = info.quote + info.id + info.quote;
            }

            if (info.file && info.file.isFile()) {
              file.addLink(info.file.subpath);
            }
            break;
          case 'uri':
            info = fis.project.lookup(value, file);
            if (info.file && info.file.isFile()) {

              file.addLink(info.file.subpath);

              if (info.file.useHash) {
                var locked = lockedCheck(file, info.file);
                if (!locked) {
                  lock(file, info.file);
                  exports(info.file, context);
                  unlock(info.file);
                  addDeps(file, info.file);
                }
              }
              var query = (info.file.query && info.query) ? '&' + info.query.substring(1) : info.query;
              var url = info.file.getUrl();
              var hash = info.hash || info.file.hash;
              ret = info.quote + url + query + hash + info.quote;
            } else {
              ret = value;
              addMissingDeps(file, value);
            }
            break;
          case 'dep':
            if (file.cache) {
              info = fis.project.lookup(value, file);
              addDeps(file, info.file);
            } else {
              fis.log.warning('unable to add deps to file [' + path + ']');
              addMissingDeps(file, value);
            }
            break;
          case 'embed':
          case 'jsEmbed':
            info = fis.project.lookup(value, file);
            var f;
            if (info.file) {
              f = info.file;
            } else if (fis.util.isAbsolute(info.rest)) {
              f = fis.file(info.rest);
            }
            if (f && f.isFile()) {
              file.addLink(f.subpath);
              var locked = lockedCheck(file, info.file);
              if (!locked) {
                lock(file, f);
                f.isInline = true;
                exports(f, context);
                unlock(f);
                addDeps(file, f);
                copyInfo(f, file);

                if (f.isText()) {
                  ret = f.getContent();
                  if (type === 'jsEmbed' && !f.isJsLike && !f.isJsonLike) {
                    ret = JSON.stringify(ret);
                  }

                  extra && (ret = filterEmbed(ret, extra));
                } else {
                  ret = info.quote + f.getBase64() + info.quote;
                }
              } else {
                var msg = 'unable to embed file[' + file.realpath + '] into itself.';

                if (locked.splice) {
                  msg = 'circular embed `' + locked.join('` -> `') + '`.';
                }

                error(msg);
              }
            } else {
              fis.log.error('unable to embed non-existent file %s', value);
            }
            break;
          case 'xlang':
            ret = partial(value, file, extra);
            break;

          case 'inlineStyle':
            ret = partial('inline-style-placeholder {' + value + '}', file, {
              ext: 'css',
              xLang: ':inline-style'
            });
            ret = ret.replace(/inline-style-placeholder\s?\{([\s\S]*)\}/, '$1');
            break;

          case 'sourceMap':
            info = fis.project.lookup(value, file, true);
            if (info.file && info.file.isFile()) {

              file.addLink(info.file.subpath);

              if (info.file.useHash) {
                var locked = lockedCheck(file, info.file);
                if (!locked) {
                  lock(file, info.file);
                  exports(info.file, context);
                  unlock(info.file);
                  addDeps(file, info.file);
                }
              }
              var query = (info.file.query && info.query) ? '&' + info.query.substring(1) : info.query;
              var url = info.file.getUrl();
              var hash = info.hash || info.file.hash;
              ret = info.quote + url + query + hash + info.quote;

              file.extras = file.extras || {};
              file.extras.derived = file.extras.derived || [];
              file.extras.derived.push(info.file);
            } else {
              ret = value;
            }
            break;

            // 用来存信息的,内容会被移除。
          case 'info':
            ret = '';
            break;
          default:
            if (!map[type]) {
              fis.log.error('unsupported fis language tag [%s]', type);
            }
        }

        // trigger event.
        var message = {
          ret: ret,
          value: value,
          file: file,
          info: info,
          type: type
        };
        fis.emit('standard:restore', message);
        fis.emit('standard:restore:' + type, message);
        ret = message.ret;
      } catch (e) {
        lockedMap = {};
        e.message = e.message + ' in [' + file.subpath + ']';
        throw e;
      }
      return ret;
    });
  }
  file.setContent(content);
  fis.emit('standard:restore:end', file);
  fis.log.debug('postStandard end');
}

/**
 * 编译代码片段。用于在 html 中内嵌其他异构语言。
 * @param {String} content 代码片段。
 * @param {File} host 代码片段所在的文件,用于片段中对其他资源的查找。
 * @param {Object} info 文件信息。
 * @memberOf fis.compile
 * @example
 * var file = fis.file(root, 'static/_nav.tmpl');
 * var content = file.getContent();
 *
 * // tmpl 文件本身是 html 文件,但是会被解析成 js 文件供 js 使用。正常情况下他只有 js 语言能力。但是:
 * content = fis.compile.partial(content, file, {
 *    ext: 'html' // set isHtmlLike
 * });
 *
 * file.setConent(content);
 *
 * // 继续走之后的 js parser 流程。
 */
function partial(content, host, info) {

  // 默认 html 内嵌的 js, css 内容,都会独立走一遍单文件编译流程。
  // 可以通过 pipeEmbed 属性设置成 `false` 来关闭。
  if (host.pipeEmbed === false) {
    return content;
  }

  if (content.trim()) {
    info = typeof info === 'string' ? {
      ext: info
    } : (info || {});
    var ext = info.ext || host.ext;
    ext[0] === '.' && (ext = ext.substring(1));
    info.ext = '.' + ext;
    info.xLang = info.xLang || (':' + ext);

    var f = fis.file(host.realpath, info);
    f.cache = host.cache;
    f.isPartial = true;
    f.isInline = true;
    f.setContent(content);
    process(f);
    copyInfo(f, host);
    content = f.getContent();
  }

  return content;
}

// todo 支持外部扩展,支持更多的语法。
function filterEmbed(ret, attrs) {
  var params = {};
  attrs.split(/\s+/).forEach(function(param) {
    var parts = param.split('=');
    params[parts[0]] = parts[1].substring(1, parts[1].length - 1);
  });

  return ret.replace(/<!-- @@((?:\n?.)*?)-->/g, function(_, operator) {
    var parts = operator.split(/\s+/);

    if (parts[0] === 'var') {
      return params[parts[1]] || '';
    }

    return _;
  });
}

function copyInfo(src, dst) {
  var addFn = {
    'requires': 'addRequire',
    'asyncs': 'addAsyncRequire',
    'links': 'addLink'
  };

  Object.keys(addFn).forEach(function(key) {
    src[key].forEach(function(item) {
      dst[addFn[key]](item);
    });
  });
}

function resourceMapFile(file) {
  var content = file.getContent();
  var reg = /\b__RESOURCE_MAP__\b/g;
  // 如果写文档时包含了此字符,不应该替换,所以强制判断一下
  if (content.match(reg) && typeof file._isResourceMap == 'undefined') {
    file._isResourceMap = true; // special file
    file.useCache = false; // disable cache
    return true;
  }
  return false;
}

/*
 * adjust file
 * @param file
 */
function adjust(file) {
  if (file.isText()) {
    resourceMapFile(file);
  }
}

exports.process = process;
exports.extJs = extJs;
exports.extCss = extCss;
exports.extHtml = extHtml;
exports.xLang = xLang;
exports.partial = partial;
exports.isInline = isInline;
exports.analyseComment = analyseComment;