amber 0.2.6
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/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
|