broadway 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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