Source: hikaru.js

/**
 * @module hikaru
 */

import * as path from "node:path";

import fse from "fs-extra";
import YAML from "yaml";
import nunjucks from "nunjucks";
import {marked} from "marked";

import Logger from "./logger.js";
import Watcher from "./watcher.js";
import Renderer from "./renderer.js";
import Compiler from "./compiler.js";
import Processor from "./processor.js";
import Generator from "./generator.js";
import Decorator from "./decorator.js";
import Helper from "./helper.js";
import Translator from "./translator.js";
import Router from "./router.js";
import * as types from "./types.js";
import * as utils from "./utils.js";
const {Site, File} = types;
const {
  hikaruDir,
  loadJSON,
  loadYAML,
  loadYAMLSync,
  isNumber,
  isString,
  isArray,
  isFunction,
  isObject,
  checkType,
  isReadableSync,
  fallbackSort,
  matchFiles,
  getVersion,
  getPathFn,
  getURLFn,
  isCurrentHostFn,
  isCurrentPathFn,
  localeCompareFn,
  formatDateTimeFn,
  paginate,
  paginateCategoriesPosts,
  genCategories,
  genTags,
  parseNode,
  serializeNode,
  resolveHeadingIDs,
  resolveAnchors,
  resolveImages,
  resolveCodeBlocks,
  genTOC,
  NjkLoader
} = utils;

/**
 * @description Hikaru main class.
 */
class Hikaru {
  /**
   * @param {Object} [opts]
   * @param {Boolean} [opts.debug=false] Enable debug output for logger.
   * @param {Boolean} [opts.color=true] Enable colored output for logger.
   * @param {Boolean} [opts.draft] Build drafts.
   * @param {String} [opts.siteConfig] Alternative site config path.
   * @param {String} [opts.themeConfig] Alternative theme config path.
   * @param {String} [opts.ip=localhost] Alternative listening IP address
   * for router.
   * @param {Number} [opts.port=2333] Alternative listening port for router.
   * @property {Logger} logger
   * @property {Watcher} watcher
   * @property {Router} router
   * @property {Renderer} renderer
   * @property {Processor} processor
   * @property {Generator} generator
   * @property {Decorator} decorator
   * @property {Translator} translator
   * @property {Object} types
   * @property {Object} utils
   * @property {Object} opts
   * @property {Site} site
   * @return {Hikaru}
   */
  constructor(opts = {}) {
    this.opts = opts;
    this.logger = new Logger(this.opts);
    this.logger.debug("Hikaru is starting...");
    this.watcher = null;
    this.types = types;
    this.utils = utils;
    // Catch all unhandled error in promises.
    process.on("unhandledRejection", (error) => {
      this.logger.warn("Hikaru catched some error during running!");
      this.logger.error(error);
    });
    const exit = () => {
      if (this.watcher != null) {
        this.watcher.close();
      }
      if (this.router != null) {
        this.router.close();
      }
      process.exit(0);
    };
    process.on("SIGINT", exit);
    process.on("SIGTERM", exit);
    process.on("exit", () => {
      this.logger.debug("Hikaru is stopping...");
    });
    if (this.opts["siteConfig"] == null && this.opts["config"] != null) {
      this.opts["siteConfig"] = this.opts["config"];
      this.logger.warn(`Hikaru suggests you to use \`${
        this.logger.cyan("--site-config")
      }\` instead of \`${
        this.logger.cyan("--config")
      }\` because it's deprecated!`);
    }
    if (!isObject(Intl)) {
      this.logger.warn("Hikaru found you are using Node.js built without ICU!");
    }
  }

  /**
   * @description Create a Hikaru site dir with needed files.
   * @param {String} siteDir Working site dir.
   */
  init(siteDir) {
    const siteConfigPath = this.opts["siteConfig"] || path.join(
      siteDir, "site-config.yaml"
    );
    return fse.mkdirp(siteDir).then(() => {
      this.logger.debug(`Hikaru is copying \`${
        this.logger.cyan(siteConfigPath)
      }\`...`);
      this.logger.debug(`Hikaru is creating \`${
        this.logger.cyan(path.join(siteDir, "srcs", path.sep))
      }\`...`);
      this.logger.debug(`Hikaru is creating \`${
        this.logger.cyan(path.join(siteDir, "docs", path.sep))
      }\`...`);
      this.logger.debug(`Hikaru is creating \`${
        this.logger.cyan(path.join(siteDir, "themes", path.sep))
      }\`...`);
      this.logger.debug(`Hikaru is creating \`${
        this.logger.cyan(path.join(siteDir, "scripts", path.sep))
      }\`...`);
      fse.copy(
        path.join(hikaruDir, "dists", "site-config.yaml"),
        siteConfigPath
      );
      fse.mkdirp(path.join(siteDir, "srcs"));
      fse.mkdirp(path.join(siteDir, "docs"));
      fse.mkdirp(path.join(siteDir, "themes"));
      fse.mkdirp(path.join(siteDir, "scripts"));
    }).catch((error) => {
      this.logger.warn("Hikaru catched some error during initializing!");
      this.logger.error(error);
    });
  }

  /**
   * @description Clean a Hikaru site's built docs.
   * @param {String} siteDir Working site dir.
   */
  clean(siteDir) {
    const siteConfig = this.loadSiteConfig(siteDir, this.opts["siteConfig"]);
    if (siteConfig == null) {
      return;
    }
    this.logger.debug(`Hikaru is cleaning \`${
      this.logger.cyan(path.join(siteConfig["docDir"], path.sep))
    }\`...`);
    fse.emptyDir(siteConfig["docDir"]).catch((error) => {
      this.logger.warn("Hikaru catched some error during cleaning!");
      this.logger.error(error);
    });
  }

  /**
   * @description Build and write docs from srcs.
   * @param {String} siteDir Working site dir.
   */
  async build(siteDir) {
    this.loadSite(siteDir);
    try {
      // Modules must be loaded before others.
      this.loadModules();
      await Promise.all([this.loadPlugins(), this.loadScripts()]);
      await Promise.all([this.loadLanguages(), this.loadLayouts()]);
      this.router = new Router(
        this.logger,
        this.renderer,
        this.processor,
        this.generator,
        this.decorator,
        this.helper,
        this.translator,
        this.site
      );
      await this.router.build();
    } catch (error) {
      this.logger.warn("Hikaru catched some error during building!");
      this.logger.error(error);
      this.logger.warn("Hikaru suggests you to check built files!");
      process.exit(-2);
    }
  }

  /**
   * @description Build and serve docs with a HTTP server from srcs.
   * @param {String} siteDir Working site dir.
   */
  async serve(siteDir) {
    const ip = this.opts["ip"] || "localhost";
    const port = this.opts["port"] || 2333;
    this.loadSite(siteDir);
    const rawFileDependencies = await this.loadFileDependencies();
    this.watcher = new Watcher(this.logger, rawFileDependencies);
    // This watcher only watch one file so don't care about arguments.
    const reloadFileDependencies = async () => {
      const rawFileDependencies = await this.loadFileDependencies();
      this.watcher.updateFileDependencies(rawFileDependencies);
    };
    this.watcher.register(
      this.site["siteConfig"]["themeDir"],
      reloadFileDependencies,
      {"customGlob": "file-dependencies.yaml"}
    );
    try {
      // Modules must be loaded before others.
      this.loadModules();
      await Promise.all([this.loadPlugins(), this.loadScripts()]);
      await Promise.all([this.loadLanguages(), this.loadLayouts()]);
      this.router = new Router(
        this.logger,
        this.renderer,
        this.processor,
        this.generator,
        this.decorator,
        this.helper,
        this.translator,
        this.site,
        this.watcher
      );
      await this.router.serve(ip, port);
    } catch (error) {
      this.logger.warn("Hikaru catched some error during serving!");
      this.logger.error(error);
      process.exit(-2);
    }
  }

  /**
   * @private
   * @description Load site config.
   * @param {String} siteDir Working site dir.
   * @param {String} [siteConfigPath] Alternative site config path.
   * @return {Object}
   */
  loadSiteConfig(siteDir, siteConfigPath) {
    if (siteConfigPath == null) {
      let defaultSiteConfigPath = path.join(siteDir, "site-config.yaml");
      if (!isReadableSync(defaultSiteConfigPath)) {
        this.logger.warn(`Hikaru suggests you to rename \`${
          this.logger.cyan(path.join(siteDir, "siteConfig.yml"))
        }\` to \`${
          this.logger.cyan(path.join(siteDir, "site-config.yaml"))
        }\` because it's deprecated!`);
        defaultSiteConfigPath = path.join(siteDir, "siteConfig.yml");
      }
      siteConfigPath = defaultSiteConfigPath;
    }
    this.logger.debug(`Hikaru is loading site config in \`${
      this.logger.cyan(siteConfigPath)
    }\`...`);
    let siteConfig;
    try {
      // Only site config and theme config can be block because they are basic.
      siteConfig = loadYAMLSync(siteConfigPath);
    } catch (error) {
      this.logger.warn("Hikaru cannot load site config!");
      this.logger.error(error);
      process.exit(-1);
    }
    siteConfig["srcDir"] = path.join(
      siteDir, siteConfig["srcDir"] || "srcs"
    );
    this.logger.debug(`Hikaru is reading sources from \`${
      this.logger.cyan(path.join(siteConfig["srcDir"], path.sep))
    }\`...`);
    siteConfig["docDir"] = path.join(
      siteDir, siteConfig["docDir"] || "docs"
    );
    this.logger.debug(`Hikaru is writing documents to \`${
      this.logger.cyan(path.join(siteConfig["docDir"], path.sep))
    }\`...`);
    siteConfig["themeDir"] = path.join(
      siteDir, siteConfig["themeDir"]
    );
    this.logger.debug(`Hikaru is loading theme from \`${
      this.logger.cyan(path.join(siteConfig["themeDir"], path.sep))
    }\`...`);
    siteConfig["themeSrcDir"] = path.join(
      siteConfig["themeDir"], "srcs"
    );
    siteConfig["themeLangDir"] = path.join(
      siteConfig["themeDir"], "languages"
    );
    siteConfig["themeLayoutDir"] = path.join(
      siteConfig["themeDir"], "layouts"
    );
    return siteConfig;
  }

  /**
   * @private
   * @description Load theme config.
   * @param {String} siteDir Working site dir.
   * @param {String} [themeConfigPath] Alternative theme config path.
   * @return {Object}
   */
  loadThemeConfig(siteDir, themeConfigPath) {
    if (themeConfigPath == null) {
      let defaultThemeConfigPath = path.join(siteDir, "theme-config.yaml");
      if (!isReadableSync(defaultThemeConfigPath)) {
        this.logger.warn(`Hikaru suggests you to rename \`${
          this.logger.cyan(path.join(siteDir, "themeConfig.yml"))
        }\` to \`${
          this.logger.cyan(path.join(siteDir, "theme-config.yaml"))
        }\` because it's deprecated!`);
        defaultThemeConfigPath = path.join(siteDir, "themeConfig.yml");
      }
      themeConfigPath = defaultThemeConfigPath;
    }
    this.logger.debug(`Hikaru is loading theme config in \`${
      this.logger.cyan(themeConfigPath)
    }\`...`);
    let themeConfig;
    try {
      // Only site config and theme config can be block because they are basic.
      themeConfig = loadYAMLSync(themeConfigPath);
    } catch (error) {
      if (error["code"] === "ENOENT") {
        this.logger.warn("Hikaru continues with a empty theme config...");
        themeConfig = {};
      } else {
        this.logger.warn("Hikaru cannot load theme config!");
        this.logger.error(error);
        process.exit(-1);
      }
    }
    return themeConfig;
  }

  /**
   * @private
   * @description Read file dependency tree.
   * @return {Object}
   */
  async loadFileDependencies() {
    const filepath = path.join(
      this.site["siteConfig"]["themeDir"], "file-dependencies.yaml"
    );
    this.logger.debug(`Hikaru is loading file dependencies in \`${
      this.logger.cyan(filepath)
    }\`...`);
    let rawFileDependencies;
    try {
      rawFileDependencies = await loadYAML(filepath);
    } catch (error) {
      // Should work if theme author does not provide such a file.
      rawFileDependencies = {};
    }
    const fullRawFileDependencies = {};
    for (const dir in rawFileDependencies) {
      const srcDir = path.join(this.site["siteConfig"]["themeDir"], dir);
      fullRawFileDependencies[srcDir] = rawFileDependencies[dir];
    }
    return fullRawFileDependencies;
  }

  /**
   * @private
   * @description Load info about the site.
   * @param {String} siteDir Working site dir.
   */
  loadSite(siteDir) {
    this.site = new Site(siteDir);
    this.site["siteConfig"] = this.loadSiteConfig(
      siteDir,
      this.opts["siteConfig"]
    );
    this.site["themeConfig"] = this.loadThemeConfig(
      siteDir,
      this.opts["themeConfig"]
    );
  }

  /**
   * @private
   * @description Load Hikaru's internal module.
   */
  loadModules() {
    this.renderer = new Renderer(
      this.logger,
      this.site["siteConfig"]["skipRender"]
    );
    this.compiler = new Compiler(this.logger);
    this.processor = new Processor(this.logger);
    this.generator = new Generator(this.logger);
    this.decorator = new Decorator(this.logger, this.compiler);
    this.helper = new Helper(this.logger);
    this.translator = new Translator(this.logger);
    this.registerInternalRenderers();
    this.registerInternalCompilers();
    this.registerInternalProcessors();
    this.registerInternalGenerators();
    this.registerInternalHelpers();
  }

  /**
   * @private
   * @description Load local plugins for site,
   * which are installed into site's dir and starts with `hikaru-`.
   */
  async loadPlugins() {
    const sitePkgPath = path.join(this.site["siteDir"], "package.json");
    let sitePkgJSON;
    try {
      sitePkgJSON = await loadJSON(sitePkgPath);
    } catch (error) {
      return null;
    }
    if (sitePkgJSON["dependencies"] == null) {
      return null;
    }
    const plugins = Object.keys(sitePkgJSON["dependencies"]).filter((name) => {
      return /^hikaru-/.test(name);
    });
    return Promise.all(plugins.map(async (name) => {
      this.logger.debug(`Hikaru is loading plugin \`${
        this.logger.blue(name)
      }\`...`);
      const pluginDir = path.join(this.site["siteDir"], "node_modules", name);
      // Unlike `require()`, `import ()` does not check entries in `package.json`
      // if you pass a path, so we do this manually.
      let pluginPkgJSON;
      try {
        pluginPkgJSON = await loadJSON(path.join(pluginDir, "package.json"));
      } catch (error) {
        // Plugin should be a valid package.
        return null;
      }
      let pluginPath;
      if (pluginPkgJSON["exports"] != null &&
          isString(pluginPkgJSON["exports"])) {
        // Could be an Object, but we don't accept this as a plugin.
        pluginPath = path.resolve(pluginDir, pluginPkgJSON["exports"]);
      } else if (pluginPkgJSON["main"] != null) {
        // If exists, main is always a string.
        pluginPath = path.resolve(pluginDir, pluginPkgJSON["main"]);
      } else {
        pluginPath = path.resolve(pluginDir, "index.js");
      }
      // `import` is a keyword, not a function.
      const module = await import(pluginPath);
      return module["default"]({
        "logger": this.logger,
        "watcher": this.watcher,
        "renderer": this.renderer,
        "compiler": this.compiler,
        "processor": this.processor,
        "generator": this.generator,
        "decorator": this.decorator,
        "helper": this.helper,
        "translator": this.translator,
        "types": this.types,
        "utils": this.utils,
        "site": this.site,
        "opts": this.opts
      });
    }));
  }

  /**
   * @private
   * @description Load local scripts for site and theme,
   * which are js files installed into scripts dir.
   */
  async loadScripts() {
    // Globs must not contain windows spearators.
    const scripts = (await matchFiles("**/*.js", {
      "workDir": path.join(this.site["siteDir"], "scripts")
    })).map((filename) => {
      return path.join(this.site["siteDir"], "scripts", filename);
    }).concat((await matchFiles("**/*.js", {
      "workDir": path.join(this.site["siteConfig"]["themeDir"], "scripts")
    })).map((filename) => {
      return path.join(
        this.site["siteConfig"]["themeDir"], "scripts", filename
      );
    }));
    return Promise.all(scripts.map(async (filepath) => {
      this.logger.debug(`Hikaru is loading script \`${
        this.logger.cyan(filepath)
      }\`...`);
      // Use absolute path to load from siteDir instead of program dir.
      //
      // `import` is a keyword, not a function.
      const module = await import(path.resolve(filepath));
      return module["default"]({
        "logger": this.logger,
        "watcher": this.watcher,
        "renderer": this.renderer,
        "compiler": this.compiler,
        "processor": this.processor,
        "generator": this.generator,
        "decorator": this.decorator,
        "helper": this.helper,
        "translator": this.translator,
        "types": this.types,
        "utils": this.utils,
        "site": this.site,
        "opts": this.opts
      });
    }));
  }

  /**
   * @private
   */
  async loadLanguages() {
    let ext = ".yaml";
    let filenames = await matchFiles(`*${ext}`, {
      "workDir": this.site["siteConfig"]["themeLangDir"],
      "recursive": false
    });
    if (filenames.length === 0) {
      ext = ".yml";
      filenames = await matchFiles(`*${ext}`, {
        "workDir": this.site["siteConfig"]["themeLangDir"],
        "recursive": false
      });
      for (const filename of filenames) {
        this.logger.warn(`Hikaru suggests you to rename \`${
          this.logger.cyan(
            path.join(
              this.site["siteConfig"]["themeLangDir"],
              filename
            )
          )
        }\` to \`${
          this.logger.cyan(
            path.join(
              this.site["siteConfig"]["themeLangDir"],
              `${path.basename(filename, ext)}.yaml`
            )
          )
        }\` because it's deprecated!`);
      }
    }
    if (!filenames.includes(`default${ext}`)) {
      this.logger.warn(
        "Hikaru cannot find default language file in your theme!"
      );
    }
    const load = async (srcDir, srcPath) => {
      const lang = path.basename(srcPath, ext);
      this.logger.debug(`Hikaru is loading language \`${
        this.logger.blue(lang)
      }\`...`);
      const filepath = path.join(srcDir, srcPath);
      const content = await fse.readFile(filepath, "utf8");
      this.site["languages"].set(srcPath, content);
      const language = YAML.parse(content);
      this.translator.register(lang, language);
    };
    const all = Promise.all(filenames.map((filename) => {
      return load(this.site["siteConfig"]["themeLangDir"], filename);
    }));
    if (this.watcher != null) {
      this.watcher.register(
        this.site["siteConfig"]["themeLangDir"],
        (srcDir, srcPaths) => {
          const {added, changed, removed} = srcPaths;
          // Handle remove first because it is sync.
          for (const srcPath of removed) {
            this.site["languages"].delete(srcPath);
            const lang = path.basename(srcPath, ext);
            this.translator.unregister(lang);
          }
          Promise.all(added.concat(changed).map((srcPath) => {
            return load(srcDir, srcPath);
          }));
        },
        {"customGlob": `*${ext}`}
      );
    }
    return all;
  }

  /**
   * @private
   */
  async loadLayouts() {
    // Load and watch all template files so we could update related layouts if
    // included template files are changed.
    const filenames = await matchFiles("**/*", {
      "workDir": this.site["siteConfig"]["themeLayoutDir"]
    });
    const load = async (srcDir, srcPath) => {
      const filepath = path.join(srcDir, srcPath);
      const content = await fse.readFile(filepath, "utf8");
      this.site["layouts"].set(srcPath, content);
    };
    const compile = async (srcPath) => {
      const ext = path.extname(srcPath);
      const layout = path.basename(srcPath, ext);
      this.logger.debug(`Hikaru is loading layout \`${
        this.logger.blue(layout)
      }\`...`);
      const content = this.site["layouts"].get(srcPath);
      const fn = await this.compiler.compile(srcPath, content);
      this.decorator.register(layout, fn);
      // Keep compatible and don't break themes. The only possible problem is
      // using new Hikaru with an old theme (Hikaru generates `home` but the
      // theme only contains `index`). If using new theme with old Hikaru, it
      // should be easy for theme to keep compatible (for example, link `index`
      // to `home`), don't do too much here.
      if (layout === "index" && !this.decorator.list().includes("home")) {
        this.decorator.register("home", fn);
      }
    };
    const all = Promise.all(filenames.map((filename) => {
      return load(this.site["siteConfig"]["themeLayoutDir"], filename);
    })).then(() => {
      return Promise.all(filenames.filter((filename) => {
        // We only compile top level templates as decorate functions.
        return path.dirname(filename) === ".";
      }).map((filename) => {
        return compile(filename);
      }));
    });
    if (this.watcher != null) {
      this.watcher.register(
        this.site["siteConfig"]["themeLayoutDir"],
        async (srcDir, srcPaths) => {
          const {added, changed, removed} = srcPaths;
          // Handle remove first because it is sync.
          for (const srcPath of removed) {
            this.site["layouts"].delete(srcPath);
            // We only register top level template files as decorate functions.
            if (path.dirname(srcPath) !== ".") {
              continue;
            }
            const ext = path.extname(srcPath);
            const layout = path.basename(srcPath, ext);
            this.decorator.unregister(layout);
          }
          const updated = added.concat(changed);
          await Promise.all(updated.map((srcPath) => {
            return load(srcDir, srcPath);
          }));
          await Promise.all(updated.filter((srcPath) => {
            // We only register top level template files as decorate functions.
            return path.dirname(srcPath) === ".";
          }).map((srcPath) => {
            return compile(srcPath);
          }));
        }
      );
    }
    return all;
  }

  /**
   * @private
   */
  registerInternalRenderers() {
    this.renderer.register(".html", (file) => {
      file["content"] = file["text"];
      return file;
    });

    const markedOpts = Object.assign(
      {"gfm": true},
      this.site["siteConfig"]["marked"]
    );
    marked.setOptions(markedOpts);
    this.renderer.register(".md", ".html", (file) => {
      file["content"] = marked.parse(file["text"]);
      return file;
    });
  }

  /**
   * @private
   */
  registerInternalCompilers() {
    const njkOpts = Object.assign(
      {"autoescape": false}, this.site["siteConfig"]["nunjucks"]
    );
    // See the comment of `NjkLoader` in `utils.js` for why we need it.
    const njkEnv = new nunjucks.Environment(new NjkLoader(this), njkOpts);
    const njkCompiler = (srcPath, content) => {
      // If content is not provided, ask environment to load it via loader.
      // Otherwise create a new template and pass it to environment.
      const template = content == null
        ? njkEnv.getTemplate(srcPath, true)
        : new nunjucks.Template(content, njkEnv, srcPath, true);
      return (ctx) => {
        return new Promise((resolve, reject) => {
          template.render(ctx, (error, result) => {
            if (error != null) {
              return reject(error);
            }
            return resolve(result);
          });
        });
      };
    };
    this.compiler.register(".njk", njkCompiler);
    this.compiler.register(".j2", njkCompiler);
  }

  /**
   * @private
   */
  registerInternalProcessors() {
    // Shared by categories and tags sorting.
    const comparePostsLength = (a, b) => {
      return -(a["posts"].length - b["posts"].length);
    };
    const localeCompare = localeCompareFn(this.site["siteConfig"]["language"]);
    const compareName = (a, b) => {
      return localeCompare(a["name"], b["name"]);
    };

    if (!this.opts["draft"]) {
      this.processor.register("draft filter", (site) => {
        site["posts"] = site["posts"].filter((p) => {
          return !p["draft"];
        });
      });
    }

    // Always sort posts first, so categories and tags will have sorted posts.
    this.processor.register("posts sequence", (site) => {
      fallbackSort(
        site["posts"],
        (a, b) => {
          return -(a["created"] - b["created"]);
        },
        (a, b) => {
          return localeCompare(a["name"], b["name"]);
        }
      );
      for (let i = 1; i < site["posts"].length; ++i) {
        site["posts"][i]["next"] = site["posts"][i - 1];
      }
      for (let i = 0; i < site["posts"].length - 1; ++i) {
        site["posts"][i]["prev"] = site["posts"][i + 1];
      }
    });

    this.processor.register("categories collection", (site) => {
      const result = genCategories(site["posts"]);
      site["categories"] = result["categories"];
      site["categoriesLength"] = result["categoriesLength"];
      const sortCategories = (categories) => {
        fallbackSort(categories, comparePostsLength, compareName);
        for (const category of categories) {
          sortCategories(category["subs"]);
        }
      };
      sortCategories(site["categories"]);
    });

    this.processor.register("tags collection", (site) => {
      const result = genTags(site["posts"]);
      site["tags"] = result["tags"];
      site["tagsLength"] = result["tagsLength"];
      fallbackSort(site["tags"], comparePostsLength, compareName);
    });

    // Do contents resolving by default.
    if (this.site["siteConfig"]["contentsResolving"]["enable"]) {
      this.processor.register("contents resolving", async (site) => {
        const crOpts = site["siteConfig"]["contentsResolving"];
        const all = site["posts"].concat(site["pages"]);
        for (const p of all) {
          const node = parseNode(p["content"]);
          if (crOpts["headingIDs"]["enable"]) {
            resolveHeadingIDs(node, crOpts["headingIDs"]);
          }
          if (crOpts["toc"]["enable"]) {
            p["toc"] = genTOC(node, crOpts["toc"]);
          }
          if (crOpts["anchors"]["enable"]) {
            resolveAnchors(
              node,
              site["siteConfig"]["baseURL"],
              site["siteConfig"]["rootDir"],
              p["docPath"],
              crOpts["anchors"]
            );
          }
          if (crOpts["images"]["enable"]) {
            resolveImages(
              node,
              site["siteConfig"]["rootDir"],
              p["docPath"],
              crOpts["images"]
            );
          }
          if (crOpts["codeBlocks"]["enable"]) {
            resolveCodeBlocks(node, crOpts["codeBlocks"]);
          }
          p["content"] = serializeNode(node);
          if (p["content"].indexOf("<!--more-->") !== -1) {
            const split = p["content"].split("<!--more-->");
            p["excerpt"] = split[0];
            p["more"] = split[1];
            p["content"] = split.join("<a id=\"more\"></a>");
          }
        }
      });
    }
  }

  /**
   * @private
   */
  registerInternalGenerators() {
    // Keep compatible and don't break themes.
    if (this.site["siteConfig"]["homeDir"] != null ||
        this.site["siteConfig"]["indexDir"] != null) {
      this.generator.register("home pages", (site) => {
        let perPage;
        if (isObject(site["siteConfig"]["perPage"])) {
          perPage = site["siteConfig"]["perPage"]["home"] ||
            site["siteConfig"]["perPage"]["index"] || 10;
        } else {
          perPage = site["siteConfig"]["perPage"] || 10;
        }
        return paginate(new File({
          "layout": "home",
          "docDir": site["siteConfig"]["docDir"],
          "docPath": path.join(
            site["siteConfig"]["homeDir"] || site["siteConfig"]["indexDir"],
            "index.html"
          ),
          "title": "home",
          "comment": false,
          "reward": false
        }), site["posts"], perPage);
      });
    }

    if (this.site["siteConfig"]["archiveDir"] != null) {
      this.generator.register("archives pages", (site) => {
        let perPage;
        if (isObject(site["siteConfig"]["perPage"])) {
          perPage = site["siteConfig"]["perPage"]["archives"] || 10;
        } else {
          perPage = site["siteConfig"]["perPage"] || 10;
        }
        return paginate(new File({
          "layout": "archives",
          "docDir": site["siteConfig"]["docDir"],
          "docPath": path.join(site["siteConfig"]["archiveDir"], "index.html"),
          "title": "archives",
          "comment": false,
          "reward": false
        }), site["posts"], perPage);
      });
    }

    if (this.site["siteConfig"]["categoryDir"] != null) {
      this.generator.register("categories pages", (site) => {
        const results = [];
        let perPage;
        if (isObject(site["siteConfig"]["perPage"])) {
          perPage = site["siteConfig"]["perPage"]["category"] || 10;
        } else {
          perPage = site["siteConfig"]["perPage"] || 10;
        }
        results.push(...paginateCategoriesPosts(
          site["categories"],
          site["siteConfig"]["categoryDir"],
          site["siteConfig"]["docDir"],
          perPage
        ));
        results.push(new File({
          "layout": "categories",
          "docDir": site["siteConfig"]["docDir"],
          "docPath": path.join(site["siteConfig"]["categoryDir"], "index.html"),
          "title": "categories",
          "comment": false,
          "reward": false
        }));
        return results;
      });
    }

    if (this.site["siteConfig"]["tagDir"] != null) {
      this.generator.register("tags pages", (site) => {
        const results = [];
        let perPage;
        if (isObject(site["siteConfig"]["perPage"])) {
          perPage = site["siteConfig"]["perPage"]["tag"] || 10;
        } else {
          perPage = site["siteConfig"]["perPage"] || 10;
        }
        for (const tag of site["tags"]) {
          tag["docPath"] = path.join(
            site["siteConfig"]["tagDir"], tag["name"], "index.html"
          );
          const sp = new File({
            "layout": "tag",
            "docDir": site["siteConfig"]["docDir"],
            "docPath": tag["docPath"],
            "title": "tag",
            "tag": tag,
            "name": tag["name"],
            "comment": false,
            "reward": false
          });
          results.push(...paginate(sp, tag["posts"], perPage));
        }
        results.push(new File({
          "layout": "tags",
          "docDir": site["siteConfig"]["docDir"],
          "docPath": path.join(site["siteConfig"]["tagDir"], "index.html"),
          "title": "tags",
          "comment": false,
          "reward": false
        }));
        return results;
      });
    }
  }

  /**
   * @private
   */
  registerInternalHelpers() {
    const getPath = getPathFn(this.site["siteConfig"]["rootDir"]);
    const getURL = getURLFn(
      this.site["siteConfig"]["baseURL"], this.site["siteConfig"]["rootDir"]
    );
    const isCurrentHost = isCurrentHostFn(
      this.site["siteConfig"]["baseURL"], this.site["siteConfig"]["rootDir"]
    );
    const formatDateTime = formatDateTimeFn(
      this.site["siteConfig"]["language"]
    );
    this.helper.register("base context", (site, file) => {
      const lang = file["language"] || site["siteConfig"]["language"];
      const decorated = new Date();
      return {
        "site": site,
        "siteConfig": site["siteConfig"],
        "themeConfig": site["themeConfig"],
        "getVersion": getVersion,
        "getPath": getPath,
        "getURL": getURL,
        "isCurrentHost": isCurrentHost,
        "isCurrentPath": isCurrentPathFn(
          this.site["siteConfig"]["rootDir"], file["docPath"]
        ),
        "isNumber": isNumber,
        "isString": isString,
        "isArray": isArray,
        "isFunction": isFunction,
        "isObject": isObject,
        "checkType": checkType,
        // Damn it, we cannot use `new` in Nunjucks. But every time a decorator
        // starts, we will get context, so we can pass decorate date and time.
        "decorated": decorated,
        "decorateDate": decorated,
        "formatDateTime": formatDateTime,
        "__": this.translator.getTranslateFn(lang)
      };
    });
  }
}

export default Hikaru;