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 |
--------------------------------------------------------------------------------