UNPKG

hikaru-coffee

Version:

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

356 lines (334 loc) 11.2 kB
fse = require("fs-extra") path = require("path") yaml = require("js-yaml") glob = require("glob") http = require("http") {URL} = require("url") colors = require("colors/safe") moment = require("moment") chokidar = require("chokidar") Promise = require("bluebird") {Site, File, Category, Tag} = require("./type") { getPathFn, getURLFn, getContentType, isCurrentPathFn, parseFrontMatter } = require("./utils") class Router constructor: (logger, renderer, processer, generator, translator, site) -> @logger = logger @renderer = renderer @processer = processer @generator = generator @translator = translator @site = site @_ = {} @srcWatcher = null @themeWatcher = null @unprocessedSite = new Site(@site["workDir"]) @getURL = getURLFn( @site.get("siteConfig")["baseURL"], @site.get("siteConfig")["rootDir"] ) @getPath = getPathFn(@site.get("siteConfig")["rootDir"]) matchFiles: (pattern, options) -> return new Promise((resolve, reject) -> glob(pattern, options, (err, res) -> if err return reject(err) return resolve(res) ) ) readFile: (file) -> raw = await fse.readFile(path.join(file["srcDir"], file["srcPath"])) # Auto detect if a file is a binary file or a UTF-8 encoding text file. if raw.equals(Buffer.from(raw.toString("utf8"), "utf8")) raw = raw.toString("utf8") file["text"] = raw file["raw"] = raw return file writeFile: (file) -> if file["content"] isnt file["raw"] return fse.outputFile( path.join(file["docDir"], file["docPath"]), file["content"] ) return fse.copy( path.join(file["srcDir"], file["srcPath"]), path.join(file["docDir"], file["docPath"]) ) loadFile: (file) => @logger.debug("Hikaru is reading `#{colors.cyan( path.join(file["srcDir"], file["srcPath"]) )}`...") file = await @readFile(file) if file["srcDir"] is @site.get("themeSrcDir") file = await @renderer.render(file) if path.dirname(file["srcPath"]) isnt "." file["type"] = "asset" @site.put("assets", file) else file["type"] = "template" @site.get("templates")[path.basename( file["srcPath"], path.extname(file["srcPath"]) )] = file else if file["srcDir"] is @site.get("srcDir") file = parseFrontMatter(file) file = await @renderer.render(file) if file["text"] isnt file["raw"] if file["layout"] is "post" file["type"] = "post" @site.put("posts", file) else file["layout"] = file["layout"] or "page" file["type"] = "page" @site.put("pages", file) else file["type"] = "asset" @site.put("assets", file) return file saveFile: (file) => @logger.debug("Hikaru is writing `#{colors.cyan( path.join(file["docDir"], file["docPath"]) )}`...") return @writeFile(file) processFile: (f) => lang = f["language"] or @site.get("siteConfig")["language"] if lang not of @translator.list() try language = yaml.safeLoad(fse.readFileSync(path.join( @site.get("themeDir"), "languages", "#{lang}.yml" ))) @translator.register(lang, language) catch err if err["code"] is "ENOENT" @logger.warn( "Hikaru cannot find `#{lang}` language file in your theme." ) fs = await @processer.process(f, @site.get("posts"), { "site": @site.raw(), "siteConfig": @site.get("siteConfig"), "themeConfig": @site.get("themeConfig"), "moment": moment, "getURL": @getURL, "getPath": @getPath, "isCurrentPath": isCurrentPathFn( @site.get("siteConfig")["rootDir"], f["docPath"] ), "__": @translator.getTranslateFn(lang) }) if fs not instanceof Array return [fs] return fs processPosts: () => @site.get("posts").sort((a, b) -> return -(a["date"] - b["date"]) ) processed = [] for ps in @site.get("posts") ps = await @processFile(ps) processed = processed.concat(ps) @site.set("posts", processed) for i in [0...@site.get("posts").length] if i > 0 @site.get("posts")[i]["next"] = @site.get("posts")[i - 1] if i < @site.get("posts").length - 1 @site.get("posts")[i]["prev"] = @site.get("posts")[i + 1] processPages: () => for ps in @site.get("pages") ps = await @processFile(ps) for p in ps @site.put("pages", p) saveAssets: () => return @site.get("assets").map((asset) => @saveFile(asset) return asset ) savePosts: () => return @site.get("posts").map((p) => p["content"] = await @site.get("templates")[p["layout"]]["content"](p) @saveFile(p) return p ) savePages: () => return @site.get("pages").map((p) => if p["layout"] not of @site.get("templates") p["layout"] = "page" p["content"] = await @site.get("templates")[p["layout"]]["content"](p) @saveFile(p) return p ) saveFiles: () => return @site.get("files").map((file) => @saveFile(file) return file ) buildServerRoutes: () => @_ = {} for f in @site.get("assets").concat(@site.get("posts")) .concat(@site.get("pages")).concat(@site.get("files")) key = @getPath(f["docPath"]) @logger.debug("Hikaru is building route `#{colors.cyan(key)}`...") @_[key] = f watchTheme: () => @themeWatcher = chokidar.watch(path.join("**", "*"), { "cwd": @site.get("themeSrcDir"), "ignoreInitial": true }) for event in ["add", "change", "unlink"] then do (event) => @themeWatcher.on(event, (srcPath) => @logger.debug( "Hikaru watched event `#{colors.blue(event)}` from `#{ colors.cyan(path.join(@site.get("themeSrcDir"), srcPath)) }`" ) @site.set("pages", @unprocessedSite.get("pages")) @unprocessedSite.set("pages", @site.get("pages")[0...]) file = {"srcDir": @site.get("themeSrcDir"), "srcPath": srcPath} if event isnt "unlink" file = await @loadFile(file) if file["type"] is "template" for k, v of @site.get("templates") @site.get("templates")[k] = await @renderer.render(v) else if file["type"] is "asset" @site.set("assets", await Promise.all( @site.get("assets").map((file) => return @renderer.render(file) ) )) else for key in ["assets", "templates"] if @site.del(key, file)? break @site = await @generator.generate("beforeProcessing", @site) await @processPosts() await @processPages() @site = await @generator.generate("afterProcessing", @site) @buildServerRoutes() ) watchSrc: () => @srcWatcher = chokidar.watch(path.join("**", "*"), { "cwd": @site.get("srcDir"), "ignoreInitial": true }) for event in ["add", "change", "unlink"] then do (event) => @srcWatcher.on(event, (srcPath) => @logger.debug( "Hikaru watched event `#{colors.blue(event)}` from `#{ colors.cyan(path.join(@site.get("srcDir"), srcPath)) }`" ) @site.set("pages", @unprocessedSite.get("pages")) @unprocessedSite.set("pages", @site.get("pages")[0...]) file = {"srcDir": @site.get("srcDir"), "srcPath": srcPath} if event isnt "unlink" file = await @loadFile(file) if file["type"] is "asset" @site.set("assets", await Promise.all( @site.get("assets").map((file) => return @renderer.render(file) ) )) else for key in ["assets", "pages", "posts"] if @site.del(key, file)? break @site = await @generator.generate("beforeProcessing", @site) await @processPosts() await @processPages() @site = await @generator.generate("afterProcessing", @site) @buildServerRoutes() ) unwatchAll: () => @themeWatcher.close() @themeWatcher = null @srcWatcher.close() @srcWatcher = null listen: (ip, port) => server = http.createServer((request, response) => if request["url"] not of @_ @logger.log("404: #{request["url"]}") res = @_[@getPath("404.html")] response.writeHead(404, { "Content-Type": getContentType(res["docPath"]) }) else @logger.log("200: #{request["url"]}") res = @_[request["url"]] response.writeHead(200, { "Content-Type": getContentType(res["docPath"]) }) if res["layout"]? if res["layout"] not of @site.get("templates") res["layout"] = "page" response.write( await @site.get("templates")[res["layout"]]["content"](res) ) else response.write(res["content"]) response.end() ) process.prependListener("exit", () => server.close() @logger.log( "Hikaru stopped listening on http://#{ip}:#{port}#{@getPath()}..." ) @unwatchAll() ) @logger.log( "Hikaru is listening on http://#{ip}:#{port}#{@getPath()}..." ) if ip isnt "localhost" server.listen(port, ip) else server.listen(port) @watchTheme() @watchSrc() build: () => allFiles = (await @matchFiles(path.join("**", "*"), { "nodir": true, "dot": true, "cwd": @site.get("themeSrcDir") })).map((srcPath) => return new File(@site.get("docDir"), @site.get("themeSrcDir"), srcPath) ).concat((await @matchFiles(path.join("**", "*"), { "nodir": true, "dot": true, "cwd": @site.get("srcDir") })).map((srcPath) => return new File(@site.get("docDir"), @site.get("srcDir"), srcPath) )) await Promise.all(allFiles.map(@loadFile)) @saveAssets() @site = await @generator.generate("beforeProcessing", @site) await @processPosts() await @processPages() @site = await @generator.generate("afterProcessing", @site) @savePosts() @savePages() @saveFiles() serve: (ip, port) => allFiles = (await @matchFiles(path.join("**", "*"), { "nodir": true, "dot": true, "cwd": @site.get("themeSrcDir") })).map((srcPath) => return new File(@site.get("docDir"), @site.get("themeSrcDir"), srcPath) ).concat((await @matchFiles(path.join("**", "*"), { "nodir": true, "dot": true, "cwd": @site.get("srcDir") })).map((srcPath) => return new File(@site.get("docDir"), @site.get("srcDir"), srcPath) )) await Promise.all(allFiles.map(@loadFile)) @unprocessedSite.set("pages", @site.get("pages")[0...]) @site = await @generator.generate("beforeProcessing", @site) await @processPosts() await @processPages() @site = await @generator.generate("afterProcessing", @site) @buildServerRoutes() @listen(ip, port) module.exports = Router