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,31 @@
|
|
1
|
+
module Broadway
|
2
|
+
module Helpers
|
3
|
+
module PartialHelper
|
4
|
+
|
5
|
+
def partial(template, options = {})
|
6
|
+
template = template.to_s.squeeze("/")
|
7
|
+
options.symbolize_keys!
|
8
|
+
options.merge!(:layout => false)
|
9
|
+
template_array = template.to_s.split('/')
|
10
|
+
template = "_#{template_array[-1]}"
|
11
|
+
local_template = File.join(request.env["PATH_INFO"][1..-1], template)
|
12
|
+
template = local_template if File.exists?(File.join("public", local_template) + ".haml")
|
13
|
+
template = template_array[0..-2].join('/') + "/#{template}"
|
14
|
+
if collection = options.delete(:collection) then
|
15
|
+
collection.inject([]) do |buffer, member|
|
16
|
+
buffer << haml(:"#{template}", options.merge(:layout =>
|
17
|
+
false, :locals => {template_array[-1].to_sym => member}))
|
18
|
+
end.join("\n")
|
19
|
+
else
|
20
|
+
haml(:"#{template}", options)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def theme_partial(template, options = {})
|
25
|
+
partial(File.join(c(:theme_path), template.to_s), options)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Broadway
|
2
|
+
module Helpers
|
3
|
+
module TextHelper
|
4
|
+
|
5
|
+
# config helper method
|
6
|
+
def c(path)
|
7
|
+
result = site.config
|
8
|
+
path.to_s.split(".").each { |node| result = result[node.to_sym] if result }
|
9
|
+
result.nil? ? [] : result
|
10
|
+
end
|
11
|
+
|
12
|
+
# read the post and parse it with Liquid
|
13
|
+
def read(post, attribute = nil)
|
14
|
+
post.content = IO.read(post.path)
|
15
|
+
if post.content =~ /^(---\s*\n.*?\n?)^(---\s*$\n?)/m
|
16
|
+
if attribute
|
17
|
+
output = YAML.load($1)[attribute] || ""
|
18
|
+
return output
|
19
|
+
end
|
20
|
+
post.content = post.content[($1.size + $2.size)..-1]
|
21
|
+
end
|
22
|
+
post.render(site.layouts, site.site_payload)
|
23
|
+
output = post.output
|
24
|
+
post.content = post.output = nil
|
25
|
+
output
|
26
|
+
end
|
27
|
+
|
28
|
+
def read_excerpt(post, max_length = nil)
|
29
|
+
result = read(post, "excerpt")
|
30
|
+
result = read(post) if result.nil? || result.empty?
|
31
|
+
max_length ||= result.length
|
32
|
+
result[0..max_length] + "..."
|
33
|
+
result
|
34
|
+
end
|
35
|
+
|
36
|
+
def code(code, options={})
|
37
|
+
options[:lang] = extension_to_lang(options[:extension]) if options.has_key?(:extension)
|
38
|
+
puts options.inspect
|
39
|
+
options[:lang] ||= "plain_text" unless Uv.syntaxes.include?(options[:lang])
|
40
|
+
options[:line_numbers] = false unless options.has_key?(:line_numbers)
|
41
|
+
options[:theme] ||= "twilight"
|
42
|
+
Uv.parse(code, "xhtml", options[:lang], options[:line_numbers], options[:theme])
|
43
|
+
end
|
44
|
+
|
45
|
+
def extension_to_lang(ext)
|
46
|
+
{
|
47
|
+
"js" => "javascript",
|
48
|
+
"rb" => "ruby",
|
49
|
+
"as" => "actionscript",
|
50
|
+
"mxml" => "mxml",
|
51
|
+
"xml" => "xml",
|
52
|
+
"css" => "css"
|
53
|
+
}[ext.gsub(/\./, "")]
|
54
|
+
end
|
55
|
+
|
56
|
+
def space(times = 1)
|
57
|
+
haml_concat("\n" * times)
|
58
|
+
end
|
59
|
+
|
60
|
+
def show_code
|
61
|
+
root = File.expand_path("public")
|
62
|
+
file = params[:file]
|
63
|
+
begin
|
64
|
+
path = File.expand_path(File.join(root, file)).untaint
|
65
|
+
if (File.dirname(path).split("/").length >= root.split("/").length)
|
66
|
+
data = IO.read(path)#.gsub(/[']/, '\\\\\'')
|
67
|
+
code(data, :extension => File.extname(path))
|
68
|
+
else
|
69
|
+
#only happens when someone tries to go outside your root directory...
|
70
|
+
"You are way out of your league"
|
71
|
+
end
|
72
|
+
rescue
|
73
|
+
"Internal Error"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
libdir = File.dirname(__FILE__)
|
2
|
+
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
|
3
|
+
|
4
|
+
require 'base'
|
5
|
+
|
6
|
+
@site = site = Broadway.build
|
7
|
+
|
8
|
+
Sinatra::Application.instance_eval do
|
9
|
+
cattr_accessor :site
|
10
|
+
self.site = site
|
11
|
+
end
|
12
|
+
|
13
|
+
def site
|
14
|
+
@site
|
15
|
+
end
|
16
|
+
|
17
|
+
def default_locals(locals = {})
|
18
|
+
locals#.merge(site.config.dup).merge({:menu => menu})
|
19
|
+
end
|
20
|
+
|
21
|
+
def theme_path
|
22
|
+
site.config[:theme_path]
|
23
|
+
end
|
24
|
+
|
25
|
+
def pages
|
26
|
+
Dir.entries("public")
|
27
|
+
end
|
28
|
+
|
29
|
+
Sinatra::Application.class_eval do
|
30
|
+
register SinatraMore::MarkupPlugin
|
31
|
+
register SinatraMore::RoutingPlugin
|
32
|
+
end
|
33
|
+
|
34
|
+
def page_path(page)
|
35
|
+
content_path(page, "index")
|
36
|
+
end
|
37
|
+
|
38
|
+
def post_path(post)
|
39
|
+
content_path(post, "show")
|
40
|
+
end
|
41
|
+
|
42
|
+
def content_path(content, action)
|
43
|
+
path = File.join(content.dir, action).squeeze("/")
|
44
|
+
exists = false
|
45
|
+
extensions = %w(html haml erb html.haml html.erb)
|
46
|
+
target_path = File.join(site.config[:source], path)
|
47
|
+
extensions.each do |ext|
|
48
|
+
exists = true if File.exists?("#{target_path}.#{ext}")
|
49
|
+
end
|
50
|
+
return path if exists
|
51
|
+
checked = [target_path]
|
52
|
+
path = target_path = File.join(site.config[:theme_path], path).squeeze("/")
|
53
|
+
exists = false
|
54
|
+
extensions.each do |ext|
|
55
|
+
exists = true if File.exists?(File.join(site.config[:source], "#{target_path}.#{ext}"))
|
56
|
+
end
|
57
|
+
return path if exists
|
58
|
+
if content.url == "/"
|
59
|
+
return File.join(site.config[:theme_path], "index")
|
60
|
+
end
|
61
|
+
checked << target_path
|
62
|
+
puts "Couldn't find paths for #{content.class.to_s.downcase.split(":").last} ('#{content.url}'): #{checked.inspect}"
|
63
|
+
return nil
|
64
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
module Broadway
|
2
|
+
|
3
|
+
class Page
|
4
|
+
include Convertible
|
5
|
+
|
6
|
+
attr_accessor :site
|
7
|
+
attr_accessor :name, :ext, :basename, :dir, :path, :slug
|
8
|
+
attr_accessor :data, :content, :output, :categories, :tags, :children
|
9
|
+
|
10
|
+
# Initialize a new Page.
|
11
|
+
# +site+ is the Site
|
12
|
+
# +base+ is the String path to the <source>
|
13
|
+
# +dir+ is the String path between <source> and the file
|
14
|
+
# +name+ is the String filename of the file
|
15
|
+
#
|
16
|
+
# Returns <Page>
|
17
|
+
def initialize(options = {})
|
18
|
+
self.site = options[:site]
|
19
|
+
self.path = options[:path] if options.has_key?(:path)
|
20
|
+
self.data = {}
|
21
|
+
self.dir = path =~ /\// ? File.dirname(path.gsub(/#{site.config[:source]}\/?/, "")).gsub(/^\//, "") : path
|
22
|
+
self.name = File.basename(self.dir)
|
23
|
+
self.ext = File.extname(path)
|
24
|
+
self.basename = File.basename(path).split('.')[0..-2].first
|
25
|
+
|
26
|
+
self.categories ||= []
|
27
|
+
self.categories.concat self.dir.split('/').reject { |x| x.empty? }
|
28
|
+
self.children ||= []
|
29
|
+
process(options) unless options.has_key?(:process) and options[:process] == false
|
30
|
+
end
|
31
|
+
|
32
|
+
# The UID for this post (useful in feeds)
|
33
|
+
# e.g. /2008/11/05/my-awesome-post
|
34
|
+
#
|
35
|
+
# Returns <String>
|
36
|
+
def id
|
37
|
+
File.join(self.dir, self.name)
|
38
|
+
end
|
39
|
+
|
40
|
+
def menu_title
|
41
|
+
self.data && self.data['menu_title']
|
42
|
+
end
|
43
|
+
|
44
|
+
def tooltip
|
45
|
+
self.data && self.data["tooltip"]
|
46
|
+
end
|
47
|
+
|
48
|
+
def permalink
|
49
|
+
self.data && self.data['permalink']
|
50
|
+
end
|
51
|
+
|
52
|
+
def show_children?
|
53
|
+
self.data && (!self.data.has_key?("show_children") || self.data["show_children"] == true)
|
54
|
+
end
|
55
|
+
|
56
|
+
def title
|
57
|
+
self.data && (self.data['title'] || self.name)
|
58
|
+
end
|
59
|
+
|
60
|
+
def parent
|
61
|
+
url ? site.find_page_by_url(url[1..-1].split("/").first) : nil
|
62
|
+
end
|
63
|
+
|
64
|
+
# The generated relative url of this page
|
65
|
+
# e.g. /about
|
66
|
+
#
|
67
|
+
# Returns <String>
|
68
|
+
def url
|
69
|
+
return permalink if permalink
|
70
|
+
@url ||= {
|
71
|
+
"name" => CGI.escape(name),
|
72
|
+
"categories" => categories[0..-2].join('/')
|
73
|
+
}.inject(template) { |result, token|
|
74
|
+
result.gsub(/:#{token.first}/, token.last)
|
75
|
+
}.gsub(/#{site.config[:posts]}/, "").squeeze("/")
|
76
|
+
end
|
77
|
+
|
78
|
+
def template
|
79
|
+
if self.site.permalink_style == :pretty
|
80
|
+
"/:categories/:name"
|
81
|
+
else
|
82
|
+
"/:categories/:name.html"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Extract information from the page filename
|
87
|
+
# +name+ is the String filename of the page file
|
88
|
+
#
|
89
|
+
# Returns nothing
|
90
|
+
def process(options = {})
|
91
|
+
self.read_yaml(path)
|
92
|
+
|
93
|
+
self.tags = self.data.pluralized_array("tag", "tags")
|
94
|
+
end
|
95
|
+
|
96
|
+
# Add any necessary layouts to this post
|
97
|
+
# +layouts+ is a Hash of {"name" => "layout"}
|
98
|
+
# +site_payload+ is the site payload hash
|
99
|
+
#
|
100
|
+
# Returns nothing
|
101
|
+
def render(layouts, site_payload)
|
102
|
+
payload = {"page" => self.data}.deep_merge(site_payload)
|
103
|
+
do_layout(payload, layouts)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Write the generated page file to the destination directory.
|
107
|
+
# +dest_prefix+ is the String path to the destination dir
|
108
|
+
# +dest_suffix+ is a suffix path to the destination dir
|
109
|
+
#
|
110
|
+
# Returns nothing
|
111
|
+
def write(dest_prefix, dest_suffix = nil)
|
112
|
+
dest = File.join(dest_prefix, @dir)
|
113
|
+
dest = File.join(dest, dest_suffix) if dest_suffix
|
114
|
+
FileUtils.mkdir_p(dest)
|
115
|
+
|
116
|
+
# The url needs to be unescaped in order to preserve the correct filename
|
117
|
+
path = File.join(dest, CGI.unescape(self.url))
|
118
|
+
if self.ext == '.html' && self.url[/\.html$/].nil?
|
119
|
+
FileUtils.mkdir_p(path)
|
120
|
+
path = File.join(path, "index.html")
|
121
|
+
end
|
122
|
+
|
123
|
+
File.open(path, 'w') do |f|
|
124
|
+
f.write(self.output)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def inspect
|
129
|
+
"#<Broadway:Page @url=#{self.url.inspect} @name=#{self.name.inspect} @categories=#{self.categories.inspect} @tags=#{self.tags.inspect} @data=#{self.data.inspect}>"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
@@ -0,0 +1,196 @@
|
|
1
|
+
module Broadway
|
2
|
+
|
3
|
+
class Post
|
4
|
+
include Comparable
|
5
|
+
include Convertible
|
6
|
+
include Resource
|
7
|
+
|
8
|
+
SRC_MATCHER = /^(.+\/)*(?:(\d+-\d+-\d+)-)?(.*)(\.[^.]+)$/
|
9
|
+
URL_MATCHER = /^(.+\/)*(.*)$/
|
10
|
+
|
11
|
+
# Post name validator. Post filenames must be like:
|
12
|
+
# 2008-11-05-my-awesome-post.textile
|
13
|
+
#
|
14
|
+
# Returns <Bool>
|
15
|
+
def self.valid?(name, site)
|
16
|
+
site.config[:posts_include].include?(File.extname(name))
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_accessor :site
|
20
|
+
# where the file is
|
21
|
+
attr_accessor :path, :dir, :name, :basename, :ext
|
22
|
+
attr_accessor :data, :content, :output
|
23
|
+
attr_accessor :date, :slug, :published, :tags, :categories, :asset
|
24
|
+
|
25
|
+
# Initialize this Post instance.
|
26
|
+
# +site+ is the Site
|
27
|
+
# +base+ is the String path to the dir containing the post file
|
28
|
+
# +name+ is the String filename of the post file
|
29
|
+
# +categories+ is an Array of Strings for the categories for this post
|
30
|
+
#
|
31
|
+
# Returns <Post>
|
32
|
+
def initialize(options = {})
|
33
|
+
self.site = options[:site]
|
34
|
+
self.path = options[:path] if options.has_key?(:path)
|
35
|
+
self.data = {}
|
36
|
+
|
37
|
+
n, cats, date, slug, ext = *path.match(SRC_MATCHER)
|
38
|
+
n, cats, slug = *path.match(URL_MATCHER) unless slug
|
39
|
+
self.date = Time.parse(date) if date
|
40
|
+
self.slug = slug
|
41
|
+
self.ext = ext
|
42
|
+
self.dir = options.has_key?(:dir) ? options[:dir] : File.dirname(path.gsub(/#{site.config[:source]}\/?/, ""))
|
43
|
+
|
44
|
+
self.categories = dir.split('/').reject { |x| x.empty? }
|
45
|
+
|
46
|
+
process(options) unless options.has_key?(:process) and options[:process] == false
|
47
|
+
end
|
48
|
+
|
49
|
+
# Extract information from the post filename
|
50
|
+
# +name+ is the String filename of the post file
|
51
|
+
#
|
52
|
+
# Returns nothing
|
53
|
+
def process(options = {})
|
54
|
+
self.read_yaml(path)
|
55
|
+
|
56
|
+
# If we've added a date and time to the yaml, use that instead of the filename date
|
57
|
+
# Means we'll sort correctly.
|
58
|
+
self.date = Time.parse(self.data["date"].to_s) if self.data.has_key?('date')
|
59
|
+
|
60
|
+
if self.data.has_key?('published') && self.data['published'] == false
|
61
|
+
self.published = false
|
62
|
+
else
|
63
|
+
self.published = true
|
64
|
+
end
|
65
|
+
|
66
|
+
if self.data.has_key?("asset")
|
67
|
+
data["asset"]["title"] ||= self.title
|
68
|
+
self.asset = Asset.new(data["asset"])
|
69
|
+
end
|
70
|
+
|
71
|
+
self.tags = self.data.pluralized_array("tag", "tags")
|
72
|
+
self.tags ||= []
|
73
|
+
end
|
74
|
+
|
75
|
+
# Spaceship is based on Post#date, slug
|
76
|
+
#
|
77
|
+
# Returns -1, 0, 1
|
78
|
+
def <=>(other)
|
79
|
+
return self.url <=> other.url
|
80
|
+
end
|
81
|
+
|
82
|
+
# The UID for this post (useful in feeds)
|
83
|
+
# e.g. /2008/11/05/my-awesome-post
|
84
|
+
#
|
85
|
+
# Returns <String>
|
86
|
+
def id
|
87
|
+
File.join(self.dir, self.slug)
|
88
|
+
end
|
89
|
+
|
90
|
+
def parent
|
91
|
+
url ? site.find_page_by_url(url[1..-1].split("/").first) : nil
|
92
|
+
end
|
93
|
+
|
94
|
+
# The full path and filename of the post.
|
95
|
+
# Defined in the YAML of the post body
|
96
|
+
# (Optional)
|
97
|
+
#
|
98
|
+
# Returns <String>
|
99
|
+
def permalink
|
100
|
+
self.data && self.data['permalink']
|
101
|
+
end
|
102
|
+
|
103
|
+
def title
|
104
|
+
self.data && (self.data['title'] || self.slug)
|
105
|
+
end
|
106
|
+
|
107
|
+
def excerpt
|
108
|
+
self.data && self.data["excerpt"]
|
109
|
+
end
|
110
|
+
|
111
|
+
def template
|
112
|
+
case self.site.permalink_style
|
113
|
+
when :pretty
|
114
|
+
"/:categories/:year/:month/:day/:title"
|
115
|
+
when :none
|
116
|
+
"/:categories/:title.html"
|
117
|
+
when :date
|
118
|
+
"/:categories/:year/:month/:day/:title.html"
|
119
|
+
else
|
120
|
+
self.site.permalink_style.to_s
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# The generated relative url of this post
|
125
|
+
# e.g. /2008/11/05/my-awesome-post.html
|
126
|
+
#
|
127
|
+
# Returns <String>
|
128
|
+
def url
|
129
|
+
return permalink if permalink
|
130
|
+
@url ||= {
|
131
|
+
"year" => date ? date.strftime("%Y") : "",
|
132
|
+
"month" => date ? date.strftime("%m") : "",
|
133
|
+
"day" => date ? date.strftime("%d") : "",
|
134
|
+
"title" => CGI.escape(slug),
|
135
|
+
"categories" => categories.join('/')
|
136
|
+
}.inject(template) { |result, token|
|
137
|
+
result.gsub(/:#{token.first}/, token.last)
|
138
|
+
}.gsub(/#{site.config[:posts]}/, "").squeeze("/")
|
139
|
+
end
|
140
|
+
|
141
|
+
# Add any necessary layouts to this post
|
142
|
+
# +layouts+ is a Hash of {"name" => "layout"}
|
143
|
+
# +site_payload+ is the site payload hash
|
144
|
+
#
|
145
|
+
# Returns nothing
|
146
|
+
def render(layouts, site_payload)
|
147
|
+
# construct payload
|
148
|
+
payload =
|
149
|
+
{
|
150
|
+
"site" => {},
|
151
|
+
"page" => self.to_liquid
|
152
|
+
}
|
153
|
+
payload = payload.deep_merge(site_payload)
|
154
|
+
|
155
|
+
do_layout(payload, layouts)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Write the generated post file to the destination directory.
|
159
|
+
# +dest+ is the String path to the destination dir
|
160
|
+
#
|
161
|
+
# Returns nothing
|
162
|
+
def write(dest)
|
163
|
+
FileUtils.mkdir_p(File.join(dest, dir))
|
164
|
+
|
165
|
+
# The url needs to be unescaped in order to preserve the correct filename
|
166
|
+
path = File.join(dest, CGI.unescape(self.url))
|
167
|
+
|
168
|
+
if template[/\.html$/].nil?
|
169
|
+
FileUtils.mkdir_p(path)
|
170
|
+
path = File.join(path, "index.html")
|
171
|
+
end
|
172
|
+
|
173
|
+
File.open(path, 'w') do |f|
|
174
|
+
f.write(self.output)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
# Convert this post into a Hash for use in Liquid templates.
|
179
|
+
#
|
180
|
+
# Returns <Hash>
|
181
|
+
def to_liquid
|
182
|
+
{ "title" => self.data["title"] || self.slug.split('-').select {|w| w.capitalize! || w }.join(' '),
|
183
|
+
"url" => self.url,
|
184
|
+
"date" => self.date,
|
185
|
+
"id" => self.id,
|
186
|
+
"categories" => self.categories,
|
187
|
+
"tags" => self.tags,
|
188
|
+
"content" => self.content }.deep_merge(self.data)
|
189
|
+
end
|
190
|
+
|
191
|
+
def inspect
|
192
|
+
"#<Broadway:Post @url=#{self.url.inspect} @categories=#{self.categories.inspect} @tags=#{self.tags.inspect} @data=#{self.data.inspect}>"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|