amber 0.2.6
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +661 -0
- data/README.md +114 -0
- data/bin/amber +71 -0
- data/lib/amber.rb +76 -0
- data/lib/amber/cli.rb +98 -0
- data/lib/amber/logger.rb +21 -0
- data/lib/amber/menu.rb +141 -0
- data/lib/amber/page_array.rb +61 -0
- data/lib/amber/render/asset.rb +55 -0
- data/lib/amber/render/autolink.rb +78 -0
- data/lib/amber/render/bracketlink.rb +33 -0
- data/lib/amber/render/helpers/haml_helper.rb +54 -0
- data/lib/amber/render/helpers/html_helper.rb +75 -0
- data/lib/amber/render/helpers/language_helper.rb +25 -0
- data/lib/amber/render/helpers/navigation_helper.rb +203 -0
- data/lib/amber/render/layout.rb +66 -0
- data/lib/amber/render/table_of_contents.rb +383 -0
- data/lib/amber/render/template.rb +158 -0
- data/lib/amber/render/view.rb +189 -0
- data/lib/amber/server.rb +113 -0
- data/lib/amber/site.rb +154 -0
- data/lib/amber/site_configuration.rb +132 -0
- data/lib/amber/static_page.rb +151 -0
- data/lib/amber/static_page/filesystem.rb +270 -0
- data/lib/amber/static_page/page_properties.rb +124 -0
- data/lib/amber/static_page/property_set.rb +78 -0
- data/lib/amber/static_page/render.rb +122 -0
- metadata +203 -0
@@ -0,0 +1,189 @@
|
|
1
|
+
#
|
2
|
+
# All pages and layouts are evaluated in the context of a View object.
|
3
|
+
#
|
4
|
+
# Any methods you want to make available to these templates should be added to this class.
|
5
|
+
#
|
6
|
+
|
7
|
+
require 'i18n'
|
8
|
+
require 'amber/render/helpers/html_helper'
|
9
|
+
require 'amber/render/helpers/navigation_helper'
|
10
|
+
require 'amber/render/helpers/haml_helper'
|
11
|
+
require 'amber/render/helpers/language_helper'
|
12
|
+
|
13
|
+
module Amber
|
14
|
+
module Render
|
15
|
+
class View
|
16
|
+
attr_reader :locals
|
17
|
+
attr_reader :page
|
18
|
+
attr_reader :site
|
19
|
+
|
20
|
+
# include helpers (get added as member functions)
|
21
|
+
include HtmlHelper
|
22
|
+
include NavigationHelper
|
23
|
+
include HamlHelper
|
24
|
+
include LanguageHelper
|
25
|
+
|
26
|
+
def initialize(page, site)
|
27
|
+
@page = page
|
28
|
+
@site = site
|
29
|
+
@stack = []
|
30
|
+
@locals = {}
|
31
|
+
@this = StaticPage::PropertySet.new # TODO: come up with a better way to handle this.
|
32
|
+
# @this is not actually used, it is just there so haml headers don't bomb out.
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# more or less the same as Rails render()
|
37
|
+
#
|
38
|
+
# supported options:
|
39
|
+
# :page -- page path or page object to render
|
40
|
+
# :file -- renders the file specified, using suffix to determine type.
|
41
|
+
# :partial -- same as :file, but disables layout
|
42
|
+
# :text -- string to render
|
43
|
+
# :type -- required for :text
|
44
|
+
#
|
45
|
+
def render(options={}, locals={}, toc_only=false, &block)
|
46
|
+
push_context @locals, @page
|
47
|
+
@locals = @locals.merge(locals)
|
48
|
+
locale = I18n.locale = @locals[:locale]
|
49
|
+
options = parse_render_options(locale, options)
|
50
|
+
@page = options[:page] if options[:page]
|
51
|
+
render_toc = should_render_toc?(locale, options, @page)
|
52
|
+
template = pick_template(locale, options)
|
53
|
+
if toc_only
|
54
|
+
template.render(self, :mode => :toc, :href_base => options[:href_base])
|
55
|
+
else
|
56
|
+
layout = pick_layout(locale, options)
|
57
|
+
if layout
|
58
|
+
layout.render(self) do |layout_yield_argument|
|
59
|
+
template.render(self, :mode => layout_yield_argument, :toc => render_toc)
|
60
|
+
end
|
61
|
+
else
|
62
|
+
template.render(self, :mode => :content, :toc => render_toc)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
rescue StandardError => exc
|
66
|
+
if @site.continue_on_error
|
67
|
+
report_error(exc, options)
|
68
|
+
else
|
69
|
+
raise exc
|
70
|
+
end
|
71
|
+
ensure
|
72
|
+
@locals, @page = pop_context
|
73
|
+
I18n.locale = @locals[:locale]
|
74
|
+
end
|
75
|
+
|
76
|
+
def render_toc(options={}, locals={})
|
77
|
+
render(options, locals, true)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def find_file(path, site, page, locale)
|
83
|
+
search = [
|
84
|
+
path,
|
85
|
+
"#{site.pages_dir}/#{path}",
|
86
|
+
"#{page.file_path}/#{path}",
|
87
|
+
"#{File.dirname(page.file_path)}/#{path}",
|
88
|
+
"#{site.config_dir}/#{path}"
|
89
|
+
]
|
90
|
+
search.each do |path|
|
91
|
+
return path if File.exists?(path)
|
92
|
+
Dir["#{path}.#{locale}.*"].each do |path_with_locale|
|
93
|
+
return path_with_locale if File.exists?(path_with_locale)
|
94
|
+
end
|
95
|
+
Dir["#{path}.*"].each do |path_with_suffix|
|
96
|
+
return path_with_suffix if File.exists?(path_with_suffix)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
raise MissingTemplate.new(path)
|
100
|
+
end
|
101
|
+
|
102
|
+
def partialize(path)
|
103
|
+
File.dirname(path) + "/_" + File.basename(path)
|
104
|
+
end
|
105
|
+
|
106
|
+
def push_context(locals, page)
|
107
|
+
@stack.push([locals, page])
|
108
|
+
end
|
109
|
+
|
110
|
+
def pop_context
|
111
|
+
@stack.pop
|
112
|
+
end
|
113
|
+
|
114
|
+
#
|
115
|
+
# cleans up the `options` arg that is passed to render()
|
116
|
+
#
|
117
|
+
def parse_render_options(locale, options)
|
118
|
+
# handle non-hash options
|
119
|
+
if options.is_a?(String)
|
120
|
+
page = @site.find_page(options)
|
121
|
+
if page
|
122
|
+
options = {:page => page}
|
123
|
+
else
|
124
|
+
options = {:partial => options}
|
125
|
+
end
|
126
|
+
elsif options.is_a?(StaticPage)
|
127
|
+
options = {:page => options}
|
128
|
+
end
|
129
|
+
|
130
|
+
# convert :page, :partial, or :file to the real deal
|
131
|
+
if options[:page]
|
132
|
+
if options[:page].is_a?(String)
|
133
|
+
options[:page] = @site.find_page(options[:page])
|
134
|
+
end
|
135
|
+
options[:href_base] ||= page_path(options[:page], locale)
|
136
|
+
elsif options[:partial]
|
137
|
+
options[:partial] = find_file(partialize(options[:partial]), @site, @page, locale)
|
138
|
+
elsif options[:file]
|
139
|
+
options[:file] = find_file(options[:file], @site, @page, locale)
|
140
|
+
end
|
141
|
+
return options
|
142
|
+
end
|
143
|
+
|
144
|
+
def should_render_toc?(locale, options, page)
|
145
|
+
if options[:partial].nil?
|
146
|
+
if page.prop(locale, :toc).nil?
|
147
|
+
true
|
148
|
+
else
|
149
|
+
page.prop(locale, :toc)
|
150
|
+
end
|
151
|
+
else
|
152
|
+
false
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def pick_template(locale, options)
|
157
|
+
if options[:page]
|
158
|
+
Template.new(file: options[:page].content_file(locale))
|
159
|
+
elsif options[:file]
|
160
|
+
Template.new(file: options[:file])
|
161
|
+
elsif options[:partial]
|
162
|
+
Template.new(file: options[:partial], partial: true)
|
163
|
+
elsif options[:text]
|
164
|
+
Template.new(content: options[:text], type: (options[:type] || :text))
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def pick_layout(locale, options)
|
169
|
+
if options[:layout] && !options[:partial]
|
170
|
+
Render::Layout[options[:layout]]
|
171
|
+
else
|
172
|
+
nil
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def report_error(exc, options)
|
177
|
+
if exc.is_a? MissingTemplate
|
178
|
+
msg = "ERROR: render() could not find file from #{options.inspect}"
|
179
|
+
Amber.logger.error(msg)
|
180
|
+
Amber.log_exception(exc)
|
181
|
+
"<pre>%s</pre>" % [msg, exc, exc.backtrace].flatten.join("\n")
|
182
|
+
else
|
183
|
+
Amber.log_exception(exc)
|
184
|
+
"<pre>%s</pre>" % [exc, exc.backtrace].flatten.join("\n")
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
data/lib/amber/server.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
#
|
2
|
+
# A very simple http server for use when previewing pages.
|
3
|
+
#
|
4
|
+
|
5
|
+
require 'webrick'
|
6
|
+
|
7
|
+
module Amber
|
8
|
+
class Server
|
9
|
+
attr_reader :site
|
10
|
+
attr_reader :port
|
11
|
+
|
12
|
+
def self.start(options)
|
13
|
+
Server.new(options).start
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(options)
|
17
|
+
@site = options[:site]
|
18
|
+
@port = options[:port]
|
19
|
+
@server = WEBrick::HTTPServer.new :Port => @port, :BindAddress => '127.0.0.1', :DocumentRoot => @site.dest_dir
|
20
|
+
@server.mount '/', StaticPageServlet, self
|
21
|
+
end
|
22
|
+
|
23
|
+
def start
|
24
|
+
trap 'INT' do
|
25
|
+
@server.shutdown
|
26
|
+
end
|
27
|
+
@server.start
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class StaticPageServlet < WEBrick::HTTPServlet::FileHandler
|
32
|
+
|
33
|
+
def initialize(http_server, amber_server)
|
34
|
+
@logger = http_server.logger
|
35
|
+
@server = amber_server
|
36
|
+
super(http_server, @server.site.dest_dir, {:FanyIndexing => true})
|
37
|
+
end
|
38
|
+
|
39
|
+
def do_GET(request, response)
|
40
|
+
dest_dir = @server.site.dest_dir
|
41
|
+
|
42
|
+
# add local prefix for all paths that don't have a file suffix
|
43
|
+
if request.path !~ /\.[a-z]{2,4}$/
|
44
|
+
locale = get_locale(request)
|
45
|
+
if !locale
|
46
|
+
location = "http://localhost:#{@server.port}/#{I18n.default_locale}#{request.path.sub(/\/$/,'')}"
|
47
|
+
@logger.info "Redirect %s ==> %s" % [request.path, location]
|
48
|
+
response.header['Location'] = location
|
49
|
+
response.status = 307
|
50
|
+
return
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
if File.exists?(File.join(dest_dir, request.path))
|
55
|
+
@logger.info "Serve static file %s" % File.join(dest_dir, request.path)
|
56
|
+
super(request, response)
|
57
|
+
else
|
58
|
+
path = strip_locale(request.path)
|
59
|
+
@server.site.load_pages
|
60
|
+
page = @server.site.find_page_by_path(path)
|
61
|
+
if page
|
62
|
+
@logger.info "Serving Page %s" % page.path.join('/')
|
63
|
+
response.status = 200
|
64
|
+
response.content_type = "text/html; charset=utf-8"
|
65
|
+
# always refresh the page we are fetching
|
66
|
+
Amber::Render::Layout.reload
|
67
|
+
@server.site.render
|
68
|
+
page.render_to_file(dest_dir, :force => true)
|
69
|
+
file = page.destination_file(dest_dir, locale)
|
70
|
+
if File.exists?(file)
|
71
|
+
content = File.read(file)
|
72
|
+
else
|
73
|
+
file = page.destination_file(dest_dir, I18n.default_locale)
|
74
|
+
if File.exists?(file)
|
75
|
+
content = File.read(file)
|
76
|
+
else
|
77
|
+
view = Render::View.new(page, @server.site)
|
78
|
+
content = view.render(:text => "No file found at #{file}")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
response.body = content
|
82
|
+
else
|
83
|
+
request = request.clone
|
84
|
+
request.instance_variable_set(:@path_info, "/" + strip_locale(request.path_info))
|
85
|
+
@logger.info "Serve static file %s" % File.join(dest_dir, request.path_info)
|
86
|
+
super(request, response)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
#rescue Exception => exc
|
91
|
+
# @logger.error exc.to_s
|
92
|
+
# @logger.error exc.backtrace
|
93
|
+
# response.status = 500
|
94
|
+
# response.content_type = 'text/text'
|
95
|
+
# response.body = exc.to_s + "\n\n\n\n" + exc.backtrace
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def strip_locale(path)
|
100
|
+
path.sub(/^\/(#{Amber::POSSIBLE_LANGUAGE_CODES.join('|')})(\/|$)/, '').sub(/\/$/, '')
|
101
|
+
end
|
102
|
+
|
103
|
+
def get_locale(request)
|
104
|
+
match = /\/(#{Amber::POSSIBLE_LANGUAGE_CODES.join('|')})(\/|$)/.match(request.path)
|
105
|
+
if match.nil?
|
106
|
+
nil
|
107
|
+
else
|
108
|
+
match[1]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
data/lib/amber/site.rb
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
module Amber
|
5
|
+
class Site
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
attr_accessor :page_list
|
9
|
+
attr_accessor :root
|
10
|
+
attr_accessor :continue_on_error
|
11
|
+
|
12
|
+
def_delegators :@config, :dest_dir, :locales, :default_locale
|
13
|
+
|
14
|
+
def initialize(root_dir)
|
15
|
+
@continue_on_error = true
|
16
|
+
@config = SiteConfiguration.load(self, root_dir)
|
17
|
+
end
|
18
|
+
|
19
|
+
def load_pages
|
20
|
+
@root = nil
|
21
|
+
@pages_by_path = {}
|
22
|
+
@pages_by_name = {}
|
23
|
+
@page_list = PageArray.new
|
24
|
+
@dir_list = []
|
25
|
+
@config.mount_points.each do |mp|
|
26
|
+
add_mount_point(mp)
|
27
|
+
mp.reset_timestamp
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def reload_pages_if_needed
|
32
|
+
if @pages_by_path.nil? || @config.pages_changed?
|
33
|
+
puts "Reloading pages ................."
|
34
|
+
load_pages
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def render
|
39
|
+
@page_list.each do |page|
|
40
|
+
page.render_to_file(@config.dest_dir)
|
41
|
+
putc '.'; $stdout.flush
|
42
|
+
end
|
43
|
+
@dir_list.each do |directory|
|
44
|
+
src = File.join(@config.pages_dir, directory)
|
45
|
+
dst = File.join(@config.dest_dir, directory)
|
46
|
+
Render::Asset.render_dir(src, dst)
|
47
|
+
putc '.'; $stdout.flush
|
48
|
+
end
|
49
|
+
puts
|
50
|
+
end
|
51
|
+
|
52
|
+
def clear
|
53
|
+
Dir.glob("#{@config.dest_dir}/*").each do |file|
|
54
|
+
FileUtils.rm_r(file)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def with_destination(new_dest)
|
59
|
+
dest_dir = @config.dest_dir
|
60
|
+
@config.dest_dir = new_dest
|
61
|
+
yield
|
62
|
+
@config.dest_dir = dest_dir
|
63
|
+
end
|
64
|
+
|
65
|
+
def find_pages(filter)
|
66
|
+
filter = filter.downcase
|
67
|
+
if filter =~ /\//
|
68
|
+
path = filter.split('/').map{|segment| segment.gsub(/[^0-9a-z_-]/, '')}
|
69
|
+
path_str = path.join('/')
|
70
|
+
if (page = @pages_by_path[path_str])
|
71
|
+
page
|
72
|
+
elsif matched_path = @page_paths.grep(/#{Regexp.escape(path_str)}/).first
|
73
|
+
@pages_by_path[matched_path]
|
74
|
+
elsif page = @pages_by_name[path.last]
|
75
|
+
page
|
76
|
+
else
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
else
|
80
|
+
@pages_by_name[filter]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def find_page(filter)
|
85
|
+
find_pages(filter)
|
86
|
+
end
|
87
|
+
|
88
|
+
def all_pages
|
89
|
+
@page_list
|
90
|
+
end
|
91
|
+
|
92
|
+
def find_page_by_path(path)
|
93
|
+
@pages_by_path[path]
|
94
|
+
end
|
95
|
+
|
96
|
+
def find_page_by_name(name)
|
97
|
+
@pages_by_name[name]
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def add_mount_point(mp)
|
103
|
+
# create base_page
|
104
|
+
base_page = begin
|
105
|
+
if mp.path == '/'
|
106
|
+
@root = StaticPage.new(nil, 'root', mp.pages_dir)
|
107
|
+
add_page(@root)
|
108
|
+
@root
|
109
|
+
else
|
110
|
+
name = File.basename(mp.path)
|
111
|
+
page = StaticPage.new(find_parent(mp.path), name, mp.pages_dir)
|
112
|
+
add_page(page)
|
113
|
+
page
|
114
|
+
end
|
115
|
+
end
|
116
|
+
base_page.mount_point = mp
|
117
|
+
|
118
|
+
# load menu and locals
|
119
|
+
I18n.load_path += Dir[File.join(mp.locales_dir, '/*.{rb,yml,yaml}')] if mp.locales_dir
|
120
|
+
|
121
|
+
# add the full directory tree
|
122
|
+
base_page.scan_directory_tree do |page, asset_dir|
|
123
|
+
add_page(page) if page
|
124
|
+
@dir_list << asset_dir if asset_dir
|
125
|
+
end
|
126
|
+
@page_paths = @pages_by_path.keys
|
127
|
+
end
|
128
|
+
|
129
|
+
def add_page(page)
|
130
|
+
@pages_by_name[page.name] = page
|
131
|
+
@pages_by_path[page.path.join('/')] = page
|
132
|
+
page.aliases.each do |alias_path|
|
133
|
+
if @pages_by_path[alias_path]
|
134
|
+
Amber.logger.warn "WARNING: page `#{page.path.join('/')}` has alias `#{alias_path}`, but this path is already taken"
|
135
|
+
else
|
136
|
+
@pages_by_path[alias_path] = page
|
137
|
+
end
|
138
|
+
end
|
139
|
+
@page_list << page
|
140
|
+
end
|
141
|
+
|
142
|
+
def find_parent(path)
|
143
|
+
so_far = []
|
144
|
+
path.split('/').compact.each do |path_segment|
|
145
|
+
so_far << path_segment
|
146
|
+
if page = @pages_by_path[so_far.join('/')]
|
147
|
+
return page
|
148
|
+
end
|
149
|
+
end
|
150
|
+
return @root
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
end
|