UNPKG

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