├── .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 | 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' --------------------------------------------------------------------------------