-
Notifications
You must be signed in to change notification settings - Fork 324
Expand file tree
/
Copy pathcontent.coffee
More file actions
294 lines (248 loc) · 9.56 KB
/
content.coffee
File metadata and controls
294 lines (248 loc) · 9.56 KB
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
### content.coffee ###
async = require 'async'
fs = require 'fs'
path = require 'path'
url = require 'url'
chalk = require 'chalk'
minimatch = require 'minimatch'
# options passed to minimatch for ignore and plugin matching
minimatchOptions =
dot: false
if not setImmediate?
setImmediate = process.nextTick
class ContentPlugin
### The mother of all plugins ###
@property = (name, getter) ->
### Define read-only property with *name*. ###
if typeof getter is 'string'
get = -> this[getter].call this
else
get = -> getter.call this
Object.defineProperty @prototype, name,
get: get
enumerable: true
@property 'view', 'getView'
getView: ->
### Return a view that renders the plugin. Either a string naming a exisitng view or a function:
`(env, locals, contents, templates, callback) ->`
Where *environment* is the current wintersmith environment, *contents* is the content-tree
and *templates* is a map of all templates as: {filename: templateInstance}. *callback* should be
called with a stream/buffer or null if this plugin instance should not be rendered. ###
throw new Error 'Not implemented.'
@property 'filename', 'getFilename'
getFilename: ->
### Return filename for this content. This is where the result of the plugin's view will be written to. ###
throw new Error 'Not implemented.'
@property 'url', 'getUrl'
getUrl: (base) ->
### Return url for this content relative to *base*. ###
filename = @getFilename()
base ?= @__env.config.baseUrl
if not base.match /\/$/
base += '/'
if process.platform is 'win32'
filename = filename.replace /\\/g, '/' #'
return url.resolve base, filename
@property 'pluginColor', 'getPluginColor'
getPluginColor: ->
### Return vanity color used to identify the plugin when printing the content tree
choices are: bold, italic, underline, inverse, yellow, cyan, white, magenta,
green, red, grey, blue, rainbow, zebra or none. ###
return 'cyan'
@property 'pluginInfo', 'getPluginInfo'
getPluginInfo: ->
### Return plugin information. Also displayed in the content tree printout. ###
return "url: #{ @url }"
ContentPlugin.fromFile = (filepath, callback) ->
### Calls *callback* with an instance of class. Where *filepath* is an object containing
both the absolute and realative paths for the file. e.g.
{full: "/home/foo/mysite/contents/somedir/somefile.ext",
relative: "somedir/somefile.ext"} ###
throw new Error 'Not implemented.'
class StaticFile extends ContentPlugin
### Static file handler, simply serves content as-is. Last in chain. ###
constructor: (@filepath) ->
getView: ->
return (args..., callback) ->
# locals, contents etc not used in this plugin
try
rs = fs.createReadStream @filepath.full
catch error
return callback error
callback null, rs
getFilename: ->
@filepath.relative
getPluginColor: ->
'none'
StaticFile.fromFile = (filepath, callback) ->
# normally you would want to read the file here, the static plugin however
# just pipes it to the file/http response
callback null, new StaticFile(filepath)
loadContent = (env, filepath, callback) ->
### Helper that loads content plugin found in *filepath*. ###
env.logger.silly "loading #{ filepath.relative }"
# any file not matched to a plugin will be handled by the static file plug
plugin =
class: StaticFile
group: 'files'
# iterate backwards over all content plugins and check if any plugin can handle this file
for i in [env.contentPlugins.length - 1..0] by -1
if minimatch filepath.relative, env.contentPlugins[i].pattern, minimatchOptions
plugin = env.contentPlugins[i]
break
# have the plugin's factory method create our instance
plugin.class.fromFile filepath, (error, instance) ->
error.message = "#{ filepath.relative }: #{ error.message }" if error?
# keep some references to the plugin and file used to create this instance
instance?.__env = env
instance?.__plugin = plugin
instance?.__filename = filepath.full
callback error, instance
# Class ContentTree
# not using Class since we need a clean prototype
ContentTree = (filename, groupNames=[]) ->
parent = null
groups = {directories: [], files: []}
for name in groupNames
groups[name] = []
Object.defineProperty this, '__groupNames',
get: -> groupNames
Object.defineProperty this, '_',
get: -> groups
Object.defineProperty this, 'filename',
get: -> filename
Object.defineProperty this, 'index',
get: ->
for key, item of this
if key[0...6] is 'index.'
return item
return
Object.defineProperty this, 'parent',
get: -> parent
set: (val) -> parent = val
ContentTree.fromDirectory = (env, directory, callback) ->
### Recursively scan *directory* and build a ContentTree with enviroment *env*.
Calls *callback* with a nested ContentTree or an error if something went wrong. ###
reldir = env.relativeContentsPath directory
tree = new ContentTree reldir, env.getContentGroups()
env.logger.silly "creating content tree from #{ directory }"
readDirectory = (callback) ->
fs.readdir directory, callback
resolveFilenames = (filenames, callback) ->
filenames.sort()
async.map filenames, (filename, callback) ->
relname = path.join reldir, filename
callback null,
full: path.join env.contentsPath, relname
relative: relname
, callback
filterIgnored = (filenames, callback) ->
### Exclude *filenames* matching ignore patterns in environment config. ###
if env.config.ignore.length > 0
async.filter filenames, (filename, callback) ->
include = true
for pattern in env.config.ignore
if minimatch filename.relative, pattern, minimatchOptions
env.logger.verbose "ignoring #{ filename.relative } (matches: #{ pattern })"
include = false
break
callback null, include
, callback
else
callback null, filenames
createInstance = (filepath, callback) ->
### Create plugin or subtree instance for *filepath*. ###
setImmediate ->
async.waterfall [
async.apply fs.stat, filepath.full
(stats, callback) ->
basename = path.basename filepath.relative
# recursively map directories to content tree instances
if stats.isDirectory()
ContentTree.fromDirectory env, filepath.full, (error, result) ->
result.parent = tree
tree[basename] = result
tree._.directories.push result # add instance to the directory group of its parent
callback error
# map files to content plugins
else if stats.isFile()
loadContent env, filepath, (error, instance) ->
if not error
instance.parent = tree
tree[basename] = instance
tree._[instance.__plugin.group].push instance
callback error
# This should never happen™
else
callback new Error "Invalid file #{ filepath.full }."
], callback
createInstances = (filenames, callback) ->
# NOTE: the file limit is not really enforced here since this is a recursive function
# but won't be a problem in 99% of cases, patches welcome :-)
# TODO: actually this has to be fixed, we want an error to stop instance creation
async.forEachLimit filenames, env.config._fileLimit, createInstance, callback
async.waterfall [
readDirectory
resolveFilenames
filterIgnored
createInstances
], (error) ->
callback error, tree
ContentTree.inspect = (tree, depth=0) ->
### Return a pretty formatted string representing the content *tree*. ###
if typeof tree is 'number'
# workaround for node.js calling inspect when converting objects to a string
return '[Function: ContentTree]'
rv = []
pad = ''
for i in [0..depth]
pad += ' '
keys = Object.keys(tree).sort (a, b) ->
# sort items by type and name to keep directories on top
ad = tree[a] instanceof ContentTree
bd = tree[b] instanceof ContentTree
return bd - ad if ad isnt bd
return -1 if a < b
return 1 if a > b
return 0
for k in keys
v = tree[k]
if v instanceof ContentTree
s = "#{ chalk.bold k }/\n"
s += ContentTree.inspect v, depth + 1
else
cfn = (s) -> s
if v.pluginColor isnt 'none'
unless cfn = chalk[v.pluginColor]
throw new Error "Plugin #{ k } specifies invalid pluginColor: #{ v.pluginColor }"
s = "#{ cfn k } (#{ chalk.grey v.pluginInfo })"
rv.push pad + s
rv.join '\n'
ContentTree.flatten = (tree) ->
### Return all the items in the *tree* as an array of content plugins. ###
rv = []
for key, value of tree
if value instanceof ContentTree
rv = rv.concat ContentTree.flatten value
else
rv.push value
return rv
ContentTree.merge = (root, tree) ->
### Merge *tree* into *root* tree. ###
for key, item of tree
if item instanceof ContentPlugin
root[key] = item
item.parent = root
root._[item.__plugin.group].push item
else if item instanceof ContentTree
if not root[key]?
root[key] = new ContentTree key, item.__groupNames
root[key].parent = root
root[key].parent._.directories.push root[key]
if root[key] instanceof ContentTree
ContentTree.merge root[key], item
else
throw new Error "Invalid item in tree for '#{ key }'"
return
### Exports ###
module.exports = {ContentTree, ContentPlugin, StaticFile, loadContent}