UNPKG

7.83 kBtext/coffeescriptView Raw
1# Hey-Coffee!
2# TJ Eastmond - tj.eastmond@gmail.com
3# Copyright 2013
4
5fs = require 'fs'
6path = require 'path'
7crypto = require 'crypto'
8http = require 'http'
9url = require 'url'
10async = require 'async'
11_ = require 'underscore'
12marked = require 'marked'
13handlebars = require 'handlebars'
14mkdirp = require 'mkdirp'
15rss = require 'rss'
16
17require 'date-utils'
18
19Hey = module.exports = class
20 constructor: () ->
21 @cwd = process.cwd() + "/"
22 @template = null
23 @cacheFile = "#{@cwd}hey-cache.json"
24 @configFile = "#{@cwd}hey-config.json"
25 @templateFile = "#{@cwd}template.html"
26 @pagesDir = "#{@cwd}pages/"
27 @siteDir = "#{@cwd}site/"
28 @rssFile = "#{@cwd}site/rss.xml"
29
30 init: ->
31 if fs.existsSync(@configFile) and fs.existsSync(@postPath())
32 console.log 'A blog is already setup here'
33 return false
34
35 mkdirp @siteDir
36 mkdirp @pagesDir
37 mkdirp @postPath()
38 defaults = @defaults()
39 fs.writeFileSync @cacheFile, '', 'utf8'
40 fs.writeFileSync @templateFile, defaults.tpl, 'utf8'
41 fs.writeFileSync @configFile, defaults.config, 'utf8'
42 fs.writeFileSync @postPath('first-post.md'), defaults.post, 'utf8'
43 yes
44
45 server: ->
46 do @loadConfig
47 server = http.createServer (req, res) =>
48 uri = url.parse(req.url).pathname
49 filename = path.join "#{@cwd}site", uri
50 return false if uri is '/favicon.ico'
51 fs.exists filename, (exists) ->
52 if not exists
53 res.writeHead 404, 'Content-Type': 'text/plain'
54 res.write '404 Not Found\n'
55 res.end()
56 false
57
58 filename += '/index.html' if fs.statSync(filename).isDirectory()
59
60 fs.readFile filename, 'binary', (error, file) ->
61 if error?
62 res.writeHead 500, 'Content-Type': 'text/plain'
63 res.write error + "\n"
64 res.end()
65 false
66
67 res.writeHead 200
68 res.write file, 'binary'
69 res.end()
70
71 server.listen 3000
72 console.log "Server running at http://localhost:3000"
73 console.log "CTRL+C to stop it"
74
75 publish: ->
76 do @loadConfig
77 @rsync @siteDir, @config.server
78
79 rsync: (from, to, callback) ->
80 {spawn} = require 'child_process'
81 port = "ssh -p #{@config.port or 22}"
82 child = spawn "rsync", ['-vurz', '--delete', '-e', port, from, to]
83 child.stdout.on 'data', (out) -> console.log out.toString()
84 child.stderr.on 'data', (err) -> console.error err.toString()
85 child.on 'exit', callback if callback
86
87 loadConfig: ->
88 @config = readJSON @configFile
89 yes
90
91 loadCache: ->
92 @cache = readJSON(@cacheFile) or []
93 yes
94
95 loadTemplate: ->
96 return true if @template?
97 @template = handlebars.compile fs.readFileSync(@templateFile).toString()
98 yes
99
100 postPath: (filename) ->
101 "#{@cwd}posts/#{filename or ''}"
102
103 postFiles: ->
104 readDir @postPath()
105
106 pageFiles: ->
107 readDir @pagesDir
108
109 setType: (post) ->
110 return post unless post.type
111 post["is#{ucWord post.type}"] = true
112 post
113
114 postInfo: (filename, isPage) ->
115 file = if isPage is true then "#{@pagesDir}#{filename}" else @postPath filename
116 content = fs.readFileSync(file).toString()
117 hash = md5 content
118 content = content.split '\n\n'
119 top = content.shift().split '\n'
120 post =
121 name: filename
122 title: top[0]
123 slug: path.basename filename, '.md'
124 hash: hash
125 body: @markup content.join '\n\n'
126 tags: []
127
128 for setting in top[2..]
129 parts = setting.split ': '
130 key = parts[0].toLowerCase()
131 post[key] = if key is 'tags'
132 parts[1].split(',').map((s) -> s.trim())
133 else
134 parts[1]
135
136 if post.published
137 date = new Date post.published
138 post.prettyDate = date.toFormat @config.prettyDateFormat
139 post.ymd = date.toFormat @config.ymdFormat
140 post.permalink = @permalink post.published, post.slug
141
142 if isPage is true
143 post.slug += '/'
144 post.type = 'page'
145
146 @setType post
147
148 update: (callback) ->
149 do @loadConfig
150 do @loadCache
151 cacheFiles = _.pluck @cache, 'name'
152 posts = @postFiles()
153
154 for post, i in @cache
155 current = @postInfo post.name
156 @cache[i] = current if post.hash isnt current.hash
157
158 @cache.push @postInfo(post) for post in posts when post not in cacheFiles
159 @cache = _.sortBy @cache, (post) -> (if post.published then new Date(post.published) else 0) * -1
160
161 fs.writeFileSync @cacheFile, JSON.stringify @cache
162
163 callback?()
164
165 yes
166
167 postDir: (pubDate, slug) ->
168 date = new Date pubDate
169 "#{@cwd}site/#{date.toFormat 'YYYY/MM/DD'}/#{slug}"
170
171 permalink: (pubDate, slug) ->
172 date = new Date pubDate
173 "/#{date.toFormat 'YYYY/MM/DD'}/#{slug}/"
174
175 build: (callback) ->
176 @update =>
177 for post in @cache when 'published' in _.keys post
178 dir = @postDir post.published, post.slug
179 mkdirp.sync dir unless fs.existsSync dir
180 fs.writeFileSync "#{dir}/index.html", @render([post]), 'utf8'
181 yes
182
183 do @buildIndex
184 do @buildTags
185
186 yes
187
188 buildIndex: (callback) ->
189 index = @config.postsOnHomePage - 1
190 posts = @cache[0..index].filter (p) -> 'published' in _.keys p
191 fs.writeFileSync "#{@cwd}site/index.html", @render(posts), 'utf8'
192 @buildRss posts
193 @buildPages()
194 yes
195
196 buildRss: (posts, callback) ->
197 feed = new rss
198 title: @config.siteTitle
199 description: @config.description
200 feed_url: "#{@config.site}/rss.xml"
201 site_url: @config.site
202 author: @config.author
203
204 for post in posts
205 feed.item
206 title: post.title
207 description: post.body
208 url: "#{@config.site}#{post.permalink}"
209 date: post.published
210
211 fs.writeFileSync @rssFile, feed.xml(), 'utf8'
212
213 callback?()
214
215 yes
216
217 buildPages: (callback) ->
218 for page in @pageFiles()
219 data = @postInfo page, yes
220 pageDir = "#{@siteDir}#{data.slug}"
221 mkdirp pageDir
222 fs.writeFileSync "#{pageDir}index.html", @render([data]), 'utf8'
223
224 buildTags: (callback) ->
225 @tags = {}
226 for post in @cache when post.tags.length > 0
227 for tag in post.tags
228 @tags[tag] = [] unless _.has @tags, tag
229 @tags[tag].push post
230
231 for tag, posts of @tags
232 tagDir = "#{@siteDir}tags/#{tag}/"
233 mkdirp tagDir unless fs.existsSync tagDir
234 fs.writeFileSync "#{tagDir}index.html", @render(posts), 'utf8'
235
236 callback?()
237
238 markup: (content) ->
239 content = marked(content).trim()
240 content.replace /\n/g, ""
241
242 render: (posts) ->
243 throw "Posts must be an array" unless _.isArray posts
244 do @loadTemplate
245 options = _.omit @config, 'server'
246 options.siteTitle = @pageTitle if posts.length is 1 then posts[0].title else ''
247 html = @template _.extend options, posts: posts
248 html.replace /\n|\r|\t/g, ''
249
250 pageTitle: (postTitle) ->
251 if postTitle then "#{postTitle} | #{@config.siteTitle}" else @config.siteTitle
252
253 defaults: ->
254 config = [
255 '{'
256 ' "siteTitle": "Hey, Coffee! Jack!",'
257 ' "author": "Si Rob",'
258 ' "description": "My awesome blog, JACK!",'
259 ' "site": "http://yoursite.com",'
260 ' "postsOnHomePage": 20,'
261 ' "server": "user@yoursite.com:/path/to/your/blog",'
262 ' "port": 22,'
263 ' "prettyDateFormat": "DDDD, DD MMMM YYYY",'
264 ' "ymdFormat": "YYYY-MM-DD"'
265 '}'
266 ].join '\n'
267
268 post = [
269 'First Post'
270 '=========='
271 'Published: 2012-03-27 12:00:00'
272 'Type: text'
273 ''
274 'This is a test post.'
275 ''
276 'This is a second paragraph.'
277 ].join '\n'
278
279 tpl = [
280 '<!DOCTYPE html>'
281 '<html>'
282 ' <head>'
283 ' <title>{{siteTitle}}</title>'
284 ' </head>'
285 ' <body>'
286 ' {{#each posts}}'
287 ' <div>'
288 ' <h2><a href="{{permalink}}">{{title}}</a></h2>'
289 ' {{{body}}}'
290 ' </div>'
291 ' {{/each}}'
292 ' </body>'
293 '</html>'
294 ].join '\n'
295
296 {config, post, tpl}
297
298# utility functions
299readJSON = (file) ->
300 throw "JSON file doesn't exist: #{file}" unless fs.existsSync file
301 fileContents = fs.readFileSync(file).toString()
302 if fileContents then JSON.parse(fileContents) else []
303
304readDir = (dir) ->
305 throw "Directory doesn't exist: #{dir}" unless fs.existsSync dir
306 files = fs.readdirSync(dir).filter (f) -> f.charAt(0) isnt '.'
307 files or []
308
309md5 = (string) ->
310 crypto.createHash('md5').update(string).digest('hex')
311
312ucWord = (string) ->
313 string.charAt(0).toUpperCase() + string.slice 1
314
315