UNPKG

hikarujs

Version:

A static site generator that generates routes based on directories naturally.

1,002 lines (969 loc) 32.7 kB
/** * @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.yellow("--site-config") }\` instead of \`${ this.logger.yellow("--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, { "filter": (p) => { return p === "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); })); }, { "filter": (p) => { return path.dirname(p) === "." && path.extname(p) === 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;