1 | cp = require 'child_process'
|
2 | fs = require 'fs'
|
3 | _ = require 'underscore'
|
4 | _.str = require 'underscore.string'
|
5 | async = require 'async'
|
6 |
|
7 | CONTENT_TYPES = require './content'
|
8 |
|
9 | class Finder
|
10 | constructor: ( @site, kind, cb ) ->
|
11 | site = @site
|
12 | kind = _.str.rtrim( kind, 's' )
|
13 | klass = _.str.capitalize(kind)
|
14 | @["_get#{klass}Paths"] ( err, filenames ) ->
|
15 | # filter the paths to exclude . and special files, etc
|
16 | filtered = _.reject( filenames || [], ( f ) -> f[0] == '.' || f[0] == '_' )
|
17 | # generate Content instances for each path
|
18 | contentGenerator = ( path, done ) ->
|
19 | content = new CONTENT_TYPES[klass]( site, path )
|
20 | content.process( done )
|
21 | async.map( filtered, contentGenerator, cb )
|
22 | _getStaticPaths: ( cb ) ->
|
23 | fs.readdir "#{@site.root}/_static", ( err, statics ) ->
|
24 | cb( err, statics || [] )
|
25 | _getStylePaths: ( cb ) ->
|
26 | fs.readdir "#{@site.root}/_styles", ( err, styles ) ->
|
27 | cb( err, styles || [] )
|
28 | _getScriptPaths: ( cb ) ->
|
29 | fs.readdir "_scripts", ( err, scripts ) ->
|
30 | cb( err, scripts || [] )
|
31 | _getPostPaths: ( cb ) ->
|
32 | fs.readdir "#{@site.root}/_posts", ( err, listings ) ->
|
33 | # error is ok, no posts.
|
34 | cb( null, listings || [] )
|
35 | _getPagePaths: ( cb ) ->
|
36 | site = @site
|
37 | paths = []
|
38 | processor = ( listing, done ) ->
|
39 | if listing == null
|
40 | done()
|
41 | return
|
42 | fs.stat "#{site.root}/_pages/#{listing}", ( err, stat ) ->
|
43 | if err
|
44 | Logger.error( "Could not stat file #{listing}" )
|
45 | done()
|
46 | else if stat.isFile()
|
47 | paths.push listing
|
48 | done()
|
49 | else
|
50 | fs.readdir "#{site.root}/_pages/#{listing}", ( err2, sublistings ) ->
|
51 | sublistings.forEach ( sublisting ) ->
|
52 | q.push( "#{listing}/#{sublisting}" )
|
53 | done()
|
54 |
|
55 | q = async.queue processor, 1
|
56 |
|
57 | fs.readdir "#{@site.root}/_pages", ( err, listings ) ->
|
58 | # again, error is ok
|
59 | (listings || []).forEach ( listing ) -> q.push( listing )
|
60 |
|
61 | q.push( null ) # kick off queue, even if there are no posts
|
62 | q.drain = () -> cb( null, paths );
|
63 |
|
64 | module.exports = Finder
|
65 |
|
66 | ###
|
67 | Builder = exports
|
68 |
|
69 | CWD = process.cwd()
|
70 |
|
71 | jade.filters.plain = ( b, c ) ->
|
72 | return b.toString()
|
73 |
|
74 | globalInfo = {}
|
75 |
|
76 | Builder.reset = () ->
|
77 | globalInfo.PAGE_INFO = {}
|
78 | globalInfo.POST_INFO = {}
|
79 |
|
80 | sortedPosts = () ->
|
81 | _(globalInfo.POST_INFO).chain()
|
82 | .values()
|
83 | .sortBy( (v) -> return v.timestamp.getTime() )
|
84 | .reverse()
|
85 | .value()
|
86 |
|
87 | Builder.addInfo = ( path, kind, info ) ->
|
88 | globalInfo["#{kind.toUpperCase()}_INFO"][path] = info
|
89 | Builder.getInfo = ( path, kind ) ->
|
90 | globalInfo["#{kind.toUpperCase()}_INFO"][path]
|
91 |
|
92 |
|
93 | renderers = {}
|
94 | innerContent = ( meta ) ->
|
95 | type = meta.extension
|
96 | switch type
|
97 | when 'md'
|
98 | meta.preprocessed = ejs.render( meta.src, locals: meta )
|
99 | renderers[type] ?= require( 'node-markdown' ).Markdown
|
100 | return renderers[type]( meta.preprocessed )
|
101 | when 'jade'
|
102 | renderers[type] ?= jade
|
103 | return renderers[type].compile( meta.src )(meta)
|
104 | when 'ejs'
|
105 | return ejs.render( meta.src, locals: meta )
|
106 |
|
107 | Builder.render = ( path, kind, pathOverride=null ) ->
|
108 | meta = _.clone(Builder.getInfo( path, kind ))
|
109 | dir = "#{CWD}/build/#{meta.permalink}"
|
110 | mkdir_p dir, 0777, ( err ) ->
|
111 | if err
|
112 | Logger.error "Error in mkdir_p - #{dir} - #{err}"
|
113 | return
|
114 | Logger.debug "Processing #{meta.permalink}"
|
115 | meta.posts = sortedPosts()
|
116 | meta.pages = _.values(globalInfo.PAGE_INFO)
|
117 | meta.config = Config
|
118 | meta._ = _
|
119 | meta.h = _.reduce(
|
120 | _.keys( Config.helpers||{} ),
|
121 | (
|
122 | (uh, key) ->
|
123 | uh[key] = _.bind( Config.helpers[key], meta )
|
124 | return uh
|
125 | ),
|
126 | {
|
127 | moment: moment
|
128 | innerContent: innerContent
|
129 | }
|
130 | )
|
131 |
|
132 | try
|
133 | meta.content = innerContent( meta )
|
134 | catch e
|
135 | Logger.error "Could not process file: #{meta.permalink} - #{e}, #{e.stack}"
|
136 | return
|
137 |
|
138 | writeFile = ( dest, html ) ->
|
139 | fs.writeFile dest, html.toString(), ( err ) ->
|
140 | Logger.error "Error writing final render #{err}" if err
|
141 | Logger.debug "Wrote #{dest}"
|
142 |
|
143 | dest = pathOverride || "#{meta.permalink}/index.html"
|
144 | dest = "#{CWD}/build/#{dest}"
|
145 |
|
146 | if meta.layout == false
|
147 | writeFile( dest, meta.content )
|
148 | else
|
149 | fs.readFile "#{CWD}/_inc/#{meta.layout||'layout'}.jade", ( err, layout ) ->
|
150 | Logger.error( meta.permalink, err, err.stack ) if err
|
151 | tmpl = jade.compile layout.toString()
|
152 | html = tmpl( meta, (err) -> Logger.error("OMG#{err}") if err )
|
153 | writeFile( dest, html )
|
154 |
|
155 | contentList = ( filenames ) ->
|
156 | _.reject( filenames || [], ( f ) -> f[0] == '.' || f[0] == '_' )
|
157 |
|
158 | # find all pages
|
159 | Builder.buildSite = () ->
|
160 | findFunctions =
|
161 | posts: ( cb ) ->
|
162 | fs.readdir "#{CWD}/_posts", ( err, listings ) ->
|
163 | # error is ok, no posts.
|
164 | cb( null, listings || [] )
|
165 | pages: ( cb ) ->
|
166 | paths = []
|
167 | processor = ( listing, done ) ->
|
168 | if listing == null
|
169 | done()
|
170 | return
|
171 | fs.stat "#{CWD}/_pages/#{listing}", ( err, stat ) ->
|
172 | if err
|
173 | Logger.error( "Could not stat file #{listing}" )
|
174 | done()
|
175 | else if stat.isFile()
|
176 | paths.push listing
|
177 | done()
|
178 | else
|
179 | fs.readdir "#{CWD}/_pages/#{listing}", ( err2, sublistings ) ->
|
180 | sublistings.forEach ( sublisting ) ->
|
181 | q.push( "#{listing}/#{sublisting}" )
|
182 | done()
|
183 |
|
184 | q = async.queue processor, 1
|
185 |
|
186 | fs.readdir "#{CWD}/_pages", ( err, listings ) ->
|
187 | # again, error is ok
|
188 | (listings || []).forEach ( listing ) -> q.push( listing )
|
189 |
|
190 | q.push( null ) # kick off queue, even if there are no posts
|
191 | q.drain = () -> cb( null, paths );
|
192 |
|
193 | async.parallel findFunctions, ( err, all ) ->
|
194 | pages = contentList( all.pages )
|
195 | posts = contentList( all.posts )
|
196 | f = ( kind ) ->
|
197 | return ( thing ) ->
|
198 | meta = Builder.getInfo( thing, kind )
|
199 | if Config.DEV || Config.drafts || !meta.draft
|
200 | if meta.permalink == Config.index || meta.index
|
201 | Logger.debug " index: #{meta.permalink}"
|
202 | Builder.render( meta.thing, kind, 'index.html' )
|
203 | Logger.debug( " #{kind}: #{meta.permalink}" )
|
204 | Builder.render meta.thing, kind
|
205 | else
|
206 | Logger.debug( " skipped: #{thing}" )
|
207 | process = ( kind ) ->
|
208 | return ( thing ) ->
|
209 | return ( cb ) ->
|
210 | fs.readFile "#{CWD}/_#{kind}s/#{thing}", ( err, src ) ->
|
211 | Logger.error "Error reading source of #{thing} - #{err}" if err
|
212 | src = src.toString().split('---\n')
|
213 | meta = eval coffeescript.compile( src[0], { bare: true } )
|
214 | meta.scripts ?= []
|
215 | meta.styles ?= []
|
216 | meta.src = src.splice(1).join('---\n')
|
217 |
|
218 | pathParts = thing.split('.')
|
219 | meta.extension = pathParts.pop()
|
220 | pathParts = pathParts.pop().split('/')
|
221 |
|
222 | meta.filename = pathParts.pop()
|
223 | meta.path = pathParts.join('/')
|
224 | meta.path += '/' if meta.path
|
225 | meta.permalink = "#{meta.path}#{meta.filename}"
|
226 |
|
227 | meta.thing = "#{meta.permalink}.#{meta.extension}"
|
228 |
|
229 | if kind == 'post'
|
230 | dateFields = _.map(meta.date.split('.').reverse(), (f) -> parseInt(f, 10))
|
231 | meta.timestamp = new Date( dateFields[0], dateFields[1] - 1, dateFields[2] )
|
232 |
|
233 | meta.kind = kind
|
234 |
|
235 | # get any old info
|
236 | oldInfo = Builder.getInfo( meta.thing, kind )
|
237 |
|
238 | # port over info we populated on first run through
|
239 | meta.index = oldInfo.index if oldInfo
|
240 |
|
241 | # insert new info
|
242 | Builder.addInfo( meta.thing, kind, meta )
|
243 | cb() if cb
|
244 |
|
245 | pageProcess = process( 'page' )
|
246 | postProcess = process( 'post' )
|
247 |
|
248 | async.parallel(
|
249 | _.flatten( [
|
250 | _.map( pages, pageProcess ),
|
251 | _.map( posts, postProcess )
|
252 | ] ),
|
253 | ( err ) ->
|
254 | if not Config.index?
|
255 | # determine newest/index post
|
256 | newestThing = _.reject( sortedPosts(), (p) -> p.draft )[0].thing
|
257 | newestMeta = Builder.getInfo( newestThing, 'post' )
|
258 | newestMeta.index = true
|
259 | Builder.addInfo( newestThing, 'post', newestMeta )
|
260 | Logger.debug " index: #{newestMeta.permalink}"
|
261 | pages.forEach f('page')
|
262 | posts.forEach f('post')
|
263 | )
|
264 |
|
265 | if Config.DEV
|
266 | pages.forEach ( thing ) ->
|
267 | Logger.debug "Watching #{thing}"
|
268 | Watcher.onChange "_pages/#{thing}", () ->
|
269 | pageProcess( thing )( () -> f('page')( thing ) )
|
270 | posts.forEach ( thing ) ->
|
271 | Logger.debug "Watching #{thing}"
|
272 | Watcher.onChange "_posts/#{thing}", () ->
|
273 | postProcess( thing )( () -> f('post')( thing ) )
|
274 |
|
275 |
|
276 | Builder.compileStyles = () ->
|
277 | parser = new(less.Parser)(
|
278 | paths: [ process.cwd() + '/_styles' ]
|
279 | )
|
280 | f = ( style ) ->
|
281 | fs.readdir "_styles", ( err, styles ) ->
|
282 | fs.readFile "_styles/#{style}", ( err, css ) ->
|
283 | return if err
|
284 | fs.mkdir "build/css", 0777, ( err ) ->
|
285 | done = ( err, src ) ->
|
286 | if err
|
287 | Logger.error "Could not process style: #{style}, #{JSON.stringify(err,undefined,2)}"
|
288 | return
|
289 | #log.info "SRC: #{src.toString()}"
|
290 | fs.writeFile "build/css/#{style.replace(/less$/, 'css')}", src.toString(), ( err ) ->
|
291 | Logger.error "Error in style done callback #{err}" if err
|
292 | Logger.debug " style: #{style}"
|
293 |
|
294 | if style.match /less$/
|
295 | parser.parse( css.toString(), ( err, src ) ->
|
296 | if err
|
297 | done( err, src )
|
298 | return
|
299 | try
|
300 | result = src.toCSS( compress: true )
|
301 | done( null, result )
|
302 | catch err2
|
303 | done( err2, src )
|
304 | )
|
305 | else
|
306 | done( null, css+"" )
|
307 | fs.readdir "_styles", ( err, styles ) ->
|
308 | buildAll = () -> contentList(styles).forEach f
|
309 | buildAll()
|
310 | styles.forEach ( style ) ->
|
311 | Watcher.onChange "_styles/#{style}", buildAll
|
312 |
|
313 | Builder.compileScripts = () ->
|
314 | fs.readdir "_scripts", ( err, scripts ) ->
|
315 | contentList(scripts).forEach ( script ) ->
|
316 | f = () ->
|
317 | fs.readFile "_scripts/#{script}", ( err, coffee ) ->
|
318 | fs.mkdir "build/js", 0777, ( err ) ->
|
319 | src = if script.match( /coffee$/ ) then coffeescript.compile( coffee+"" ) else (coffee+"")
|
320 | fs.writeFile "build/js/#{script.replace(/coffee$/, 'js')}", src.toString(), ( err ) ->
|
321 | Logger.error "Error in script done callback #{err}" if err
|
322 | Logger.debug " script: #{script}"
|
323 | f()
|
324 | Watcher.onChange "_scripts/#{script}", f
|
325 |
|
326 | Builder.copyStatics = () ->
|
327 | # force delete anything that exists in data/static
|
328 | # and then based on env:
|
329 | # development: symlink anything in data/static to build
|
330 | # production: fs copy anything in data/static to build
|
331 | fs.readdir "_static", ( err, statics ) ->
|
332 | contentList(statics).forEach ( static ) ->
|
333 | cp.exec "rm -rf build/#{static}", ( err ) ->
|
334 | Logger.error "Error in static copy #{err}" if err
|
335 | if !Config.DEV
|
336 | src = "_static/#{static}"
|
337 | dest = "build/#{static}"
|
338 | cp.exec "cp -R #{src} #{dest}", ( err ) ->
|
339 | Logger.error " Could not write build/#{static} during generation." if err
|
340 | Logger.error arguments if err
|
341 | Logger.error err if err
|
342 | else
|
343 | fs.symlink "../_static/#{static}", "build/#{static}", ( err ) ->
|
344 | Logger.error "Error symlinking statics #{err}" if err
|
345 | ###
|