broadway 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ module Broadway
2
+ module Resource
3
+
4
+ end
5
+ end
@@ -0,0 +1,49 @@
1
+ module Broadway
2
+
3
+ class Runner
4
+
5
+ attr_accessor :site, :content, :url
6
+
7
+ def initialize(site)
8
+ self.site = site
9
+ self.url = URI.parse(site.config[:url])
10
+ self.content = site.pages + site.posts
11
+ end
12
+
13
+ def run(&block)
14
+ begin
15
+ Net::HTTP.start(url.host, url.port) do |http|
16
+ self.content.each do |item|
17
+ response = http.get(item.url)
18
+ # if the file doesn't exist or is not rendered correctly
19
+ next if error?(response, item.url, false)
20
+ write_dir = File.expand_path(File.join(site.config[:destination], item.dir))
21
+ name = File.join(write_dir, "index.html")
22
+ FileUtils.mkdir_p(write_dir)
23
+ text = ""
24
+ text << "---\n"
25
+ text << "categories: #{item.categories.sort.join(" ")}\n"
26
+ text << "---\n"
27
+ text << response.body
28
+ yield text if block_given?
29
+ File.write(name, text)
30
+ end
31
+ end
32
+ rescue Exception => e
33
+ puts e.inspect
34
+ raise "Make sure you've started the server, run: 'ruby app.rb'"
35
+ end
36
+ end
37
+
38
+ def error?(response, path, whiny = true)
39
+ if !response.is_a?(Net::HTTPSuccess)
40
+ raise message if whiny
41
+ puts "Error at '#{path}': #{response.to_s}"
42
+ return true
43
+ end
44
+ false
45
+ end
46
+
47
+ end
48
+
49
+ end
@@ -0,0 +1,398 @@
1
+ module Broadway
2
+
3
+ class Site
4
+ attr_accessor :config, :layouts, :posts, :pages, :static_files, :categories, :exclude,
5
+ :source, :dest, :lsi, :pygments, :permalink_style, :tags, :tree
6
+
7
+ def self.generate
8
+
9
+ end
10
+
11
+ # Initialize the site
12
+ # +config+ is a Hash containing site configurations details
13
+ #
14
+ # Returns <Site>
15
+ def initialize(config)
16
+ merge_extra_config!(config)
17
+
18
+ config.recursive_symbolize_keys!
19
+
20
+ self.config = config.clone
21
+
22
+ self.source = config[:source]
23
+ self.dest = config[:destination]
24
+ self.lsi = config[:lsi]
25
+ self.pygments = config[:pygments]
26
+ self.permalink_style = config[:permalink].to_sym
27
+ self.exclude = config[:exclude] || []
28
+ self.tree = []
29
+
30
+ self.reset
31
+ self.setup
32
+ end
33
+
34
+ def merge_extra_config!(config)
35
+ locale = File.join("locales", "#{config[:language]}.yml")
36
+ if File.exists?(locale)
37
+ config.merge!(YAML.load_file(locale))
38
+ end
39
+ config_dir = "config"
40
+ return unless File.exists?(config_dir)
41
+ Dir.glob("#{config_dir}/**/*").each do |file|
42
+ next if File.directory?(file)
43
+ path = file.gsub(/config\//, "").split(".")[0..-2].join("")
44
+ ext = File.extname(file)
45
+ if ext =~ /yml/
46
+ data = YAML.load_file(file)
47
+ elsif ext =~ /xml/
48
+ data = parse_children Nokogiri::XML(IO.read(file)).children[0]
49
+ else
50
+ data = IO.read(file)
51
+ end
52
+ next unless data
53
+ name = path.split("/").first
54
+ if path =~ /\//
55
+ config[name] ||= {}
56
+ target = config[name]
57
+ name = path.split("/").last
58
+ else
59
+ target = config
60
+ end
61
+ if target.has_key?(name)
62
+ target[name].merge!(data)
63
+ else
64
+ target[name] = data
65
+ end
66
+ end
67
+ end
68
+
69
+ # first
70
+ def reset
71
+ self.layouts = {}
72
+ self.posts = []
73
+ self.pages = []
74
+ self.static_files = []
75
+ self.categories = Hash.new { |hash, key| hash[key] = [] }
76
+ self.tags = Hash.new { |hash, key| hash[key] = [] }
77
+ end
78
+
79
+ # second
80
+ # this just sets configuration variables on the dependencies, if necessary
81
+ def setup
82
+ # Check to see if LSI is enabled.
83
+ require 'classifier' if self.lsi
84
+
85
+ # Set the Markdown interpreter (and Maruku self.config, if necessary)
86
+ case self.config[:markdown]
87
+ when 'rdiscount'
88
+ begin
89
+ require 'rdiscount'
90
+
91
+ def markdown(content)
92
+ RDiscount.new(content).to_html
93
+ end
94
+
95
+ rescue LoadError
96
+ puts 'You must have the rdiscount gem installed first'
97
+ end
98
+ when 'maruku'
99
+ begin
100
+ require 'maruku'
101
+
102
+ def markdown(content)
103
+ Maruku.new(content).to_html
104
+ end
105
+
106
+ if self.config[:maruku][:use_divs]
107
+ require 'maruku/ext/div'
108
+ puts 'Maruku: Using extended syntax for div elements.'
109
+ end
110
+
111
+ if self.config[:maruku][:use_tex]
112
+ require 'maruku/ext/math'
113
+ puts "Maruku: Using LaTeX extension. Images in `#{self.config[:maruku][:png_dir]}`."
114
+
115
+ # Switch off MathML output
116
+ MaRuKu::Globals[:html_math_output_mathml] = false
117
+ MaRuKu::Globals[:html_math_engine] = 'none'
118
+
119
+ # Turn on math to PNG support with blahtex
120
+ # Resulting PNGs stored in `images/latex`
121
+ MaRuKu::Globals[:html_math_output_png] = true
122
+ MaRuKu::Globals[:html_png_engine] = self.config[:maruku][:png_engine]
123
+ MaRuKu::Globals[:html_png_dir] = self.config[:maruku][:png_dir]
124
+ MaRuKu::Globals[:html_png_url] = self.config[:maruku][:png_url]
125
+ end
126
+ rescue LoadError
127
+ puts "The maruku gem is required for markdown support!"
128
+ end
129
+ else
130
+ raise "Invalid Markdown processor: '#{self.config[:markdown]}' -- did you mean 'maruku' or 'rdiscount'?"
131
+ end
132
+ end
133
+
134
+ def textile(content)
135
+ RedCloth.new(content).to_html
136
+ end
137
+
138
+ # third
139
+ # Do the actual work of processing the site and generating the
140
+ # real deal. Now has 4 phases; reset, read, render, write. This allows
141
+ # rendering to have full site payload available.
142
+ #
143
+ # Returns nothing
144
+ def process
145
+ build
146
+ generate
147
+ end
148
+
149
+ def build
150
+ puts "Building site: #{config[:source]} -> #{config[:destination]}"
151
+ self.reset
152
+ self.read
153
+ puts "Successfully built site, nothing was written."
154
+ end
155
+
156
+ def generate
157
+ puts "Generating site: #{config[:source]} -> #{config[:destination]}"
158
+ self.render
159
+ self.write
160
+ puts "Successfully generated site: #{config[:source]} -> #{config[:destination]}"
161
+ end
162
+
163
+ def read
164
+ self.read_layouts # existing implementation did this at top level only so preserved that
165
+ self.read_directories
166
+ end
167
+
168
+ # Read all the files in <source>/<dir>/_layouts and create a new Layout
169
+ # object with each one.
170
+ #
171
+ # Returns nothing
172
+ def read_layouts(dir = '')
173
+ base = File.join(self.source, dir, config[:layouts])
174
+ return unless File.exists?(base)
175
+ entries = []
176
+ Dir.chdir(base) { entries = filter_entries(Dir['*.*']) }
177
+ entries.each do |f|
178
+ name = f.split(".")[0..-2].join(".")
179
+ self.layouts[name] = Layout.new(self, base, f)
180
+ end
181
+ end
182
+
183
+ # Reads the directories and finds posts, pages and static files that will
184
+ # become part of the valid site according to the rules in +filter_entries+.
185
+ # The +dir+ String is a relative path used to call this method
186
+ # recursively as it descends through directories
187
+ #
188
+ # Returns nothing
189
+ def read_directories(dir = '')
190
+ base = File.join(self.source, dir).gsub(/\/$/, "")
191
+ lists = []
192
+ # RULES:
193
+ # Pages are only index.textile files, or static .html files (TODO)
194
+ # Posts are leaf nodes (name.textile files)
195
+ # Categories are the directory name split plus anything extra defined
196
+ # Pages and Posts can also be obtained via index.xml files
197
+ Dir.glob("#{base}/**/*").each do |path|
198
+ # removes junk, leaves us with .xml, .textile, .html
199
+ next if filtered?(path)
200
+ name = File.basename(path).split(".").first
201
+ ext = File.extname(path).gsub(".", "")
202
+ if name == "index"
203
+ if ext != "xml"
204
+ new_page(path)
205
+ end
206
+ elsif %w(textile markdown).include?(ext)
207
+ new_post(path)
208
+ else
209
+ self.static_files << StaticFile.new(:site => self, :path => path)
210
+ end
211
+ end
212
+
213
+ # lists are xml files we've collect, but we want to make sure we've
214
+ # created all the posts we need to beforehand
215
+ list = File.join(config[:source], "index.xml")
216
+ lists << list if File.exists?(list)
217
+ lists.each do |path|
218
+ new_tree(path)
219
+ end
220
+
221
+ # finally, we have set all the initial variables on the
222
+ # pages and posts we need, now we can process them to find
223
+ # the content and generate the urls
224
+ self.pages.each do |page|
225
+ page.process
226
+ page.categories.each { |c| self.categories[c] << page }
227
+ page.tags.each { |c| self.tags[c] << page }
228
+ end
229
+ self.posts.each do |post|
230
+ post.process
231
+ post.categories.each { |c| self.categories[c] << post }
232
+ post.tags.each { |c| self.tags[c] << post }
233
+ end
234
+ end
235
+
236
+ def new_page(path, options = {})
237
+ return if path.nil? || path.empty?
238
+ page = Page.new(options.merge(:site => self, :path => path, :process => false))
239
+ self.pages << page
240
+ page
241
+ end
242
+
243
+ def new_post(path, options = {})
244
+ return if path.nil? || path.empty?
245
+ post = Post.new(options.merge(:site => self, :path => path, :process => false))
246
+ self.posts << post
247
+ post
248
+ end
249
+
250
+ def new_tree(path)
251
+ self.tree.concat parse_tree(Nokogiri::XML(IO.read(path)).root)
252
+ end
253
+
254
+ def parse_tree(parent)
255
+ result = []
256
+ return result if parent.nil? || parent.children.nil? || parent.children.empty?
257
+ parent.children.each do |child|
258
+ next unless child.elem?
259
+ content = child.children.empty? ? post_from_xml(child) : page_from_xml(child)
260
+ next unless content
261
+ if content.respond_to?(:children)
262
+ content.children.concat parse_tree(child)
263
+ end
264
+ result << content
265
+ end
266
+ result
267
+ end
268
+
269
+ # http://stackoverflow.com/questions/1769126/fastest-one-liner-way-to-get-xml-nodes-into-array-of-path-to-nodes-in-ruby
270
+ def node_list(elem, &proc)
271
+ return [] unless elem.class == Nokogiri::XML::Element
272
+ str = proc.call(elem)
273
+ [str] + elem.children.inject([]){|a,c| a+node_list(c,&proc)}.map{|e| "#{str}/#{e}"}
274
+ end
275
+
276
+ def post_from_xml(content)
277
+ # post
278
+ # 1. If a "src" is defined, then we should have already created the post
279
+ # 2. If no "src", then content is specified inline (or there is no content yet)
280
+ post = find_post_by_path(content["src"]) || find_post_by_url(content["url"])
281
+ path = (content["src"] || content["url"] || "").gsub(/^\//, "")
282
+ post ||= new_post(path)
283
+ return unless post
284
+ %w(title image excerpt menu_title tooltip show_children content).each do |key|
285
+ post.data[key] = content[key] if content.has_attribute?(key)
286
+ end
287
+ post
288
+ end
289
+
290
+ def page_from_xml(content)
291
+ # page
292
+ # 1. If a "src" is defined, then we should have already created the post
293
+ # 2. If no "src", then content is specified inline (or there is no content yet)
294
+ page = find_page_by_path(content["src"]) || find_page_by_url(content["url"])
295
+ path = (content["src"] || content["url"] || "").gsub(/^\//, "")
296
+ page ||= new_page(path)
297
+ return unless page
298
+ %w(title image excerpt menu_title tooltip show_children content).each do |key|
299
+ page.data[key] = content[key] if content.has_attribute?(key)
300
+ end
301
+ page
302
+ end
303
+
304
+ # While in Jekyll this method renders the content explicitly,
305
+ # I'm using Sinatra, partly because I like having the flexibility
306
+ # of using their get/post methods, and partly because I don't have
307
+ # the time/desire to try to hack jekyll to use haml.
308
+ # I'd rather just start over
309
+ def render
310
+ Runner.new(self).run
311
+ end
312
+
313
+ # Write static files, pages and posts
314
+ #
315
+ # Returns nothing
316
+ def write
317
+ self.static_files.each do |sf|
318
+ sf.write(self.dest)
319
+ end
320
+ end
321
+
322
+ # Constructs a hash map of Posts indexed by the specified Post attribute
323
+ #
324
+ # Returns {post_attr => [<Post>]}
325
+ def post_attr_hash(post_attr)
326
+ # Build a hash map based on the specified post attribute ( post attr => array of posts )
327
+ # then sort each array in reverse order
328
+ hash = Hash.new { |hash, key| hash[key] = Array.new }
329
+ self.posts.each { |p| p.send(post_attr.to_sym).each { |t| hash[t] << p } }
330
+ hash.values.map { |sortme| sortme.sort! { |a, b| b <=> a} }
331
+ return hash
332
+ end
333
+
334
+ # The Hash payload containing site-wide data
335
+ #
336
+ # Returns {"site" => {"time" => <Time>,
337
+ # "posts" => [<Post>],
338
+ # "categories" => [<Post>]}
339
+ def site_payload
340
+ {"site" => self.config.merge({
341
+ "time" => Time.now,
342
+ "posts" => self.posts.sort { |a,b| b <=> a },
343
+ "categories" => post_attr_hash('categories'),
344
+ "tags" => post_attr_hash('tags')})}
345
+ end
346
+
347
+ # Filter out any files/directories that are hidden or backup files (start
348
+ # with "." or "#" or end with "~"), or contain site content (start with "_"),
349
+ # or are excluded in the site configuration, unless they are web server
350
+ # files such as '.htaccess'
351
+ def filtered?(path)
352
+ return true if File.directory?(path)
353
+ file = File.basename(path)
354
+ return true if ['.htaccess'].include?(file)
355
+ ['.', '_', '#'].include?(file[0..0]) || file[-1..-1] == '~' || self.exclude.include?(file)
356
+ end
357
+
358
+ def find(type, method, value)
359
+ self.send(type.to_s.pluralize).select do |content|
360
+ if content.respond_to?(method)
361
+ content.send(method) == value
362
+ else
363
+ content.send(method.to_s.pluralize).include?(value)
364
+ end
365
+ end
366
+ end
367
+
368
+ def first(type, method, value)
369
+ find(type, method, value).first
370
+ end
371
+
372
+ %w(category url path tag title).each do |property|
373
+ %w(post page).each do |type|
374
+ define_method "find_#{type}_by_#{property}" do |value|
375
+ first(type.to_sym, property, value)
376
+ end
377
+ define_method "find_#{type.pluralize}_by_#{property}" do |value|
378
+ find(type.to_sym, property, value)
379
+ end
380
+ end
381
+ define_method "find_by_#{property}" do |value|
382
+ first(:page, property, value) || first(:post, property, value)
383
+ end
384
+ end
385
+
386
+ def page_count
387
+ self.pages ? self.pages.length : 0
388
+ end
389
+
390
+ def post_count
391
+ self.posts ? self.posts.length : 0
392
+ end
393
+
394
+ def inspect
395
+ "#<Broadway:Site @page_count=#{self.page_count.to_s} @post_count=#{self.post_count.to_s}>"
396
+ end
397
+ end
398
+ end