hexo/filter/locals.js

/**
 * Register the Hexo filter for merging theme and site config, and add all helper functions to a
 * dedicated property (<code>helper</code>) in the locals.
 * @module hexo/filter/locals
 */
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');

/**
 * Register the Hexo filter for merging theme and site config, and add all helper functions to a
 * dedicated property (<code>helper</code>) in the locals.
 * <p>
 * This filter will try to merge
 * <ul>
 * <li>the post/page config defined in the Markdown file's front-matter,</li>
 * <li>post/page config defined in <code>themes/[theme_name]/_config.post.yml</code> or
 * <code>themes/[theme_name]/_config.page.yml</code>,</li>
 * <li>post/page layout config defined in <code>[site_root]/_config.post.yml</code> or
 * <code>[site_root]/_config.page.yml</code>,</li>
 * <li>theme config defined in <code>themes/[theme_name]/_config.yml</code> or
 * <code>node_modules/hexo-theme-[theme_name]/_config.yml</code> if it is installed as an NPM package,
 * </li>
 * <li>and the site config defined in <code>_config.yml</code>,</li>
 * </ul>
 * in the above order (the former config will overwrite later ones).
 * This allow the user to customize the theme for each post/page, as well as for all post or pages.
 * <br>
 * It also adds a new variable named <code>helper</code>, containing all Hexo helper functions, to the page
 * locals so that you can them through it instead.
 * It is useful for passing helper functions to the cacheable view components.
 * <p>
 * <b>NOTE: This filter must be registered before any other Hexo extensions or views in order to take effect.</b>
 *
 * @param {Hexo} hexo The Hexo instance.
 */
module.exports = (hexo) => {
  const RESERVED_KEYS = {
    post: Object.keys(require('hexo/dist/models/post')(hexo).paths),
    page: Object.keys(require('hexo/dist/models/page')(hexo).paths),
  };

  function loadLayoutConfig(layout) {
    let config = {};
    const configInSiteDir = path.join(hexo.base_dir, '_config.' + layout + '.yml');
    const configInThemeDir = path.join(hexo.theme_dir, '_config.' + layout + '.yml');
    [configInSiteDir, configInThemeDir].forEach((configPath) => {
      if (fs.existsSync(configPath)) {
        config = Object.assign(config, yaml.load(fs.readFileSync(configPath)));
      }
    });
    return config;
  }

  const ALTERNATIVE_CONFIG = {
    post: loadLayoutConfig('post'),
    page: loadLayoutConfig('page'),
  };

  function stripConfig(source, reservedKeys) {
    const result = {};
    Object.keys(source)
      .filter(
        (key) =>
          !key.startsWith('_') && !reservedKeys.includes(key) && typeof source[key] !== 'function',
      )
      .forEach((key) => {
        result[key] = source[key];
      });
    return result;
  }

  hexo.extend.filter.register('template_locals', (locals) => {
    // inject helper functions
    locals.helper = {};
    const helpers = hexo.extend.helper.list();
    for (const name in helpers) {
      locals.helper[name] = helpers[name].bind(locals);
    }
    if (typeof locals.__ === 'function') {
      locals.helper.__ = locals.__;
    }
    if (typeof locals._p === 'function') {
      locals.helper._p = locals._p;
    }

    const page = locals.page;
    if (page) {
      locals.config = Object.assign({}, locals.config, locals.theme);
      if (page.layout in ALTERNATIVE_CONFIG) {
        // load alternative config if exists
        locals.config = Object.assign(locals.config, ALTERNATIVE_CONFIG[page.layout]);
      }
      // merge page configs
      if (page.__post === true) {
        Object.assign(locals.config, stripConfig(page, RESERVED_KEYS.post));
      } else if (page.__page === true) {
        Object.assign(locals.config, stripConfig(page, RESERVED_KEYS.page));
      }
    }

    return locals;
  });
};