├── .gitignore ├── markdownRenderer.js ├── util.js ├── highlightCode.js ├── package.json ├── LICENSE ├── README.md └── heckle.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .tern-* 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /markdownRenderer.js: -------------------------------------------------------------------------------- 1 | const markdownIt = require("markdown-it") 2 | const highlightCode = require("./highlightCode") 3 | 4 | let markdown = markdownIt({highlight: highlightCode, langPrefix: 'lang-', html: true}) 5 | markdown.use(require("markdown-it-anchor"), { 6 | slugify: s => s.toLowerCase().replace(/\W/g, '-').replace(/-+/g, '-') 7 | }); 8 | module.exports = markdown.render.bind(markdown) 9 | -------------------------------------------------------------------------------- /util.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | 3 | exports.copyFileSync = function(input, output) { 4 | var buff = Buffer.alloc(65536), pos = 0; 5 | var infd = fs.openSync(input, "r"), outfd = fs.openSync(output, "w"); 6 | do { 7 | var read = fs.readSync(infd, buff, 0, 65536, pos); 8 | pos += read; 9 | fs.writeSync(outfd, buff, 0, read); 10 | } while (read); 11 | fs.closeSync(infd); fs.closeSync(outfd); 12 | }; 13 | 14 | exports.exists = function(file, isDir) { 15 | try { return fs.statSync(file)[isDir ? "isDirectory" : "isFile"](); } 16 | catch(e) { return false; } 17 | }; 18 | 19 | exports.escapeHTML = function(text) { 20 | return String(text).replace(/[<&\"]/g, function(ch) {return HTMLspecial[ch];}); 21 | }; -------------------------------------------------------------------------------- /highlightCode.js: -------------------------------------------------------------------------------- 1 | const escapeHTML = require("./util").escapeHTML 2 | const CodeMirror = require("codemirror/addon/runmode/runmode.node.js") 3 | 4 | module.exports = function highlightCode(code, lang) { 5 | if (!lang) return escapeHTML(code) 6 | if (!CodeMirror.modes.hasOwnProperty(lang)) { 7 | try { require("codemirror/mode/" + lang + "/" + lang) } 8 | catch(e) { console.log(e.toString()); CodeMirror.modes[lang] = false } 9 | } 10 | if (CodeMirror.modes[lang]) { 11 | let html = "" 12 | CodeMirror.runMode(code, lang, function(token, style) { 13 | if (style) html += "" + escapeHTML(token) + "" 14 | else html += escapeHTML(token) 15 | }); 16 | return html 17 | } else return escapeHTML(code) 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "heckle-blog", 3 | "version": "0.2.0", 4 | "main": "heckle.js", 5 | "description": "Static blog generator", 6 | "licenses": [ 7 | { 8 | "type": "MIT", 9 | "url": "https://raw.github.com/marijnh/heckle/master/LICENSE" 10 | } 11 | ], 12 | "dependencies": { 13 | "codemirror": "^5.24.0", 14 | "dateformat": "^3.0.0", 15 | "handlebars": "^4.1.2", 16 | "js-yaml": "^3.12.2", 17 | "markdown-it": "^8.4.2", 18 | "markdown-it-anchor": "^5.0.0", 19 | "rimraf": "^2.6.3" 20 | }, 21 | "bugs": "http://github.com/marijnh/heckle/issues", 22 | "keywords": [ 23 | "Jekyll", 24 | "Blog" 25 | ], 26 | "homepage": "https://github.com/marijnh/heckle", 27 | "author": "Marijn Haverbeke ", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/marijnh/heckle.git" 31 | }, 32 | "license": "MIT", 33 | "bin": { 34 | "heckle": "./heckle.js" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 by Marijn Haverbeke 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | Please note that some subdirectories of the CodeMirror distribution 22 | include their own LICENSE files, and are released under different 23 | licences. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Heckle 2 | 3 | A minimal [Jekyll][1] clone in node.js. 4 | 5 | [1]: https://github.com/mojombo/jekyll 6 | 7 | ## Why? 8 | 9 | I like the approach to managing a site taken by Jekyll. A lot. 10 | 11 | I don't like Ruby, and I don't like strict logic-less templates. 12 | Jekyll is Ruby with Liquid as the templating engine. 13 | 14 | Heckle is JavaScript with Mold (programmable template extravaganza) as 15 | the templating engine. 16 | 17 | ## Setup 18 | 19 | Don't use Heckle at this point if you want something stable and 20 | finished. It's a work in progress, and may be radically changed or 21 | pitilessly abandoned at any time. 22 | 23 | If that didn't scare you off, you should be able to get dependencies 24 | with `npm install`. 25 | 26 | When the dependencies have been installed, you should be able to 27 | change to the directory that contains your blog files, and run... 28 | 29 | nodejs /path/to/heckle/heckle.js 30 | 31 | It parses a `_config.yml` and treats `_posts`, `_layouts`, and 32 | `_includes` dirs much like [Jekyll][1]. Your templates should be in 33 | [Mold][3] syntax and read `$arg` rather than `post` or `page` to get 34 | context information. 35 | 36 | [3]: http://marijnhaverbeke.nl/mold/ 37 | 38 | At some point, more detailed docs, as well as commmand-line arguments, 39 | might materialize. For now, read the code, it's (at the time of 40 | writing) less than 200 lines. 41 | -------------------------------------------------------------------------------- /heckle.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var path = require("path"); 4 | var fs = require("fs"); 5 | var rmrf = require("rimraf"); 6 | var yaml = require("js-yaml"); 7 | var util = require("./util"); 8 | var Handlebars = require('handlebars'); 9 | 10 | /** 11 | * @param contents 12 | * A string containing the file contents. 13 | * @return 14 | * An object with the properties `frontMatter` and `mainText`, 15 | * representing the file front matter and a string containing the 16 | * remainder of the file, respectively. `frontMatter` will be null if 17 | * the file contains no front matter block, otherwise the block's YAML 18 | * properties will be available as JS properties. 19 | */ 20 | function readContents(contents) { 21 | if (/^---\n/.test(contents)) { 22 | var end = contents.search(/\n---\n/); 23 | if (end != -1) 24 | return { 25 | frontMatter: yaml.load(contents.slice(4, end + 1)) || {}, 26 | mainText: contents.slice(end + 5) 27 | }; 28 | } 29 | return {frontMatter: null, mainText: contents}; 30 | } 31 | 32 | let renderMarkdown = null; 33 | 34 | function getRenderMarkdown(config) { 35 | return require(config.markdownRenderer ? path.resolve(process.cwd(), config.markdownRenderer) : "./markdownRenderer"); 36 | } 37 | 38 | function readPosts(config) { 39 | var postsDir = "_posts/"; 40 | var posts = []; 41 | if (!fs.existsSync(postsDir)) return posts; 42 | fs.readdirSync(postsDir).forEach(function(file) { 43 | var d = file.match(/^(\d{4})-(\d\d?)-(\d\d?)-(.+)\.(md|markdown|link|html)$/); 44 | if (!d) return; 45 | var contents = readContents(fs.readFileSync(postsDir + file, "utf8")); 46 | var post = contents.frontMatter || {}; 47 | post.date = new Date(d[1], d[2] - 1, d[3]); 48 | post.name = d[4]; 49 | if (!post.tags) post.tags = []; 50 | if (!post.tags.forEach && post.tags.split) post.tags = post.tags.split(/\s+/); 51 | var extension = d[5]; 52 | if (extension == "link") { 53 | var escd = util.escapeHTML(post.url); 54 | post.content = "

Read this post at " + escd + ".

"; 55 | post.isLink = true; 56 | } else { 57 | post.content = (extension == "md" || extension == "markdown") ? 58 | renderMarkdown(contents.mainText) : 59 | contents.mainText; 60 | post.url = getURL(config, post); 61 | } 62 | posts.push(post); 63 | }); 64 | posts.sort(function(a, b){return b.date - a.date;}); 65 | return posts; 66 | } 67 | 68 | function gatherTags(posts) { 69 | var tags = {}; 70 | posts.forEach(function(post) { 71 | if (post.tags) post.tags.forEach(function(tag) { 72 | (tags.hasOwnProperty(tag) ? tags[tag] : (tags[tag] = [])).push(post); 73 | }); 74 | else post.tags = []; 75 | }); 76 | return tags; 77 | } 78 | 79 | var defaults = { 80 | postLink: "${name}.html", 81 | postFileName: "${url}" 82 | }; 83 | 84 | function readConfig() { 85 | var config = (util.exists("_config.yml") && yaml.load(fs.readFileSync("_config.yml", "utf8"))) || {}; 86 | for (var opt in defaults) if (defaults.hasOwnProperty(opt) && !config.hasOwnProperty(opt)) 87 | config[opt] = defaults[opt]; 88 | return config; 89 | } 90 | 91 | function fillTemplate(tpl, vars) { 92 | for (var prop in vars) tpl = tpl.replace("${" + prop + "}", vars[prop]); 93 | return tpl; 94 | } 95 | 96 | function getURL(config, post) { 97 | return fillTemplate(config.postLink, post); 98 | } 99 | 100 | function ensureDirectories(path) { 101 | var parts = path.split("/"), cur = ""; 102 | for (var i = 0; i < parts.length - 1; ++i) { 103 | cur += parts[i] + "/"; 104 | if (!util.exists(cur, true)) fs.mkdirSync(cur); 105 | } 106 | } 107 | 108 | function partialTemplate(tmpl, ctx) { 109 | return function (newCtx) { 110 | var finalCtx = JSON.parse(JSON.stringify(ctx)); 111 | Object.keys(newCtx).forEach(function (key) { 112 | finalCtx[key] = newCtx[key]; 113 | }); 114 | return tmpl(finalCtx); 115 | }; 116 | } 117 | 118 | function prepareIncludes(ctx) { 119 | if (!util.exists("_includes/", true)) return; 120 | fs.readdirSync("_includes/").forEach(function(file) { 121 | var includeName = file.match(/^(.*?)\.[^\.]+$/)[1]; 122 | Handlebars.registerPartial(includeName, fs.readFileSync("_includes/" + file, "utf8")); 123 | }); 124 | return ctx; 125 | } 126 | 127 | var layouts = {}; 128 | function getLayout(name, ctx) { 129 | if (name.indexOf(".") == -1) name = name + ".html"; 130 | if (layouts.hasOwnProperty(name)) return layouts[name]; 131 | var tmpl = Handlebars.compile(fs.readFileSync("_layouts/" + name, "utf8")); 132 | 133 | var mergedTmpl = partialTemplate(tmpl, ctx); 134 | 135 | mergedTmpl.filename = name; 136 | layouts[name] = mergedTmpl; 137 | 138 | return mergedTmpl; 139 | } 140 | 141 | function generate() { 142 | var config = readConfig(); 143 | renderMarkdown = getRenderMarkdown(config); 144 | var posts = readPosts(config); 145 | var ctx = {site: {posts: posts, tags: gatherTags(posts), config: config}, 146 | dateFormat: require("dateformat")}; 147 | var includes = prepareIncludes(ctx); 148 | if (util.exists("_site", true)) rmrf.sync("_site"); 149 | posts.forEach(function(post) { 150 | if (post.isLink) return; 151 | var path = "_site/" + fillTemplate(config.postFileName, post); 152 | ensureDirectories(path); 153 | fs.writeFileSync(path, getLayout(post.layout || "post.html", includes)(post), "utf8"); 154 | }); 155 | function isExcluded(path) { 156 | return (config.exclude || []).some(function (exclude) { 157 | return path.slice(2).indexOf(exclude) === 0; 158 | }); 159 | } 160 | function walkDir(dir) { 161 | fs.readdirSync(dir).forEach(function(fname) { 162 | if (/^[_\.]/.test(fname)) return; 163 | var file = dir + fname; 164 | if (isExcluded(file)) return; 165 | if (fs.statSync(file).isDirectory()) { 166 | walkDir(file + "/"); 167 | } else { 168 | var out = "_site/" + file; 169 | ensureDirectories(out); 170 | var contents = readContents(fs.readFileSync(file, "utf8")); 171 | if (contents.frontMatter) { 172 | var doc = contents.frontMatter; 173 | var layout = getLayout(doc.layout || "default.html", includes); 174 | doc.content = /\.(md|markdown)$/.test(fname) ? 175 | renderMarkdown(contents.mainText) : 176 | contents.mainText; 177 | doc.name = fname.match(/^(.*?)\.[^\.]+$/)[1]; 178 | doc.url = file; 179 | out = out.replace(/\.(md|markdown)$/, layout.filename.match(/(\.\w+|)$/)[1]); 180 | fs.writeFileSync(out, layout(doc), "utf8"); 181 | } else { 182 | util.copyFileSync(file, out); 183 | } 184 | } 185 | }); 186 | } 187 | walkDir("./"); 188 | } 189 | 190 | generate(); 191 | --------------------------------------------------------------------------------