/**
* @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;