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.
- data/README.textile +300 -0
- data/Rakefile +80 -0
- data/lib/broadway.rb +7 -0
- data/lib/broadway/api.rb +51 -0
- data/lib/broadway/asset.rb +17 -0
- data/lib/broadway/base.rb +121 -0
- data/lib/broadway/convertible.rb +89 -0
- data/lib/broadway/core_ext.rb +93 -0
- data/lib/broadway/helpers.rb +7 -0
- data/lib/broadway/helpers/collection_helper.rb +152 -0
- data/lib/broadway/helpers/partial_helper.rb +31 -0
- data/lib/broadway/helpers/text_helper.rb +78 -0
- data/lib/broadway/main.rb +64 -0
- data/lib/broadway/page.rb +133 -0
- data/lib/broadway/post.rb +196 -0
- data/lib/broadway/resource.rb +5 -0
- data/lib/broadway/runner.rb +49 -0
- data/lib/broadway/site.rb +398 -0
- data/lib/broadway/static_file.rb +32 -0
- metadata +119 -0
@@ -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
|