hikaru-coffee
Version:
A static site generator that generates routes based on directories naturally.
682 lines (645 loc) • 26.9 kB
JavaScript
// Generated by CoffeeScript 2.3.1
(function() {
var Category, File, Generator, Hikaru, Logger, Processer, Promise, Renderer, Router, Site, Tag, Translator, URL, cheerio, coffee, colors, escapeHTML, fse, getPathFn, getURLFn, glob, highlight, marked, moment, nib, nunjucks, paginate, paginateCategories, path, removeControlChars, sortCategories, stylus, yaml;
fse = require("fs-extra");
path = require("path");
({URL} = require("url"));
glob = require("glob");
cheerio = require("cheerio");
moment = require("moment");
colors = require("colors/safe");
Promise = require("bluebird");
yaml = require("js-yaml");
nunjucks = require("nunjucks");
marked = require("marked");
stylus = require("stylus");
nib = require("nib");
coffee = require("coffeescript");
highlight = require("./highlight");
Logger = require("./logger");
({Site, File, Category, Tag} = require("./type"));
Renderer = require("./renderer");
Processer = require("./processer");
Generator = require("./generator");
Translator = require("./translator");
Router = require("./router");
({escapeHTML, removeControlChars, paginate, sortCategories, paginateCategories, getPathFn, getURLFn} = require("./utils"));
Hikaru = class Hikaru {
constructor(debug = false) {
this.init = this.init.bind(this);
this.clean = this.clean.bind(this);
this.build = this.build.bind(this);
this.serve = this.serve.bind(this);
this.loadSite = this.loadSite.bind(this);
this.loadModules = this.loadModules.bind(this);
this.registerInternalRenderers = this.registerInternalRenderers.bind(this);
this.registerInternalProcessers = this.registerInternalProcessers.bind(this);
this.registerInternalGenerators = this.registerInternalGenerators.bind(this);
this.debug = debug;
this.logger = new Logger(this.debug);
this.logger.debug("Hikaru is starting...");
process.on("exit", () => {
return this.logger.debug("Hikaru is stopping...");
});
if (process.platform === "win32") {
require("readline").createInterface({
"input": process.stdin,
"output": process.stdout
}).on("SIGINT", function() {
return process.emit("SIGINT");
});
}
process.on("SIGINT", function() {
return process.exit(0);
});
}
init(workDir = ".", configPath) {
return fse.mkdirp(workDir).then(() => {
this.logger.debug(`Hikaru is copying \`${colors.cyan(configPath || path.join(workDir, "config.yml"))}\`.`);
this.logger.debug(`Hikaru is creating \`${colors.cyan(path.join(workDir, "srcs", path.sep))}\`.`);
this.logger.debug(`Hikaru is creating \`${colors.cyan(path.join(workDir, "docs", path.sep))}\`.`);
this.logger.debug(`Hikaru is creating \`${colors.cyan(path.join(workDir, "themes", path.sep))}\`.`);
fse.copy(path.join(__dirname, "..", "dist", "config.yml"), configPath || path.join(workDir, "config.yml"));
fse.mkdirp(path.join(workDir, "srcs")).then(() => {
this.logger.debug(`Hikaru is copying \`${colors.cyan(path.join(workDir, "srcs", "archives", "index.md"))}\`.`);
this.logger.debug(`Hikaru is copying \`${colors.cyan(path.join(workDir, "srcs", "categories", "index.md"))}\`.`);
this.logger.debug(`Hikaru is copying \`${colors.cyan(path.join(workDir, "srcs", "tags", "index.md"))}\`.`);
fse.copy(path.join(__dirname, "..", "dist", "archives.md"), path.join(workDir, "srcs", "archives", "index.md"));
fse.copy(path.join(__dirname, "..", "dist", "categories.md"), path.join(workDir, "srcs", "categories", "index.md"));
return fse.copy(path.join(__dirname, "..", "dist", "tags.md"), path.join(workDir, "srcs", "tags", "index.md"));
});
fse.mkdirp(path.join(workDir, "docs"));
return fse.mkdirp(path.join(workDir, "themes"));
}).catch((err) => {
this.logger.warn("Hikaru catched some error during initializing!");
return this.logger.error(err);
});
}
clean(workDir = ".", configPath) {
var siteConfig;
configPath = configPath || path.join(workDir, "config.yml");
siteConfig = yaml.safeLoad(fse.readFileSync(configPath, "utf8"));
if ((siteConfig != null ? siteConfig["docDir"] : void 0) == null) {
return;
}
return glob("*", {
"cwd": path.join(workDir, siteConfig["docDir"])
}, (err, res) => {
if (err) {
return err;
}
return res.map((r) => {
return fse.stat(path.join(workDir, siteConfig["docDir"], r)).then((stats) => {
if (stats.isDirectory()) {
this.logger.debug(`Hikaru is removing \`${colors.cyan(path.join(workDir, siteConfig["docDir"], r, path.sep))}\`.`);
} else {
this.logger.debug(`Hikaru is removing \`${colors.cyan(path.join(workDir, siteConfig["docDir"], r))}\`.`);
}
return fse.remove(path.join(workDir, siteConfig["docDir"], r));
}).catch((err) => {
this.logger.warn("Hikaru catched some error during cleaning!");
return this.logger.error(err);
});
});
});
}
async build(workDir = ".", configPath) {
var err;
this.loadSite(workDir, configPath);
this.loadModules();
try {
return (await this.router.build());
} catch (error) {
err = error;
this.logger.warn("Hikaru catched some error during generating!");
this.logger.error(err);
return this.logger.warn("Hikaru advise you to check generated files!");
}
}
async serve(workDir = ".", configPath, ip, port) {
var err;
this.loadSite(workDir, configPath);
this.loadModules();
try {
return (await this.router.serve(ip || "localhost", Number.parseInt(port) || 2333));
} catch (error) {
err = error;
this.logger.warn("Hikaru catched some error during serving!");
return this.logger.error(err);
}
}
loadSite(workDir, configPath) {
var err;
this.site = new Site(workDir);
configPath = configPath || path.join(this.site.get("workDir"), "config.yml");
try {
this.site.set("siteConfig", yaml.safeLoad(fse.readFileSync(configPath, "utf8")));
} catch (error) {
err = error;
this.logger.warn("Hikaru cannot find site config!");
this.logger.error(err);
process.exit(-1);
}
this.site.set("srcDir", path.join(this.site.get("workDir"), this.site.get("siteConfig")["srcDir"] || "srcs"));
this.site.set("docDir", path.join(this.site.get("workDir"), this.site.get("siteConfig")["docDir"] || "docs"));
this.site.set("themeDir", path.join(this.site.get("workDir"), "themes", this.site.get("siteConfig")["themeDir"]));
this.site.set("themeSrcDir", path.join(this.site.get("themeDir"), "srcs"));
try {
this.site.set("themeConfig", yaml.safeLoad(fse.readFileSync(path.join(this.site.get("themeDir"), "config.yml"))));
} catch (error) {
err = error;
if (err["code"] === "ENOENT") {
this.logger.warn("Hikaru continues with a empty theme config...");
}
}
this.site.set("categoryDir", this.site.get("siteConfig")["categoryDir"] || "categories");
return this.site.set("tagDir", this.site.get("siteConfig")["tagDir"] || "tags");
}
loadModules() {
var defaultLanguage, err;
this.renderer = new Renderer(this.logger, this.site.get("siteConfig")["skipRender"]);
this.processer = new Processer(this.logger);
this.generator = new Generator(this.logger);
this.translator = new Translator(this.logger);
try {
defaultLanguage = yaml.safeLoad(fse.readFileSync(path.join(this.site.get("themeDir"), "languages", "default.yml")));
this.translator.register("default", defaultLanguage);
} catch (error) {
err = error;
if (err["code"] === "ENOENT") {
this.logger.warn("Hikaru cannot find default language file in your theme.");
}
}
this.router = new Router(this.logger, this.renderer, this.processer, this.generator, this.translator, this.site);
try {
this.registerInternalRenderers();
this.registerInternalProcessers();
return this.registerInternalGenerators();
} catch (error) {
err = error;
this.logger.warn("Hikaru cannot register internal functions!");
this.logger.error(err);
return process.exit(-2);
}
}
registerInternalRenderers() {
var coffeeConfig, markedConfig, njkConfig, njkEnv, stylConfig;
njkConfig = Object.assign({
"autoescape": false
}, this.site.get("siteConfig")["nunjucks"]);
njkEnv = nunjucks.configure(this.site.get("themeSrcDir"), njkConfig);
this.renderer.register([".njk", ".j2"], null, function(file, ctx) {
return new Promise(function(resolve, reject) {
var err, template;
try {
template = nunjucks.compile(file["text"], njkEnv, file["srcPath"]);
// For template you must give a render function.
file["content"] = function(ctx) {
return new Promise(function(resolve, reject) {
return template.render(ctx, function(err, res) {
if (err) {
return reject(err);
}
return resolve(res);
});
});
};
return resolve(file);
} catch (error) {
err = error;
return reject(err);
}
});
});
markedConfig = Object.assign({
"gfm": true,
"langPrefix": "",
"highlight": (code, lang) => {
return highlight(code, Object.assign({
"lang": lang != null ? lang.toLowerCase() : void 0,
"hljs": true,
"gutter": true
}, this.site.get("siteConfig")["highlight"]));
}
}, this.site.get("siteConfig")["marked"]);
marked.setOptions(markedConfig);
this.renderer.register(".md", ".html", function(file, ctx) {
return new Promise(function(resolve, reject) {
var err;
try {
file["content"] = marked(file["text"]);
return resolve(file);
} catch (error) {
err = error;
return reject(err);
}
});
});
stylConfig = this.site.get("siteConfig")["stylus"] || {};
this.renderer.register(".styl", ".css", (file, ctx) => {
return new Promise((resolve, reject) => {
return stylus(file["text"]).use(nib()).use((style) => {
return style.define("getSiteConfig", (file) => {
var j, k, keys, len, res;
keys = file["val"].toString().split(".");
res = this.site.get("siteConfig");
for (j = 0, len = keys.length; j < len; j++) {
k = keys[j];
if (!(k in res)) {
return null;
}
res = res[k];
}
return res;
});
}).use((style) => {
return style.define("getThemeConfig", (file) => {
var j, k, keys, len, res;
keys = file["val"].toString().split(".");
res = this.site.get("themeConfig");
for (j = 0, len = keys.length; j < len; j++) {
k = keys[j];
if (!(k in res)) {
return null;
}
res = res[k];
}
return res;
});
}).set("filename", path.join(this.site.get("themeSrcDir"), file["srcPath"])).set("sourcemap", stylConfig["sourcemap"]).set("compress", stylConfig["compress"]).set("include css", true).render(function(err, res) {
if (err) {
return reject(err);
}
file["content"] = res;
return resolve(file);
});
});
});
coffeeConfig = this.site.get("siteConfig")["coffeescript"] || {};
return this.renderer.register(".coffee", ".js", function(file, ctx) {
return new Promise(function(resolve, reject) {
var err;
try {
file["content"] = coffee.compile(file["text"], coffeeConfig);
return resolve(file);
} catch (error) {
err = error;
return reject(err);
}
});
});
}
registerInternalProcessers() {
this.processer.register("index", (p, posts, ctx) => {
return new Promise((resolve, reject) => {
var err;
try {
posts.sort(function(a, b) {
return -(a["date"] - b["date"]);
});
return resolve(paginate(p, posts, this.site.get("siteConfig")["perPage"], ctx));
} catch (error) {
err = error;
return reject(err);
}
});
});
this.processer.register("archives", (p, posts, ctx) => {
return new Promise((resolve, reject) => {
var err;
try {
posts.sort(function(a, b) {
return -(a["date"] - b["date"]);
});
return resolve(paginate(p, posts, this.site.get("siteConfig")["perPage"], ctx));
} catch (error) {
err = error;
return reject(err);
}
});
});
this.processer.register("categories", (p, posts, ctx) => {
return new Promise((resolve, reject) => {
var err;
try {
return resolve(Object.assign(new File(), p, ctx, {
"categories": this.site.get("categories")
}));
} catch (error) {
err = error;
return reject(err);
}
});
});
this.processer.register("tags", (p, posts, ctx) => {
return new Promise((resolve, reject) => {
var err;
try {
return resolve(Object.assign(new File(), p, ctx, {
"tags": this.site.get("tags")
}));
} catch (error) {
err = error;
return reject(err);
}
});
});
return this.processer.register(["post", "page"], (p, posts, ctx) => {
return new Promise((resolve, reject) => {
var $, a, err, escaped, getPath, getURL, h, hNames, headerIds, headings, href, i, id, imgs, j, l, len, len1, len2, level, links, m, split, src, text, toc;
try {
getURL = getURLFn(this.site.get("siteConfig")["baseURL"], this.site.get("siteConfig")["rootDir"]);
getPath = getPathFn(this.site.get("siteConfig")["rootDir"]);
$ = cheerio.load(p["content"]);
// TOC generate.
hNames = ["h1", "h2", "h3", "h4", "h5", "h6"];
headings = $(hNames.join(", "));
toc = [];
headerIds = {};
for (j = 0, len = headings.length; j < len; j++) {
h = headings[j];
level = toc;
while (level.length > 0 && hNames.indexOf(level[level.length - 1]["name"]) < hNames.indexOf(h["name"])) {
level = level[level.length - 1]["subs"];
}
text = $(h).text();
// Remove some chars in escaped ID because
// bootstrap scrollspy cannot support it.
escaped = escapeHTML(text).trim().replace(/[\s\(\)\[\]{}<>\.,\!\@#\$%\^&\*=\|`"'\/\?~]+/g, "");
if (headerIds[escaped]) {
id = `${escaped}-${headerIds[escaped]++}`;
} else {
id = escaped;
headerIds[escaped] = 1;
}
$(h).attr("id", `${id}`);
$(h).html(`<a class="headerlink" href="#${id}" title="${escaped}">` + "</a>" + `${text}`);
// Don't set archor to absolute path because bootstrap scrollspy
// can only accept relative path for ID.
level.push({
"archor": `#${id}`,
"name": h["name"],
"text": text.trim(),
"subs": []
});
}
// Replace relative path to absolute path.
links = $("a");
for (l = 0, len1 = links.length; l < len1; l++) {
a = links[l];
href = $(a).attr("href");
if (href == null) {
continue;
}
if (new URL(href, this.site.get("siteConfig")["baseURL"]).host !== getURL(p["docPath"]).host) {
$(a).attr("target", "_blank");
}
if (href.startsWith("https://") || href.startsWith("http://") || href.startsWith("//") || href.startsWith("/") || href.startsWith("javascript:")) {
continue;
}
$(a).attr("href", getPath(path.join(path.dirname(p["docPath"]), href)));
}
imgs = $("img");
for (m = 0, len2 = imgs.length; m < len2; m++) {
i = imgs[m];
src = $(i).attr("src");
if (src == null) {
continue;
}
if (src.startsWith("https://") || src.startsWith("http://") || src.startsWith("//") || src.startsWith("/") || src.startsWith("file:image")) {
continue;
}
$(i).attr("src", getPath(path.join(path.dirname(p["docPath"]), src)));
}
p["content"] = $("body").html();
if (p["content"].indexOf("<!--more-->") !== -1) {
split = p["content"].split("<!--more-->");
p["excerpt"] = split[0];
p["more"] = split[1];
p["content"] = split.join("<a id=\"more\"></a>");
}
return resolve(Object.assign(new File(), p, ctx, {
"toc": toc,
"$": $
}));
} catch (error) {
err = error;
return reject(err);
}
});
});
}
registerInternalGenerators() {
this.generator.register("beforeProcessing", function(site) {
// Generate categories
return new Promise(function(resolve, reject) {
var cateName, categories, categoriesLength, category, err, found, j, l, len, len1, len2, len3, len4, m, n, newCate, o, p, post, postCategories, ref, ref1, ref2, sub, subCategories;
try {
categories = [];
categoriesLength = 0;
ref = site.get("posts");
for (j = 0, len = ref.length; j < len; j++) {
post = ref[j];
if (post["frontMatter"]["categories"] == null) {
continue;
}
postCategories = [];
subCategories = categories;
ref1 = post["frontMatter"]["categories"];
for (l = 0, len1 = ref1.length; l < len1; l++) {
cateName = ref1[l];
found = false;
for (m = 0, len2 = subCategories.length; m < len2; m++) {
category = subCategories[m];
if (category["name"] === cateName) {
found = true;
postCategories.push(category);
category["posts"].push(post);
subCategories = category["subs"];
break;
}
}
if (!found) {
newCate = new Category(cateName, [post], []);
++categoriesLength;
postCategories.push(newCate);
subCategories.push(newCate);
subCategories = newCate["subs"];
}
}
post["categories"] = postCategories;
}
categories.sort(function(a, b) {
return a["name"].localeCompare(b["name"]);
});
for (n = 0, len3 = categories.length; n < len3; n++) {
sub = categories[n];
sortCategories(sub);
ref2 = paginateCategories(sub, site.get("categoryDir"), site.get("siteConfig")["perPage"], site);
for (o = 0, len4 = ref2.length; o < len4; o++) {
p = ref2[o];
site.put("pages", p);
}
}
site.set("categories", categories);
site.set("categoriesLength", categoriesLength);
return resolve(site);
} catch (error) {
err = error;
return reject(err);
}
});
});
this.generator.register("beforeProcessing", function(site) {
// Generate tags.
return new Promise(function(resolve, reject) {
var err, found, j, l, len, len1, len2, len3, len4, m, n, newTag, o, p, post, postTags, ref, ref1, ref2, sp, tag, tagName, tags, tagsLength;
try {
tags = [];
tagsLength = 0;
ref = site.get("posts");
for (j = 0, len = ref.length; j < len; j++) {
post = ref[j];
if (post["frontMatter"]["tags"] == null) {
continue;
}
postTags = [];
ref1 = post["frontMatter"]["tags"];
for (l = 0, len1 = ref1.length; l < len1; l++) {
tagName = ref1[l];
found = false;
for (m = 0, len2 = tags.length; m < len2; m++) {
tag = tags[m];
if (tag["name"] === tagName) {
found = true;
postTags.push(tag);
tag["posts"].push(post);
break;
}
}
if (!found) {
newTag = new Tag(tagName, [post]);
++tagsLength;
postTags.push(newTag);
tags.push(newTag);
}
}
post["tags"] = postTags;
}
tags.sort(function(a, b) {
return a["name"].localeCompare(b["name"]);
});
for (n = 0, len3 = tags.length; n < len3; n++) {
tag = tags[n];
tag["posts"].sort(function(a, b) {
return -(a["date"] - b["date"]);
});
sp = Object.assign(new File(site.get("docDir")), {
"layout": "tag",
"docPath": path.join(site.get("tagDir"), `${tag["name"]}`, "index.html"),
"title": "tag",
"name": tag["name"].toString()
});
tag["docPath"] = sp["docPath"];
ref2 = paginate(sp, tag["posts"], site.get("siteConfig")["perPage"]);
for (o = 0, len4 = ref2.length; o < len4; o++) {
p = ref2[o];
site.put("pages", p);
}
}
site.set("tags", tags);
site.set("tagsLength", tagsLength);
return resolve(site);
} catch (error) {
err = error;
return reject(err);
}
});
});
this.generator.register("afterProcessing", function(site) {
return new Promise(function(resolve, reject) {
var all, err, file, getPath, j, len, p, search;
try {
if (!site.get("siteConfig")["search"]["enable"]) {
return resolve(site);
}
// Generate search index.
search = [];
all = site.get("pages").concat(site.get("posts"));
getPath = getPathFn(site.get("siteConfig")["rootDir"]);
for (j = 0, len = all.length; j < len; j++) {
p = all[j];
search.push({
"title": `${p["title"]}`,
"url": getPath(p["docPath"]),
"content": p["text"]
});
}
file = new File(site.get("docDir"));
file["docPath"] = site.get("siteConfig")["search"]["path"] || "search.json";
file["content"] = JSON.stringify(search);
site.put("files", file);
return resolve(site);
} catch (error) {
err = error;
return reject(err);
}
});
});
this.generator.register("afterProcessing", function(site) {
return new Promise(function(resolve, reject) {
var content, err, file, tmpContent;
try {
if (!site.get("siteConfig")["sitemap"]["enable"]) {
return resolve(site);
}
// Generate sitemap.
tmpContent = fse.readFileSync(path.join(__dirname, "..", "dist", "sitemap.njk"), "utf8");
content = nunjucks.renderString(tmpContent, {
"posts": site.get("posts"),
"getURL": getURLFn(site.get("siteConfig")["baseURL"], site.get("siteConfig")["rootDir"]),
"getPath": getPathFn(site.get("siteConfig")["rootDir"])
});
file = new File(site.get("docDir"));
file["docPath"] = site.get("siteConfig")["sitemap"]["path"] || "sitemap.xml";
file["content"] = content;
site.put("files", file);
return resolve(site);
} catch (error) {
err = error;
return reject(err);
}
});
});
return this.generator.register("afterProcessing", function(site) {
return new Promise(function(resolve, reject) {
var content, err, file, tmpContent;
try {
if (!site.get("siteConfig")["feed"]["enable"]) {
return resolve(site);
}
// Generate RSS feed.
tmpContent = fse.readFileSync(path.join(__dirname, "..", "dist", "atom.njk"), "utf8");
content = nunjucks.renderString(tmpContent, {
"siteConfig": site.get("siteConfig"),
"themeConfig": site.get("themeConfig"),
"posts": site.get("posts"),
"removeControlChars": removeControlChars,
"getURL": getURLFn(site.get("siteConfig")["baseURL"], site.get("siteConfig")["rootDir"]),
"getPath": getPathFn(site.get("siteConfig")["rootDir"])
});
file = new File(site.get("docDir"));
file["docPath"] = site.get("siteConfig")["feed"]["path"] || "atom.xml";
file["content"] = content;
site.put("files", file);
return resolve(site);
} catch (error) {
err = error;
return reject(err);
}
});
});
}
};
module.exports = Hikaru;
}).call(this);