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 Mold = require("mold-template");
8 | var util = require("./util");
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 = Mold.prototype.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 prepareMold(ctx) {
109 | var mold = new Mold(ctx)
110 | if (util.exists("_includes/", true))
111 | fs.readdirSync("_includes/").forEach(function(file) {
112 | mold.bake(file.match(/^(.*?)\.[^\.]+$/)[1], fs.readFileSync("_includes/" + file, "utf8"));
113 | });
114 | return mold
115 | }
116 |
117 | var layouts = {};
118 | function getLayout(name, mold) {
119 | if (name.indexOf(".") == -1) name = name + ".html";
120 | if (layouts.hasOwnProperty(name)) return layouts[name];
121 | var tmpl = function layout(doc) {
122 | var text = layout.template(doc);
123 | if (layout.parent) {
124 | var wrapper = Object.create(doc, {content: {value: text}});
125 | text = getLayout(layout.parent, mold)(wrapper);
126 | }
127 | return text;
128 | };
129 | tmpl.filename = name;
130 | var contents = readContents(fs.readFileSync("_layouts/" + name, "utf8"));
131 | tmpl.template = mold.bake(name, contents.mainText);
132 | tmpl.parent = contents.frontMatter && "layout" in contents.frontMatter ?
133 | contents.frontMatter.layout : null;
134 | return layouts[name] = tmpl;
135 | }
136 |
137 | function generate(mold) {
138 | var config = readConfig();
139 | renderMarkdown = getRenderMarkdown(config);
140 | var posts = readPosts(config);
141 | var ctx = {site: {posts: posts, tags: gatherTags(posts), config: config},
142 | dateFormat: require("dateformat")};
143 | var mold = prepareMold(ctx);
144 | if (util.exists("_site", true)) rmrf.sync("_site");
145 | posts.forEach(function(post) {
146 | if (post.isLink) return;
147 | var path = "_site/" + fillTemplate(config.postFileName, post);
148 | ensureDirectories(path);
149 | fs.writeFileSync(path, getLayout(post.layout || "post.html", mold)(post), "utf8");
150 | });
151 | function walkDir(dir) {
152 | fs.readdirSync(dir).forEach(function(fname) {
153 | if (/^[_\.]/.test(fname)) return;
154 | var file = dir + fname;
155 | if (fs.statSync(file).isDirectory()) {
156 | walkDir(file + "/");
157 | } else {
158 | var out = "_site/" + file;
159 | ensureDirectories(out);
160 | var contents = readContents(fs.readFileSync(file, "utf8"));
161 | if (contents.frontMatter) {
162 | var doc = contents.frontMatter;
163 | var layout = getLayout(doc.layout || "default.html", mold);
164 | doc.content = /\.(md|markdown)$/.test(fname) ?
165 | renderMarkdown(contents.mainText) :
166 | contents.mainText;
167 | doc.name = fname.match(/^(.*?)\.[^\.]+$/)[1];
168 | doc.url = file;
169 | out = out.replace(/\.(md|markdown)$/, layout.filename.match(/(\.\w+|)$/)[1]);
170 | fs.writeFileSync(out, layout(doc), "utf8");
171 | } else {
172 | util.copyFileSync(file, out);
173 | }
174 | }
175 | });
176 | }
177 | walkDir("./");
178 | }
179 |
180 | generate();
181 |
--------------------------------------------------------------------------------