-
Notifications
You must be signed in to change notification settings - Fork 2
/
giles.coffee
333 lines (280 loc) · 9.98 KB
/
giles.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
fs = require 'fs'
path = require 'path'
log = require './log'
connect = require 'connect'
class Giles
constructor : () ->
@compilerMap = {}
@reverseCompilerMap = {}
@ignored = []
@locals = {}
@routes = {}
extendLocals : (dynLocals) ->
locals = {}
for key,value of @locals
locals[key]=value
for key, value of dynLocals
locals[key] = value
return locals
#the connect module for giles, to use it do
#express.use(giles.connect(assetDirectory))
connect : (dir) =>
(req, res, next) =>
[route, args] = req.url.split('?')
locals = @locals
route = '/index.html' if(route == '/')
# Check for user defined route first
if @routes[route]
file = @routes[route].source
#Load in any custom locals
dynLocals = @routes[route].locals
locals = @extendLocals(dynLocals) if dynLocals
else
#Otherwise look for 1-1 file mappings
[fullFilePath,args] = (dir+route).split("?")
file = @reverseLookup(fullFilePath)
if(file)
@compileFile file, locals, {}, (result) ->
relInput = path.relative(process.cwd(), file)
relOutput = path.relative(process.cwd(), result.outputFile)
if result.exists
log.notice "up to date #{relOutput} from #{relInput}"
else
log.notice "compiled #{relInput} into #{relOutput}"
log.encourage()
extname = path.extname(relOutput)
if extname == '.css'
#Sets MIME type for CSS stylesheets
res.setHeader "Content-Type", "text/css"
else if extname == '.html'
#Sets MIME type for HTML files
res.setHeader "Content-Type", "text/html"
else if extname == '.js'
#Sets MIME type for JavaScript files
res.setHeader "Content-Type", "application/javascript"
res.end result.content
else
next()
# Tells giles to not output anything to the console
quiet : () ->
log.quiet(true)
#adds an endpoint to the list of generated files
get : (endpoint, source, locals) ->
@routes[endpoint]={source: source, locals:locals}
server : (dir, opts) ->
port = opts['port'] || 2255
@app = connect().use(@connect(dir))
.use(connect.static(dir))
.listen(port)
log.notice("Giles is watching on port "+port)
reverseLookup : (file) ->
[name, ext] = @parseFileName(file)
pwd = process.cwd()
relativeName = path.relative(pwd, name)
numberFound = 0
foundFile = null
if @reverseCompilerMap[ext]
for extension in @reverseCompilerMap[ext]
if(fs.existsSync(name+extension))
foundFile = name+extension
numberFound += 1
if numberFound > 1
throw "You can only have one file that can compile into #{file} - you have #{numberFound} - #{@reverseCompilerMap[ext]}"
return foundFile
#Crawls a directory recursively
#calls onDirectory for every directory encountered
#calls onFile for every file encountered
crawl : (dir, onFile) ->
handlePath = (resource) =>
(err, stats) =>
if err
log.error(err)
else if stats.isFile()
onFile(resource)
else if stats.isDirectory()
@crawl(resource, onFile)
else
#wtf are we dealing with. A device?!
log.error("Could not determine file "+filename)
log.error(stats)
fs.readdir dir, (err, files) =>
if err
log.error("cannot read dir")
log.error(err)
else
for file in files
resource=dir+'/'+file
fs.stat resource, handlePath(resource)
#Adds a compiler. See README.md for usage
addCompiler : (extensions, target, callback) ->
compiler =
callback : callback,
extension: target
if typeof extensions is 'object'
@compilerMap[ext] = compiler for ext in extensions
@reverseCompilerMap[target] = [] unless @reverseCompilerMap[target]
@reverseCompilerMap[target].push ext for ext in extensions
else
@compilerMap[extensions] = compiler
@reverseCompilerMap[target] = [] unless @reverseCompilerMap[target]
@reverseCompilerMap[target].push extensions
process : (dir, onFile) ->
stats = fs.statSync(dir)
if stats.isDirectory()
@crawl dir, onFile
else if stats.isFile()
onFile(dir)
else
log.error(dir + " is not a directory or file")
# Removes a specific file
rmFile : (file) ->
fs.unlink file, (err) ->
log.error("Failed to remove: #{file}", err) if(err)
# Cleans up a directory's generated files. See README.md for usage
clean: (dir, opts) ->
for route, opts of @routes
source = opts.source
fullPath = path.resolve(process.cwd() + route)
log.notice "cleaning #{fullPath}"
@rmFile(fullPath)
@process dir, (f) =>
# Assume: f is a .html file with a jade file.
# 1) Check if both the .jade file and the .html file exist
sourceFile = @reverseLookup(f)
if fs.existsSync(sourceFile)
log.notice("Cleaning: " + f)
@rmFile(f)
#Builds a directory. See README.md for usage
build : (dir, opts) ->
for route, opts of @routes
source = opts.source
locals = @locals
locals = @extendLocals(opts.locals) if opts.locals
log.notice "building user-defined route #{route}"
fullPath = path.resolve(process.cwd() + route)
@compile source, locals, {outputFile: fullPath}
@process dir, (f) => @compile(f)
# Ignore an array of various directory names
ignore : (types) ->
@ignored = types
# Compiles a file and writes it out to disk
# `file` is the absolute path to the input file
# `locals` is an object of dynamic variables available
# to the view template.
# `options` accepts the following
# {
# outputFile : The destination file to output to
# }
compile : (file, locals, options) ->
return if @isIgnored(file)
locals = locals || @locals
result = @compileFile file, locals, options, (result) =>
# Convert to relative output for ease of reading
relInput = path.relative(process.cwd(), file)
relOutput = path.relative(process.cwd(), result.outputFile)
if result.exists
log.notice "up to date #{relOutput} from #{relInput}"
else
log.notice "compiled #{relInput} into #{relOutput}"
log.encourage()
return unless result
fs.writeFileSync result.outputFile, result.content, 'utf8'
#Compiles a file and calls cb() with the result object
compileFile : (file, locals, options, cb) ->
[prefix, ext] = @parseFileName(file)
compiler = @compilerMap[ext]
return unless compiler
unless fs.existsSync(file)
console.error "Could not find source file #{file}"
return
outputFile = prefix+compiler.extension
if options?.outputFile
outputFile = options.outputFile
content = fs.readFileSync(file, 'utf8')
outputContent = null
if fs.existsSync(outputFile)
outputContent = fs.readFileSync(outputFile, 'utf8')
cwd = process.cwd()
try
compiler.callback content, file, locals, (output) ->
if output == outputContent
cb({
content: outputContent,
outputFile : outputFile,
inputFile : file,
originalContent : content,
exists: true
})
else
cb({
outputFile : outputFile,
content : output,
inputFile : file,
originalContent : content
})
catch error
log.error(error)
log.error("stack trace:")
log.error(error.stack.replace(cwd, "."))
# Get the prefix and extension for a filename
parseFileName : (file) ->
ext = path.extname(file)
base = file.substr(0,file.length - ext.length)
[base, ext]
# true if name contains an ignored directory
isIgnored : (name) ->
filename = name.split('/')
filename = filename[filename.length-1]
return true if /^_/.test(filename) # ignore all files beginning with underscore
for ignore in @ignored
return true if ignore.test(name) #this matches really greedy
return false
[stylus, coffee, iced, jade, markdown] = []
#create our export singleton to set up default values
giles = new Giles()
#Stylus compiler. Nothing fancy
giles.addCompiler [".styl", ".stylus"], '.css', (contents, filename, options, output) ->
stylus = require 'stylus' unless stylus
styl = stylus(contents)
styl.set('filename', filename)
styl.include(options.cwd)
for key, val in options
styl.define(key, val)
stylus.render contents, {filename: filename}, (err, css) ->
if err
log.error "Could not render stylus file: "+filename
log.error err
else
output(css)
#coffeescript compiler
giles.addCompiler ['.coffee', '.cs'], '.js', (contents, filename, options, output) ->
coffee = require 'coffee-script' unless coffee
options.header = true
options.bare = false
delete options.scope if options.scope # Fix scope setting on options that kills options.bare with repeat coffee compiling :(
output(coffee.compile(contents, options))
#iced-coffeescript compiler
giles.addCompiler '.iced', '.js', (contents, filename, options, output) ->
iced = require 'iced-coffee-script' unless iced
iced_output = iced.compile(contents, options)
output(iced_output)
#jade compiler
giles.addCompiler '.jade', '.html', (contents, filename, options, output) ->
jade = require 'jade' unless jade
compileOpts = {}
compileOpts.filename = filename
compileOpts.pretty = true
try
compiled = jade.compile(contents, compileOpts)(options)
output(compiled)
catch e
output("<h1>Error compiling #{filename}</h1><code><pre>#{e.message}#{e.stack}</pre></code>")
throw e
giles.addCompiler '.md', '.html', (contents, filename, options, output) ->
markdown = require("markdown-js")
html = markdown.encode(contents)
output(html)
#default ignores, may be overriden
giles.ignore [/node_modules/, /.git/]
#export the giles singleton
module.exports = giles