├── .gitignore
├── .npmignore
├── bin
└── typeblog
├── converter
└── ghost-to-typeblog.coffee
├── example
├── config.json
├── package.json
├── plugins
│ ├── plugin-example-2.coffee
│ └── plugin-example.coffee
├── posts
│ ├── hello.md
│ ├── hello1.md
│ ├── hello2.md
│ ├── hello3.md
│ ├── hello4.md
│ ├── hello5.md
│ └── hello6.md
└── template
│ ├── assets
│ ├── highlight.css
│ └── main.css
│ ├── default.hbs
│ ├── index.hbs
│ └── post.hbs
├── index.js
├── lib
└── utils
│ └── async.js
├── package.json
├── plugins
├── highlight
│ ├── .npmignore
│ ├── index.coffee
│ └── package.json
└── markdown
│ ├── .npmignore
│ ├── index.coffee
│ └── package.json
└── src
├── plugin
├── default.coffee
└── plugin.coffee
├── server.coffee
├── template.coffee
├── typeblog.coffee
└── utils
├── configuration.coffee
└── dependencies.coffee
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | /lib/*
4 | !/lib/utils
5 | /lib/utils/*
6 | !/lib/utils/async.js
7 | plugins/markdown/index.js
8 | plugins/highlight/index.js
9 | converter/from.json
10 | converter/out
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | example
2 | plugins
3 | src
4 | npm-debug.log
5 | converter
--------------------------------------------------------------------------------
/bin/typeblog:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | var path = require('path');
4 | var fs = require('fs');
5 | var dir = path.join(path.dirname(fs.realpathSync(__filename)), '../');
6 |
7 | require(dir + '/index.js')
--------------------------------------------------------------------------------
/converter/ghost-to-typeblog.coffee:
--------------------------------------------------------------------------------
1 | data = require './from.json'
2 | posts = data.db[0].data.posts
3 | fs = require 'fs'
4 | moment = require 'moment'
5 |
6 | fs.mkdirSync './out'
7 | arr = []
8 | posts.forEach (post) ->
9 | result = {}
10 | result.title = post.title
11 | result.url = post.slug
12 | result.date = moment(post.created_at).format 'YYYY-MM-DD'
13 | result.parser = "Markdown"
14 |
15 | content = """
16 | ```json
17 | #{JSON.stringify result, null, 2}
18 | ```
19 |
20 | #{post.markdown}
21 | """
22 | fs.writeFileSync "./out/#{post.slug}.md", content
23 |
24 | if post.published_at?
25 | arr.unshift "posts/#{post.slug}.md"
26 |
27 | fs.writeFileSync "./out/list.json", JSON.stringify arr, null, 2
--------------------------------------------------------------------------------
/example/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1",
3 | "title": "Typeblog",
4 | "description": "Thoughtcrime does not entail death, thoughtcrime IS death",
5 | "url": "http://127.0.0.1:2333",
6 | "plugins": [
7 | "plugins/plugin-example",
8 | "plugins/plugin-example-2",
9 | "npm://typeblog-highlight",
10 | "npm://typeblog-markdown"
11 | ],
12 | "posts": [
13 | "posts/hello6.md",
14 | "posts/hello5.md",
15 | "posts/hello4.md",
16 | "posts/hello3.md",
17 | "posts/hello2.md",
18 | "posts/hello1.md",
19 | "posts/hello.md"
20 | ]
21 | }
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "typeblog-example",
3 | "version": "1.0.0",
4 | "description": "Example",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "author": "",
10 | "license": "WTFPL",
11 | "dependencies": {
12 | "typeblog-markdown": "file:../plugins/markdown",
13 | "typeblog-highlight": "file:../plugins/highlight"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/example/plugins/plugin-example-2.coffee:
--------------------------------------------------------------------------------
1 | {Plugin} = require 'plugin'
2 |
3 | class Example2Plugin extends Plugin
4 | transformExpressApp: ->
5 | return [false, (app) ->
6 | app.get '/plugin/example2', (req, res) ->
7 | res.send 'Hello, plugins (2)!'
8 | return app
9 | ]
10 |
11 | transformRenderResult: ->
12 | return [false, (content) ->
13 | return content.replace 'Typeblog', 'Typeblog_2'
14 | ]
15 |
16 | module.exports = new Example2Plugin
--------------------------------------------------------------------------------
/example/plugins/plugin-example.coffee:
--------------------------------------------------------------------------------
1 | {Plugin} = require 'plugin'
2 |
3 | class ExamplePlugin extends Plugin
4 | transformExpressApp: ->
5 | return [false, (app) ->
6 | app.get '/plugin/example', (req, res) ->
7 | res.send 'Hello, plugins!'
8 | return app
9 | ]
10 |
11 | transformRenderResult: ->
12 | return [false, (content) ->
13 | return content.replace 'Typeblog', 'Typeblog_1'
14 | ]
15 |
16 | module.exports = new ExamplePlugin
--------------------------------------------------------------------------------
/example/posts/hello.md:
--------------------------------------------------------------------------------
1 | ```json
2 | {
3 | "title": "Hello, 世界",
4 | "url": "test/hello-world",
5 | "date": "2016-07-09",
6 | "parser": "Markdown",
7 | "tags": ["hello"]
8 | }
9 | ```
10 |
11 | This is the hello-world post! __test__
--------------------------------------------------------------------------------
/example/posts/hello1.md:
--------------------------------------------------------------------------------
1 | ```json
2 | {
3 | "title": "Hello, 世界",
4 | "url": "test/hello-world-1",
5 | "date": "2016-07-09",
6 | "parser": "Markdown",
7 | "tags": ["hello"]
8 | }
9 | ```
10 |
11 | This is the hello-world post! __test__
12 |
13 | ```
14 | {Plugin} = require 'plugin'
15 |
16 | class ExamplePlugin extends Plugin
17 | transformExpressApp: (app) ->
18 | app.get '/plugin/example', (req, res) ->
19 | res.send 'Hello, plugins!'
20 |
21 | module.exports = new ExamplePlugin
22 | ```
23 |
24 | The following should be an incorrect highlighting
25 |
26 | ```bash
27 | {Plugin} = require 'plugin'
28 |
29 | class ExamplePlugin extends Plugin
30 | transformExpressApp: (app) ->
31 | app.get '/plugin/example', (req, res) ->
32 | res.send 'Hello, plugins!'
33 |
34 | module.exports = new ExamplePlugin
35 | ```
--------------------------------------------------------------------------------
/example/posts/hello2.md:
--------------------------------------------------------------------------------
1 | ```json
2 | {
3 | "title": "Hello, 世界",
4 | "url": "test/hello-world-2",
5 | "date": "2016-07-09",
6 | "tags": ["hello"]
7 | }
8 | ```
9 |
10 | This is the hello-world post! __test__
--------------------------------------------------------------------------------
/example/posts/hello3.md:
--------------------------------------------------------------------------------
1 | ```json
2 | {
3 | "title": "Hello, 世界",
4 | "url": "test/hello-world-3",
5 | "date": "2016-07-09",
6 | "tags": ["hello"]
7 | }
8 | ```
9 |
10 | This is the hello-world post! __test__
--------------------------------------------------------------------------------
/example/posts/hello4.md:
--------------------------------------------------------------------------------
1 | ```json
2 | {
3 | "title": "Hello, 世界",
4 | "url": "test/hello-world-4",
5 | "date": "2016-07-09",
6 | "tags": ["hello"]
7 | }
8 | ```
9 |
10 | This is the hello-world post! __test__
--------------------------------------------------------------------------------
/example/posts/hello5.md:
--------------------------------------------------------------------------------
1 | ```json
2 | {
3 | "title": "Hello, 世界",
4 | "url": "test/hello-world-5",
5 | "date": "2016-07-09",
6 | "tags": ["hello"]
7 | }
8 | ```
9 |
10 | This is the hello-world post! __test__
--------------------------------------------------------------------------------
/example/posts/hello6.md:
--------------------------------------------------------------------------------
1 | ```json
2 | {
3 | "title": "Hello, 世界",
4 | "url": "test/hello-world-6",
5 | "date": "2016-07-09"
6 | }
7 | ```
8 |
9 | This is the hello-world post! __test__
--------------------------------------------------------------------------------
/example/template/assets/highlight.css:
--------------------------------------------------------------------------------
1 | /* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
2 |
3 | /* Tomorrow Comment */
4 | .hljs-comment,
5 | .hljs-quote {
6 | color: #8e908c;
7 | }
8 |
9 | /* Tomorrow Red */
10 | .hljs-variable,
11 | .hljs-template-variable,
12 | .hljs-tag,
13 | .hljs-name,
14 | .hljs-selector-id,
15 | .hljs-selector-class,
16 | .hljs-regexp,
17 | .hljs-deletion {
18 | color: #c82829;
19 | }
20 |
21 | /* Tomorrow Orange */
22 | .hljs-number,
23 | .hljs-built_in,
24 | .hljs-builtin-name,
25 | .hljs-literal,
26 | .hljs-type,
27 | .hljs-params,
28 | .hljs-meta,
29 | .hljs-link {
30 | color: #f5871f;
31 | }
32 |
33 | /* Tomorrow Yellow */
34 | .hljs-attribute {
35 | color: #eab700;
36 | }
37 |
38 | /* Tomorrow Green */
39 | .hljs-string,
40 | .hljs-symbol,
41 | .hljs-bullet,
42 | .hljs-addition {
43 | color: #718c00;
44 | }
45 |
46 | /* Tomorrow Blue */
47 | .hljs-title,
48 | .hljs-section {
49 | color: #4271ae;
50 | }
51 |
52 | /* Tomorrow Purple */
53 | .hljs-keyword,
54 | .hljs-selector-tag {
55 | color: #8959a8;
56 | }
57 |
58 | .hljs {
59 | display: block;
60 | overflow-x: auto;
61 | background: white;
62 | color: #4d4d4c;
63 | padding: 0.5em;
64 | }
65 |
66 | .hljs-emphasis {
67 | font-style: italic;
68 | }
69 |
70 | .hljs-strong {
71 | font-weight: bold;
72 | }
--------------------------------------------------------------------------------
/example/template/assets/main.css:
--------------------------------------------------------------------------------
1 | body {
2 | text-align: center;
3 | }
4 |
5 | div.content {
6 | display: inline-block;
7 | width: 50%;
8 | text-align: left;
9 | }
10 |
11 | div.content a:hover, a:visited, a:link, a:active {
12 | color: black;
13 | }
--------------------------------------------------------------------------------
/example/template/default.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{#if blog.isHome}}
5 | {{blog.title}}
6 | {{else}}
7 | {{#if page.curPage}}
8 | {{blog.title}} - {{page.curPage}}
9 | {{else}}
10 | {{page.post.title}}
11 | {{/if}}
12 | {{/if}}
13 |
14 |
15 |
16 |
17 |
18 | {{{content}}}
19 |
20 |
--------------------------------------------------------------------------------
/example/template/index.hbs:
--------------------------------------------------------------------------------
1 | {{blog.title}}
2 |
3 |
4 | {{#each posts}}
5 | - {{title}}
6 | {{/each}}
7 |
8 |
9 |
10 | {{#unless firstPage}}
11 | Prev
12 | {{/unless}}
13 | {{#unless lastPage}}
14 | Next
15 | {{/unless}}
--------------------------------------------------------------------------------
/example/template/post.hbs:
--------------------------------------------------------------------------------
1 | {{blog.title}}
2 |
3 |
{{date post.date "MMMM Do YYYY"}}
4 | {{#if post.tags}}
5 |
{{#each post.tags}}{{this}}{{/each}}
6 | {{/if}}
7 |
{{{post.content}}}
8 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | require('./lib/typeblog')
--------------------------------------------------------------------------------
/lib/utils/async.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /// provides the async helper functionality
4 |
5 | var alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_';
6 |
7 | function genId() {
8 | var res = '';
9 | for (var i = 0; i < 8; ++i) {
10 | res += alphabet[Math.floor(Math.random() * alphabet.length)];
11 | }
12 | return res;
13 | }
14 |
15 | // global baton which contains the current
16 | // set of deferreds
17 | var waiter;
18 |
19 | function Waiter() {
20 | var self = this;
21 | // found values
22 | self.values = {};
23 | // callback when done
24 | self.callback = null;
25 | self.resolved = false;
26 | self.count = 0;
27 | }
28 |
29 | Waiter.prototype.wait = function() {
30 | var self = this;
31 | ++self.count;
32 | };
33 |
34 | // resolve the promise
35 | Waiter.prototype.resolve = function(name, val) {
36 | var self = this;
37 | self.values[name] = val;
38 | // done with all items
39 | if (--self.count === 0) {
40 | self.resolved = true;
41 | // we may not have a done callback yet
42 | if (self.callback) {
43 | self.callback(self.values);
44 | }
45 | }
46 | };
47 |
48 | // sets the done callback for the waiter
49 | // notifies when the promise is complete
50 | Waiter.prototype.done = function(fn) {
51 | var self = this;
52 |
53 | self.callback = fn;
54 | if (self.resolved) {
55 | fn(self.values);
56 | // free mem
57 | Object.keys(self.values).forEach(function(id) {
58 | self.values[id] = null;
59 | });
60 | }
61 | };
62 |
63 | // callback fn when all async helpers have finished running
64 | // if there were no async helpers, then it will callback right away
65 | Waiter.done = function(fn) {
66 | // no async things called
67 | if (!waiter) {
68 | return fn({});
69 | }
70 | waiter.done(fn);
71 | // clear the waiter for the next template
72 | waiter = undefined;
73 | };
74 |
75 | Waiter.resolve = function(fn, context) {
76 | // we want to do async things, need a waiter for that
77 | if (!waiter) {
78 | waiter = new Waiter();
79 | }
80 | var id = '__aSyNcId_<_' + genId() + '__';
81 | var curWaiter = waiter;
82 | waiter.wait();
83 | fn(context, function(res) {
84 | curWaiter.resolve(id, res);
85 | });
86 | // return the id placeholder, which is replaced later
87 | return id;
88 | };
89 |
90 | module.exports = Waiter;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "typeblog",
3 | "version": "1.0.0",
4 | "description": "Yet another blogging platform (written initially for typeblog.net)",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "prepublish": "coffee -o lib -c src",
9 | "debug": "coffee -o lib -c src && cd example && rm -rf node_modules && npm install && NODE_PATH=./node_modules ../bin/typeblog"
10 | },
11 | "bin": {
12 | "typeblog": "./bin/typeblog"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/PeterCxy/Typeblog.git"
17 | },
18 | "keywords": [
19 | "blog"
20 | ],
21 | "author": "PeterCxy",
22 | "license": "WTFPL",
23 | "bugs": {
24 | "url": "https://github.com/PeterCxy/Typeblog/issues"
25 | },
26 | "homepage": "https://github.com/PeterCxy/Typeblog#readme",
27 | "dependencies": {
28 | "bluebird": "^3.4.1",
29 | "chokidar": "^1.6.0",
30 | "coffee-script": "^1.10.0",
31 | "express": "^4.14.0",
32 | "handlebars": "^4.0.5",
33 | "md5-file": "^3.1.1",
34 | "moment": "^2.14.1",
35 | "rss": "^1.2.1",
36 | "striptags": "^2.1.1"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/plugins/highlight/.npmignore:
--------------------------------------------------------------------------------
1 | index.coffee
--------------------------------------------------------------------------------
/plugins/highlight/index.coffee:
--------------------------------------------------------------------------------
1 | {Plugin, dependencies} = require 'plugin'
2 | {Promise} = dependencies
3 | highlight = require 'highlight.js'
4 |
5 | class HighlighterPlugin extends Plugin
6 | highlight: (code, lang) ->
7 | promise = Promise.try ->
8 | if lang?
9 | return highlight.highlight(lang, code).value
10 | else
11 | return highlight.highlightAuto(code).value
12 |
13 | return [true, promise]
14 |
15 | module.exports = new HighlighterPlugin
--------------------------------------------------------------------------------
/plugins/highlight/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "typeblog-highlight",
3 | "version": "1.0.0",
4 | "description": "Code highlighting plugin for Typeblog",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "prepublish": "coffee -c index.coffee"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/PeterCxy/Typeblog.git"
13 | },
14 | "keywords": [
15 | "highlight"
16 | ],
17 | "author": "PeterCxy",
18 | "license": "WTFPL",
19 | "bugs": {
20 | "url": "https://github.com/PeterCxy/Typeblog/issues"
21 | },
22 | "homepage": "https://github.com/PeterCxy/Typeblog#readme",
23 | "devDependencies": {
24 | "coffee-script": "^1.10.0"
25 | },
26 | "dependencies": {
27 | "highlight.js": "^9.5.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/plugins/markdown/.npmignore:
--------------------------------------------------------------------------------
1 | index.coffee
--------------------------------------------------------------------------------
/plugins/markdown/index.coffee:
--------------------------------------------------------------------------------
1 | {Plugin, dependencies, callPluginMethod} = require 'plugin'
2 | {Promise} = dependencies
3 | marked = require 'marked'
4 |
5 | marked.setOptions highlight: (code, lang, cb) ->
6 | callPluginMethod 'highlight', [code, lang]
7 | .then (result) -> cb null, result
8 | .catch (err) -> cb null, code
9 |
10 | class MarkdownPlugin extends Plugin
11 | parseContentMarkdown: (content) ->
12 | promise = new Promise (resolve, reject) ->
13 | marked content, (err, result) ->
14 | if err?
15 | reject err
16 | else
17 | resolve result
18 |
19 | return [true, promise]
20 |
21 | highlight: (code, lang) ->
22 | return [true, Promise.try ->
23 | return code
24 | ]
25 |
26 | module.exports = new MarkdownPlugin
--------------------------------------------------------------------------------
/plugins/markdown/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "typeblog-markdown",
3 | "version": "1.0.0",
4 | "description": "Markdown parser for Typeblog",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "prepublish": "coffee -c index.coffee"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/PeterCxy/Typeblog.git"
13 | },
14 | "keywords": [
15 | "markdown",
16 | "typeblog"
17 | ],
18 | "author": "PeterCxy",
19 | "license": "WTFPL",
20 | "bugs": {
21 | "url": "https://github.com/PeterCxy/Typeblog/issues"
22 | },
23 | "homepage": "https://github.com/PeterCxy/Typeblog#readme",
24 | "devDependencies": {
25 | "coffee-script": "^1.10.0"
26 | },
27 | "dependencies": {
28 | "marked": "^0.3.5"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/plugin/default.coffee:
--------------------------------------------------------------------------------
1 | {Plugin, dependencies} = require 'plugin'
2 | {Promise, fs} = dependencies
3 |
4 | class DefaultPlugin extends Plugin
5 | constructor: ->
6 | # The default one need not to be registered
7 |
8 | transformExpressApp: (app) ->
9 | # Do nothing by default
10 | return [true, Promise.try ->
11 | # ???
12 | ]
13 |
14 | transformRenderResult: (content) ->
15 | # Do nothing by default
16 | return [true, Promise.try ->
17 | content
18 | ]
19 |
20 | loadPost: (file) ->
21 | promise = fs.readFileAsync file
22 | .then (buf) -> buf.toString()
23 | return [true, promise]
24 |
25 | parsePost: (content) ->
26 | end = content.indexOf '```\n'
27 | return [false, null] if not (content.startsWith('```json') and end > 0)
28 | start = '```json'.length + 1
29 |
30 | promise = Promise.try ->
31 | json = content[start...end]
32 | data = JSON.parse json
33 | data.content = content[end + '```'.length...].trim()
34 | return data
35 | .then (data) ->
36 | if not (data.title? and data.date?)
37 | throw new Error 'You must provide at least `title` and `date`'
38 | if not data.parser?
39 | data.parser = 'Default'
40 | if not data.url?
41 | data.url = encodeURIComponent data.title
42 | if not data.template?
43 | data.template = "post"
44 | return data
45 | .then (data) ->
46 | data.date = new Date data.date
47 | return data
48 |
49 | return [true, promise]
50 |
51 | parseContentDefault: (content) ->
52 | promise = Promise.try ->
53 | return content # Do no change on the content
54 | return [true, promise]
55 |
56 | module.exports = new DefaultPlugin()
--------------------------------------------------------------------------------
/src/plugin/plugin.coffee:
--------------------------------------------------------------------------------
1 | require.cache['plugin'] = module # Enable this to be directly required
2 | Module = require 'module'
3 | realResolve = Module._resolveFilename
4 | Module._resolveFilename = (request, parent) ->
5 | if request is 'plugin'
6 | return 'plugin'
7 | realResolve request, parent
8 |
9 | {Promise} = require '../utils/dependencies'
10 |
11 | class Plugin
12 | constructor: ->
13 | registerPlugin @
14 |
15 | plugins = []
16 | registerPlugin = (plugin) ->
17 | plugins.push plugin
18 | loadPlugins = (config) ->
19 | return if not config.plugins?
20 | config.plugins.forEach (it) ->
21 | if it.startsWith 'npm://'
22 | require it.replace 'npm://', ''
23 | else
24 | require "#{process.cwd()}/#{it}"
25 | # To enable chaining, please return [false, function] in the plugin method
26 | # Otherwise chaining will be disabled.
27 | # When allowChaining is true, the plugin method will receive the original
28 | # arguments as its own arguments. To get the result from the last plugin,
29 | # please use the arguments passed to [function] returned by the method.
30 | # See example/plugins/ for details.
31 | callPluginMethod = (name, args, allowChaining = false) ->
32 | lastPromise = null
33 | for p in plugins
34 | if p[name]? and (typeof p[name] is 'function')
35 | [ok, promise] = p[name].apply @, args
36 | return promise if ok
37 | if promise? and allowChaining
38 | throw new Error 'Not returning a function for chaining plugins' if typeof promise isnt 'function'
39 | if lastPromise?
40 | lastPromise = lastPromise.then promise
41 | else
42 | lastPromise = Promise.try =>
43 | promise.apply @, args
44 | if (not allowChaining) or (not lastPromise?)
45 | [ok, promise] = defaultPlugin[name].apply @, args
46 | return promise if ok
47 | throw new Error "No plugin for #{name} found"
48 | else
49 | return lastPromise
50 | loadPost = (name) ->
51 | callPluginMethod 'loadPost', arguments
52 | parsePost = (content) ->
53 | callPluginMethod 'parsePost', arguments
54 | transformExpressApp = (app) ->
55 | callPluginMethod 'transformExpressApp', arguments, true # Allow chaining plugins
56 | transformRenderResult = (content) ->
57 | callPluginMethod 'transformRenderResult', arguments, true # Allow chaining plugins
58 |
59 | module.exports =
60 | registerPlugin: registerPlugin
61 | loadPlugins: loadPlugins
62 | callPluginMethod: callPluginMethod
63 | loadPost: loadPost
64 | parsePost: parsePost
65 | transformExpressApp: transformExpressApp
66 | transformRenderResult: transformRenderResult
67 | Plugin: Plugin
68 | dependencies: require '../utils/dependencies'
69 | configuration: require '../utils/configuration'
70 |
71 | defaultPlugin = require './default'
--------------------------------------------------------------------------------
/src/server.coffee:
--------------------------------------------------------------------------------
1 | {Promise, fs} = require './utils/dependencies'
2 | striptags = require 'striptags'
3 | {loadPlugins, callPluginMethod, loadPost, parsePost, transformExpressApp} = require './plugin/plugin'
4 | express = require 'express'
5 | RSS = require 'rss'
6 | {renderIndex, renderPost} = require './template'
7 | configuration = require './utils/configuration'
8 | configuration.on 'change', (config) ->
9 | checkConfig config
10 |
11 | posts = {}
12 | postsArr = []
13 | postsByTags = {}
14 | feed = null
15 |
16 | # Cache the rendering results
17 | cache = {}
18 | setupCache = (app) ->
19 | app.use (req, res, next) ->
20 | if req.method isnt 'GET'
21 | next()
22 | else if cache[req.path]?
23 | res.send cache[req.path] # Cache hit!
24 | else
25 | _send = res.send.bind res
26 | res.send = (body) ->
27 | cache[req.path] = body
28 | _send body
29 | next()
30 |
31 | clearCache = -> cache = {}
32 |
33 | start = ->
34 | return if not checkConfig configuration.config
35 | app = express()
36 | app.use '/assets', express.static 'template/assets'
37 | setupCache app
38 | app.get '/', (req, res) ->
39 | renderIndex postsArr, 0
40 | .then (index) -> res.send index
41 | app.get '/page/:id(\\d+)', (req, res) ->
42 | promise = renderIndex postsArr, parseInt req.params.id
43 | if not promise?
44 | res.sendStatus 404
45 | else
46 | promise.then (page) -> res.send page
47 | app.get '/tag/:name', (req, res) ->
48 | if not postsByTags[req.params.name]?
49 | res.sendStatus 404
50 | else
51 | renderIndex postsByTags[req.params.name], 0, "/tag/#{req.params.name}/"
52 | .then (index) -> res.send index
53 | app.get '/tag/:name/page/:id(\\d+)', (req, res) ->
54 | if not postsByTags[req.params.name]?
55 | res.sendStatus 404
56 | else
57 | promise = renderIndex postsByTags[req.params.name], parseInt req.params.id, "/tag/#{req.params.name}/"
58 | if not promise?
59 | res.sendStatus 404
60 | else
61 | promise.then (page) -> res.send page
62 | app.get '/rss/', (req, res) ->
63 | if feed?
64 | res.set('Content-Type', 'application/rss+xml')
65 | res.send feed.xml indent: true
66 | else
67 | res.sendStatus 404
68 |
69 | # Allow transforming before we set up the wildcard rule
70 | transformExpressApp app
71 | .then ->
72 | app.get '/*', (req, res) ->
73 | if not req.path.endsWith '/'
74 | res.redirect 301, req.path + '/'
75 | return
76 | postName = req.params[0].replace(/\/$/, '')
77 | if posts[postName]?
78 | renderPost posts[postName]
79 | .then (content) -> res.send content
80 | else
81 | res.sendStatus 404
82 |
83 | app.listen configuration.config.port, '127.0.0.1', ->
84 | console.log "Listening on 127.0.0.1:#{configuration.config.port}"
85 |
86 | checkConfig = (config) ->
87 | if not (config.title? and config.url? and config.description?)
88 | console.error 'Please provide at least `title` `url` and `description`'
89 | return false
90 | if not config.posts? or config.posts.length is 0
91 | console.error 'No posts found'
92 | return false
93 | if not config.port?
94 | config.port = "2333"
95 | if isNaN parseInt config.port
96 | console.error "Invalid port #{config.port}"
97 | return false
98 | if not config.posts_per_page?
99 | config.posts_per_page = 5
100 | if isNaN parseInt config.posts_per_page
101 | console.error "Invalid number #{config.posts_per_page}"
102 | return false
103 | loadPlugins config
104 | reloadPosts()
105 | true
106 |
107 | reloadPosts = ->
108 | newPosts = {}
109 | newPostsByTags = {}
110 | Promise.map configuration.config.posts, (item) ->
111 | loadPost item
112 | .map parsePost
113 | .map (data) ->
114 | callPluginMethod "parseContent#{data.parser}", [data.content]
115 | .then (content) ->
116 | data.content = content
117 | data.contentStripped = striptags content
118 | return data
119 | .each (item) ->
120 | newPosts[item.url] = item
121 |
122 | if item.tags? and item.tags.length > 0
123 | item.tags.forEach (it) ->
124 | if not newPostsByTags[it]?
125 | newPostsByTags[it] = []
126 | newPostsByTags[it].push item
127 | .all()
128 | .then ->
129 | posts = newPosts
130 | postsArr = (post for _, post of posts when not post.hide)
131 | postsByTags = newPostsByTags
132 |
133 | feed = new RSS
134 | title: configuration.config.title
135 | description: configuration.config.description
136 | site_url: configuration.config.url
137 | feed_url: configuration.config.url + "/rss/"
138 |
139 | arr = postsArr
140 | if arr.length > 5
141 | arr = arr[0..4]
142 | arr.forEach (it) ->
143 | feed.item
144 | title: it.title
145 | description: it.content
146 | date: it.date
147 | url: configuration.config.url + "/" + it.url
148 | .then -> clearCache()
149 | .catch (e) ->
150 | console.error e
151 | process.exit 1
152 |
153 | module.exports =
154 | start: start
--------------------------------------------------------------------------------
/src/template.coffee:
--------------------------------------------------------------------------------
1 | # Template renderer
2 | handlebars = require 'handlebars'
3 | async = require './utils/async' # From express-hbs
4 | md5 = require 'md5-file'
5 | moment = require 'moment'
6 | chokidar = require 'chokidar'
7 | configuration = require './utils/configuration'
8 | {Promise, fs} = require './utils/dependencies'
9 | {transformRenderResult} = require './plugin/plugin'
10 |
11 | # Hack from express-hbs
12 | handlebars.registerAsyncHelper = (name, fn) ->
13 | @registerHelper name, (context, options) ->
14 | if options && fn.length > 2
15 | resolver = (arr, cb) ->
16 | fn.call @, arr[0], arr[1], cb
17 | return async.resolve resolver.bind(@), [context, options]
18 | return async.resolve fn.bind(@), context
19 |
20 | handlebars.registerAsyncHelper 'asset', (name, cb) ->
21 | md5 "./template/assets/#{name}", (err, hash) ->
22 | throw err if err
23 | cb new handlebars.SafeString "/assets/#{name}?v=#{hash[0..7]}"
24 |
25 | handlebars.registerHelper 'date', (date, format) ->
26 | return moment(date).format format
27 |
28 | handlebars.registerHelper 'tag', (name) ->
29 | return "/tag/#{name}"
30 |
31 | handlebars.registerHelper 'shorten', (content, len) ->
32 | if len >= content.length
33 | return content
34 | else
35 | return content[0..len]
36 |
37 | chokidar.watch './template'
38 | .on 'all', -> reload()
39 |
40 | template = {}
41 |
42 | reload = ->
43 | fs.readdirAsync './template/partials'
44 | .filter (file) -> file.endsWith '.hbs'
45 | .each (file) ->
46 | fs.readFileAsync "./template/partials/#{file}"
47 | .then (t) ->
48 | handlebars.registerPartial file, handlebars.compile t.toString()
49 | .all()
50 | .catch (e) -> ''
51 | .then -> fs.readdirAsync './template'
52 | .filter (file) -> file.endsWith '.hbs'
53 | .each (file) ->
54 | fs.readFileAsync "./template/#{file}"
55 | .then (t) ->
56 | template[file.replace('.hbs', '')] = handlebars.compile t.toString()
57 | .catch (e) -> ''
58 | .all()
59 | .then ->
60 | if not (template.default? and template.index?)
61 | throw new Error 'No default template provided'
62 |
63 | buildBlogContext = (isHome = false) ->
64 | title: configuration.config.title
65 | description: configuration.config.description
66 | url: configuration.config.url
67 | isHome: isHome
68 |
69 | renderTemplate = (fn, context) ->
70 | ret = fn context
71 | new Promise (resolve) ->
72 | async.done (values) ->
73 | Object.keys(values).forEach (id) ->
74 | ret = ret.replace id, values[id]
75 | ret = ret.replace handlebars.Utils.escapeExpression(id), handlebars.Utils.escapeExpression(values[id])
76 | resolve ret
77 |
78 | renderDefault = (content, pageContext, isHome = false) ->
79 | context =
80 | blog: buildBlogContext(isHome)
81 | arguments: configuration.config.template_arguments
82 | content: content
83 | page: pageContext
84 |
85 | renderTemplate template.default, context
86 | .then transformRenderResult
87 |
88 | renderIndex = (posts, page = 0, baseURL = "/") ->
89 | context =
90 | blog: buildBlogContext(page is 0)
91 | arguments: configuration.config.template_arguments
92 | firstPage: page is 0
93 | lastPage: false
94 | nextPage: "#{baseURL}page/#{page + 1}"
95 | prevPage: if page == 1 then "/" else "/page/#{page - 1}"
96 | curPage: page
97 | totalPages = Math.floor(posts.length / configuration.config.posts_per_page)
98 | if totalPages * configuration.config.posts_per_page != posts.length
99 | totalPages += 1
100 | return null if page >= totalPages
101 | start = page * configuration.config.posts_per_page
102 | end = (page + 1) * configuration.config.posts_per_page - 1
103 | if end >= posts.length
104 | end = posts.length - 1
105 | context.lastPage = true
106 | context.posts = posts[start..end]
107 | renderTemplate template.index, context
108 | .then (index) -> renderDefault index, context, (page is 0)
109 |
110 | renderPost = (post) ->
111 | context =
112 | blog: buildBlogContext false
113 | arguments: configuration.config.template_arguments
114 | post: post
115 | renderTemplate template[post.template], context
116 | .then (content) -> renderDefault content, context, false
117 |
118 | module.exports =
119 | reload: reload
120 | renderIndex: renderIndex
121 | renderPost: renderPost
--------------------------------------------------------------------------------
/src/typeblog.coffee:
--------------------------------------------------------------------------------
1 | {fs} = require './utils/dependencies'
2 |
3 | # Check for the existence of config.json in the current working directory
4 | fs.statAsync "./config.json"
5 | .then (stats) ->
6 | fs.statAsync "./template/index.hbs"
7 | .then (stats) ->
8 | # Load the templates
9 | require('./template').reload()
10 | # File exists, load and serve the blog.
11 | configuration = require('./utils/configuration')
12 | configuration.once 'change', ->
13 | require('./server').start()
14 | configuration.reload()
15 | .catch (e) ->
16 | console.error e
17 | console.error 'Unable to stat config.json or template/index.hbs in the current working directory. Make sure this is a folder with a Typeblog structure.'
--------------------------------------------------------------------------------
/src/utils/configuration.coffee:
--------------------------------------------------------------------------------
1 | {fs} = require './dependencies'
2 | {EventEmitter} = require 'events'
3 | chokidar = require 'chokidar'
4 |
5 | class Configuration extends EventEmitter
6 | constructor: ->
7 | @config = {}
8 | chokidar.watch './config.json'
9 | .on 'change', => @reload()
10 |
11 | reload: ->
12 | fs.readFileAsync './config.json'
13 | .then JSON.parse
14 | .then (config) => @config = config
15 | .then => @emit 'change', @config
16 | .catch (e) ->
17 | console.error e
18 | process.exit 1
19 |
20 | module.exports = new Configuration()
--------------------------------------------------------------------------------
/src/utils/dependencies.coffee:
--------------------------------------------------------------------------------
1 | # This file contains the common dependencies for all modules in this project
2 | # Allow directly requiring .coffee files for extensions
3 | require 'coffee-script/register'
4 |
5 | # The Promise implemetation
6 | Promise = require 'bluebird'
7 |
8 | module.exports =
9 | Promise: Promise
10 | fs: Promise.promisifyAll require 'fs'
--------------------------------------------------------------------------------