broadway 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|